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;
onDragStart?: (chatId: string, event: React.DragEvent) => void;
activeWorkflowId?: string;
onCreateNew?: () => void;
chatListRefreshKey?: number;
onRenameChat?: (chatId: string, newName: string) => void | Promise<void>;
onDeleteChat?: (chatId: string) => void | Promise<void>;
}
@ -72,7 +72,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
onSelectChat,
onDragStart,
activeWorkflowId,
onCreateNew,
chatListRefreshKey,
onRenameChat,
onDeleteChat,
}) => {
@ -82,13 +82,14 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
const [search, setSearch] = useState('');
const [filter, setFilter] = useState<ChatFilter>('active');
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 [editName, setEditName] = useState('');
const renameInputRef = useRef<HTMLInputElement>(null);
const groupsRef = useRef(groups);
groupsRef.current = groups;
const _loadChats = useCallback(async (serverSearch?: string) => {
setLoading(true);
try {
const params: Record<string, unknown> = { includeArchived: true };
if (serverSearch) params.search = serverSearch;
@ -140,7 +141,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
} catch (err) {
console.error('Failed to load chats:', err);
} finally {
setLoading(false);
setHasLoadedOnce(true);
}
}, [context.instanceId, t]);
@ -163,6 +164,12 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
}
}, [activeWorkflowId]);
useEffect(() => {
if (chatListRefreshKey) {
_loadChats();
}
}, [chatListRefreshKey, _loadChats]);
useEffect(() => {
if (editingId && renameInputRef.current) {
renameInputRef.current.focus();
@ -188,8 +195,18 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
const trimmed = editName.trim();
setEditingId(null);
if (!trimmed || !onRenameChat) return;
await onRenameChat(chatId, trimmed);
_loadChats();
const prev = groupsRef.current;
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) => {
@ -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 prev = groupsRef.current;
_setChatStatus(chatId, 'archived');
try {
await api.patch(`/api/workspace/${context.instanceId}/workflows/${chatId}`, { status: 'archived' });
_loadChats();
} catch (err) {
console.error('Failed to archive chat:', err);
setGroups(prev);
}
}, [context.instanceId, _loadChats]);
}, [context.instanceId, _setChatStatus]);
const _restoreChat = useCallback(async (chatId: string) => {
const prev = groupsRef.current;
_setChatStatus(chatId, 'active');
try {
await api.patch(`/api/workspace/${context.instanceId}/workflows/${chatId}`, { status: 'active' });
_loadChats();
} catch (err) {
console.error('Failed to restore chat:', err);
setGroups(prev);
}
}, [context.instanceId, _loadChats]);
}, [context.instanceId, _setChatStatus]);
const _isArchived = (chat: ChatItem) => chat.status === 'archived';
@ -311,7 +346,17 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
{onDeleteChat && (
<button
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')}
>
🗑
@ -334,8 +379,6 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
return labels[code] || code;
};
if (loading) return <div className={styles.loading}>{t('Chats werden geladen…')}</div>;
return (
<div className={styles.chatsTab}>
<div className={styles.toolbar}>
@ -346,11 +389,6 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{onCreateNew && (
<button className={styles.createBtn} onClick={() => { onCreateNew(); setTimeout(_loadChats, 500); }} title={t('Neuer Chat')}>
+
</button>
)}
<button
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
onClick={() => setFlatMode(!flatMode)}
@ -437,7 +475,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
</div>
)}
{_allChats.length === 0 && (
{hasLoadedOnce && _allChats.length === 0 && (
<div className={styles.emptyState}>
{filter === 'archived' ? t('Keine archivierten Chats') : t('Keine aktiven Chats')}
</div>

View file

@ -47,8 +47,8 @@ interface UnifiedDataBarProps {
hideTabs?: UdbTab[];
onSelectChat?: (chatId: string, featureInstanceId: string) => void;
activeWorkflowId?: string;
onCreateNewChat?: () => void;
onRenameChat?: (chatId: string, newName: string) => void;
chatListRefreshKey?: number;
onDeleteChat?: (chatId: string) => void;
onChatDragStart?: (chatId: string, event: React.DragEvent) => void;
onFileSelect?: (fileId: string, fileName?: string) => void;
@ -78,8 +78,8 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
hideTabs,
onSelectChat,
activeWorkflowId,
onCreateNewChat,
onRenameChat,
chatListRefreshKey,
onDeleteChat,
onChatDragStart,
onFileSelect,
@ -122,7 +122,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
onSelectChat={onSelectChat}
onDragStart={onChatDragStart}
activeWorkflowId={activeWorkflowId}
onCreateNew={onCreateNewChat}
chatListRefreshKey={chatListRefreshKey}
onRenameChat={onRenameChat}
onDeleteChat={onDeleteChat}
/>

View file

@ -94,6 +94,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
);
const [mobileLeftOpen, setMobileLeftOpen] = useState(false);
const [mobileRightOpen, setMobileRightOpen] = useState(false);
const [chatListRefreshKey, setChatListRefreshKey] = useState(0);
useEffect(() => {
const _handleResize = () => {
@ -254,6 +255,27 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
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 => ({
flex: 1,
padding: '6px 0',
@ -356,7 +378,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
onTabChange={setUdbTab}
onSelectChat={_handleConversationSelect}
activeWorkflowId={workspace.workflowId ?? undefined}
onCreateNewChat={workspace.resetToNew}
chatListRefreshKey={chatListRefreshKey}
onRenameChat={_handleRenameChat}
onDeleteChat={_handleDeleteChat}
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' }}>
<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>
{_leftPanelBody}
</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' }}>
<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>
{_leftPanelBody}
</aside>