feat: New Chat, moved new chat button, fixed reloading animation to be more user friendly

This commit is contained in:
Ida 2026-05-27 10:25:09 +02:00
parent 8e67efa092
commit ece5f17e2a
3 changed files with 92 additions and 26 deletions

View file

@ -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>

View file

@ -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}
/> />

View file

@ -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>