feat: New Chat, moved new chat button, fixed reloading animation to be more user friendly
This commit is contained in:
parent
8e67efa092
commit
ece5f17e2a
3 changed files with 92 additions and 26 deletions
|
|
@ -29,7 +29,7 @@ interface ChatsTabProps {
|
||||||
onSelectChat?: (chatId: string, featureInstanceId: string) => void;
|
onSelectChat?: (chatId: string, featureInstanceId: string) => void;
|
||||||
onDragStart?: (chatId: string, event: React.DragEvent) => void;
|
onDragStart?: (chatId: string, event: React.DragEvent) => void;
|
||||||
activeWorkflowId?: string;
|
activeWorkflowId?: string;
|
||||||
onCreateNew?: () => void;
|
chatListRefreshKey?: number;
|
||||||
onRenameChat?: (chatId: string, newName: string) => void | Promise<void>;
|
onRenameChat?: (chatId: string, newName: string) => void | Promise<void>;
|
||||||
onDeleteChat?: (chatId: string) => void | Promise<void>;
|
onDeleteChat?: (chatId: string) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
onSelectChat,
|
onSelectChat,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
onCreateNew,
|
chatListRefreshKey,
|
||||||
onRenameChat,
|
onRenameChat,
|
||||||
onDeleteChat,
|
onDeleteChat,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -82,13 +82,14 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [filter, setFilter] = useState<ChatFilter>('active');
|
const [filter, setFilter] = useState<ChatFilter>('active');
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||||
const [loading, setLoading] = useState(true);
|
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [editName, setEditName] = useState('');
|
const [editName, setEditName] = useState('');
|
||||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const groupsRef = useRef(groups);
|
||||||
|
groupsRef.current = groups;
|
||||||
|
|
||||||
const _loadChats = useCallback(async (serverSearch?: string) => {
|
const _loadChats = useCallback(async (serverSearch?: string) => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const params: Record<string, unknown> = { includeArchived: true };
|
const params: Record<string, unknown> = { includeArchived: true };
|
||||||
if (serverSearch) params.search = serverSearch;
|
if (serverSearch) params.search = serverSearch;
|
||||||
|
|
@ -140,7 +141,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load chats:', err);
|
console.error('Failed to load chats:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setHasLoadedOnce(true);
|
||||||
}
|
}
|
||||||
}, [context.instanceId, t]);
|
}, [context.instanceId, t]);
|
||||||
|
|
||||||
|
|
@ -163,6 +164,12 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
}
|
}
|
||||||
}, [activeWorkflowId]);
|
}, [activeWorkflowId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatListRefreshKey) {
|
||||||
|
_loadChats();
|
||||||
|
}
|
||||||
|
}, [chatListRefreshKey, _loadChats]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingId && renameInputRef.current) {
|
if (editingId && renameInputRef.current) {
|
||||||
renameInputRef.current.focus();
|
renameInputRef.current.focus();
|
||||||
|
|
@ -188,8 +195,18 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
const trimmed = editName.trim();
|
const trimmed = editName.trim();
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
if (!trimmed || !onRenameChat) return;
|
if (!trimmed || !onRenameChat) return;
|
||||||
await onRenameChat(chatId, trimmed);
|
const prev = groupsRef.current;
|
||||||
_loadChats();
|
setGroups(gs => gs.map(g => ({
|
||||||
|
...g,
|
||||||
|
chats: g.chats.map(c => (c.id === chatId ? { ...c, label: trimmed } : c)),
|
||||||
|
})));
|
||||||
|
try {
|
||||||
|
await onRenameChat(chatId, trimmed);
|
||||||
|
_loadChats();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to rename chat:', err);
|
||||||
|
setGroups(prev);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const _handleRenameKeyDown = (e: React.KeyboardEvent, chatId: string) => {
|
const _handleRenameKeyDown = (e: React.KeyboardEvent, chatId: string) => {
|
||||||
|
|
@ -201,23 +218,41 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _setChatStatus = useCallback((chatId: string, status: string) => {
|
||||||
|
setGroups(gs => gs.map(g => ({
|
||||||
|
...g,
|
||||||
|
chats: g.chats.map(c => (c.id === chatId ? { ...c, status } : c)),
|
||||||
|
})));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const _removeChat = useCallback((chatId: string) => {
|
||||||
|
setGroups(gs => gs.map(g => ({
|
||||||
|
...g,
|
||||||
|
chats: g.chats.filter(c => c.id !== chatId),
|
||||||
|
})).filter(g => g.chats.length > 0));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const _archiveChat = useCallback(async (chatId: string) => {
|
const _archiveChat = useCallback(async (chatId: string) => {
|
||||||
|
const prev = groupsRef.current;
|
||||||
|
_setChatStatus(chatId, 'archived');
|
||||||
try {
|
try {
|
||||||
await api.patch(`/api/workspace/${context.instanceId}/workflows/${chatId}`, { status: 'archived' });
|
await api.patch(`/api/workspace/${context.instanceId}/workflows/${chatId}`, { status: 'archived' });
|
||||||
_loadChats();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to archive chat:', err);
|
console.error('Failed to archive chat:', err);
|
||||||
|
setGroups(prev);
|
||||||
}
|
}
|
||||||
}, [context.instanceId, _loadChats]);
|
}, [context.instanceId, _setChatStatus]);
|
||||||
|
|
||||||
const _restoreChat = useCallback(async (chatId: string) => {
|
const _restoreChat = useCallback(async (chatId: string) => {
|
||||||
|
const prev = groupsRef.current;
|
||||||
|
_setChatStatus(chatId, 'active');
|
||||||
try {
|
try {
|
||||||
await api.patch(`/api/workspace/${context.instanceId}/workflows/${chatId}`, { status: 'active' });
|
await api.patch(`/api/workspace/${context.instanceId}/workflows/${chatId}`, { status: 'active' });
|
||||||
_loadChats();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to restore chat:', err);
|
console.error('Failed to restore chat:', err);
|
||||||
|
setGroups(prev);
|
||||||
}
|
}
|
||||||
}, [context.instanceId, _loadChats]);
|
}, [context.instanceId, _setChatStatus]);
|
||||||
|
|
||||||
const _isArchived = (chat: ChatItem) => chat.status === 'archived';
|
const _isArchived = (chat: ChatItem) => chat.status === 'archived';
|
||||||
|
|
||||||
|
|
@ -311,7 +346,17 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
{onDeleteChat && (
|
{onDeleteChat && (
|
||||||
<button
|
<button
|
||||||
className={`${styles.actionBtn} ${styles.actionBtnDanger}`}
|
className={`${styles.actionBtn} ${styles.actionBtnDanger}`}
|
||||||
onClick={async (e) => { e.stopPropagation(); await onDeleteChat(chat.id); _loadChats(); }}
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const prev = groupsRef.current;
|
||||||
|
_removeChat(chat.id);
|
||||||
|
try {
|
||||||
|
await onDeleteChat(chat.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete chat:', err);
|
||||||
|
setGroups(prev);
|
||||||
|
}
|
||||||
|
}}
|
||||||
title={t('Löschen')}
|
title={t('Löschen')}
|
||||||
>
|
>
|
||||||
🗑️
|
🗑️
|
||||||
|
|
@ -334,8 +379,6 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
return labels[code] || code;
|
return labels[code] || code;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <div className={styles.loading}>{t('Chats werden geladen…')}</div>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chatsTab}>
|
<div className={styles.chatsTab}>
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
|
|
@ -346,11 +389,6 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{onCreateNew && (
|
|
||||||
<button className={styles.createBtn} onClick={() => { onCreateNew(); setTimeout(_loadChats, 500); }} title={t('Neuer Chat')}>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
|
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
|
||||||
onClick={() => setFlatMode(!flatMode)}
|
onClick={() => setFlatMode(!flatMode)}
|
||||||
|
|
@ -437,7 +475,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{_allChats.length === 0 && (
|
{hasLoadedOnce && _allChats.length === 0 && (
|
||||||
<div className={styles.emptyState}>
|
<div className={styles.emptyState}>
|
||||||
{filter === 'archived' ? t('Keine archivierten Chats') : t('Keine aktiven Chats')}
|
{filter === 'archived' ? t('Keine archivierten Chats') : t('Keine aktiven Chats')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ interface UnifiedDataBarProps {
|
||||||
hideTabs?: UdbTab[];
|
hideTabs?: UdbTab[];
|
||||||
onSelectChat?: (chatId: string, featureInstanceId: string) => void;
|
onSelectChat?: (chatId: string, featureInstanceId: string) => void;
|
||||||
activeWorkflowId?: string;
|
activeWorkflowId?: string;
|
||||||
onCreateNewChat?: () => void;
|
|
||||||
onRenameChat?: (chatId: string, newName: string) => void;
|
onRenameChat?: (chatId: string, newName: string) => void;
|
||||||
|
chatListRefreshKey?: number;
|
||||||
onDeleteChat?: (chatId: string) => void;
|
onDeleteChat?: (chatId: string) => void;
|
||||||
onChatDragStart?: (chatId: string, event: React.DragEvent) => void;
|
onChatDragStart?: (chatId: string, event: React.DragEvent) => void;
|
||||||
onFileSelect?: (fileId: string, fileName?: string) => void;
|
onFileSelect?: (fileId: string, fileName?: string) => void;
|
||||||
|
|
@ -78,8 +78,8 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
|
||||||
hideTabs,
|
hideTabs,
|
||||||
onSelectChat,
|
onSelectChat,
|
||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
onCreateNewChat,
|
|
||||||
onRenameChat,
|
onRenameChat,
|
||||||
|
chatListRefreshKey,
|
||||||
onDeleteChat,
|
onDeleteChat,
|
||||||
onChatDragStart,
|
onChatDragStart,
|
||||||
onFileSelect,
|
onFileSelect,
|
||||||
|
|
@ -122,7 +122,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
|
||||||
onSelectChat={onSelectChat}
|
onSelectChat={onSelectChat}
|
||||||
onDragStart={onChatDragStart}
|
onDragStart={onChatDragStart}
|
||||||
activeWorkflowId={activeWorkflowId}
|
activeWorkflowId={activeWorkflowId}
|
||||||
onCreateNew={onCreateNewChat}
|
chatListRefreshKey={chatListRefreshKey}
|
||||||
onRenameChat={onRenameChat}
|
onRenameChat={onRenameChat}
|
||||||
onDeleteChat={onDeleteChat}
|
onDeleteChat={onDeleteChat}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
);
|
);
|
||||||
const [mobileLeftOpen, setMobileLeftOpen] = useState(false);
|
const [mobileLeftOpen, setMobileLeftOpen] = useState(false);
|
||||||
const [mobileRightOpen, setMobileRightOpen] = useState(false);
|
const [mobileRightOpen, setMobileRightOpen] = useState(false);
|
||||||
|
const [chatListRefreshKey, setChatListRefreshKey] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _handleResize = () => {
|
const _handleResize = () => {
|
||||||
|
|
@ -254,6 +255,27 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
workspace.loadWorkflow(wfId);
|
workspace.loadWorkflow(wfId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sidebarHeaderBtnStyle: React.CSSProperties = {
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#888',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChatBtnStyle: React.CSSProperties = {
|
||||||
|
...sidebarHeaderBtnStyle,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: 'var(--text-secondary, #555)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _handleCreateNewChat = useCallback(() => {
|
||||||
|
workspace.resetToNew();
|
||||||
|
setChatListRefreshKey(k => k + 1);
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
const tabButtonStyle = (active: boolean): React.CSSProperties => ({
|
const tabButtonStyle = (active: boolean): React.CSSProperties => ({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: '6px 0',
|
padding: '6px 0',
|
||||||
|
|
@ -356,7 +378,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
onTabChange={setUdbTab}
|
onTabChange={setUdbTab}
|
||||||
onSelectChat={_handleConversationSelect}
|
onSelectChat={_handleConversationSelect}
|
||||||
activeWorkflowId={workspace.workflowId ?? undefined}
|
activeWorkflowId={workspace.workflowId ?? undefined}
|
||||||
onCreateNewChat={workspace.resetToNew}
|
chatListRefreshKey={chatListRefreshKey}
|
||||||
onRenameChat={_handleRenameChat}
|
onRenameChat={_handleRenameChat}
|
||||||
onDeleteChat={_handleDeleteChat}
|
onDeleteChat={_handleDeleteChat}
|
||||||
onFileSelect={_handleFileSelect}
|
onFileSelect={_handleFileSelect}
|
||||||
|
|
@ -408,7 +430,10 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
}}>
|
}}>
|
||||||
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{t('Workspace')}</span>
|
<span style={{ fontWeight: 600, fontSize: 14 }}>{t('Workspace')}</span>
|
||||||
<button onClick={() => setLeftCollapsed(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: '#888' }}>◀</button>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<button onClick={_handleCreateNewChat} style={createChatBtnStyle} title={t('Neuer Chat')}>+</button>
|
||||||
|
<button onClick={() => setLeftCollapsed(true)} style={sidebarHeaderBtnStyle}>◀</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{_leftPanelBody}
|
{_leftPanelBody}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
@ -604,7 +629,10 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
>
|
>
|
||||||
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ padding: '10px 12px', borderBottom: '1px solid var(--border-color, #e0e0e0)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{t('Workspace')}</span>
|
<span style={{ fontWeight: 600, fontSize: 14 }}>{t('Workspace')}</span>
|
||||||
<button onClick={() => setMobileLeftOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 18, color: '#666' }}>×</button>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<button onClick={_handleCreateNewChat} style={createChatBtnStyle} title={t('Neuer Chat')}>+</button>
|
||||||
|
<button onClick={() => setMobileLeftOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 18, color: '#666' }}>×</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{_leftPanelBody}
|
{_leftPanelBody}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue