diff --git a/examples/server/public/index.html.gz b/examples/server/public/index.html.gz index 674e227571e2d..a27ce3c504fd3 100644 Binary files a/examples/server/public/index.html.gz and b/examples/server/public/index.html.gz differ diff --git a/examples/server/webui/package-lock.json b/examples/server/webui/package-lock.json index b2e3cf94aca41..cb1a384d39c5f 100644 --- a/examples/server/webui/package-lock.json +++ b/examples/server/webui/package-lock.json @@ -30,7 +30,8 @@ "remark-math": "^6.0.0", "tailwindcss": "^4.1.1", "textlinestream": "^1.1.1", - "vite-plugin-singlefile": "^2.0.3" + "vite-plugin-singlefile": "^2.0.3", + "webui": "file:" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -6181,6 +6182,10 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/webui": { + "resolved": "", + "link": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/examples/server/webui/package.json b/examples/server/webui/package.json index 6ac06b1a49bd3..8cda9fd50fe5c 100644 --- a/examples/server/webui/package.json +++ b/examples/server/webui/package.json @@ -33,7 +33,8 @@ "remark-math": "^6.0.0", "tailwindcss": "^4.1.1", "textlinestream": "^1.1.1", - "vite-plugin-singlefile": "^2.0.3" + "vite-plugin-singlefile": "^2.0.3", + "webui": "file:" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/examples/server/webui/src/components/Sidebar.tsx b/examples/server/webui/src/components/Sidebar.tsx index 34727c6231c97..b71449bec8e19 100644 --- a/examples/server/webui/src/components/Sidebar.tsx +++ b/examples/server/webui/src/components/Sidebar.tsx @@ -3,28 +3,70 @@ import { classNames } from '../utils/misc'; import { Conversation } from '../utils/types'; import StorageUtils from '../utils/storage'; import { useNavigate, useParams } from 'react-router'; +import { useAppContext } from '../utils/app.context'; export default function Sidebar() { const params = useParams(); const navigate = useNavigate(); + const { pendingMessages } = useAppContext(); const [conversations, setConversations] = useState([]); const [currConv, setCurrConv] = useState(null); + // Handler function for the clear all button + const handleClearAll = async () => { + const isAnyGenerating = Object.keys(pendingMessages).length > 0; + if (isAnyGenerating) { + alert( + 'Cannot clear conversations while message generation is in progress. Please wait or stop the generation.' + ); + return; // Stop the function here + } + // Show confirmation dialog to the user + const isConfirmed = window.confirm( + 'Are you sure you want to delete ALL conversations? This action cannot be undone.' + ); + if (isConfirmed) { + try { + // Call the storage utility function to clear data + await StorageUtils.clearAllConversations(); + // Navigate to the home/new conversation page after clearing + // The onConversationChanged listener will handle updating the 'conversations' state automatically + navigate('/'); + } catch (error) { + console.error('Failed to clear conversations:', error); + alert('Failed to clear conversations. See console for details.'); + } + } + }; + useEffect(() => { StorageUtils.getOneConversation(params.convId ?? '').then(setCurrConv); }, [params.convId]); useEffect(() => { const handleConversationChange = async () => { + // Always refresh the full list setConversations(await StorageUtils.getAllConversations()); + + // Check if the currently selected conversation still exists after a change (deletion/clear all) + if (currConv?.id) { + // Check if there *was* a selected conversation + const stillExists = await StorageUtils.getOneConversation(currConv.id); + if (!stillExists) { + // If the current conv was deleted/cleared, update the local state for highlighting + setCurrConv(null); + // Navigation happens via handleClearAll or if user manually deletes and stays on the page + } + } }; StorageUtils.onConversationChanged(handleConversationChange); handleConversationChange(); return () => { StorageUtils.offConversationChanged(handleConversationChange); }; - }, []); + // Dependency added to re-check existence if currConv changes while mounted + }, [currConv]); // Changed dependency from [] to [currConv] return ( <> @@ -41,7 +83,7 @@ export default function Sidebar() { aria-label="close sidebar" className="drawer-overlay" > -
+

Conversations

@@ -77,8 +119,11 @@ export default function Sidebar() {
navigate(`/chat/${conv.id}`)} dir="auto" @@ -86,9 +131,30 @@ export default function Sidebar() { {conv.name}
))} -
+
Conversations are saved to browser's IndexedDB
+ {/* Clear All Button - Added */} + {conversations.length > 0 && ( // Only show if there are conversations to clear + + )}
diff --git a/examples/server/webui/src/utils/app.context.tsx b/examples/server/webui/src/utils/app.context.tsx index 54bb65b6e3cb2..df641aa1502ba 100644 --- a/examples/server/webui/src/utils/app.context.tsx +++ b/examples/server/webui/src/utils/app.context.tsx @@ -89,16 +89,22 @@ export const AppContextProvider = ({ useEffect(() => { // also reset the canvas data setCanvasData(null); - const handleConversationChange = async (changedConvId: string) => { - if (changedConvId !== convId) return; - setViewingChat(await getViewingChat(changedConvId)); + const handleConversationChange = async (changedConvId: string | null) => { + // Refresh if the change affects the current viewing conversation OR if all conversations were cleared (null) + if (changedConvId === convId || changedConvId === null) { + // Re-fetch data for the current URL's convId (which might be undefined now) + const currentUrlConvId = params?.params?.convId; + // Ensure getViewingChat can handle potential undefined/null input if needed, or provide fallback like '' + setViewingChat(await getViewingChat(currentUrlConvId ?? '')); // Use currentUrlConvId + } + // Otherwise, ignore changes for conversations not being viewed. }; StorageUtils.onConversationChanged(handleConversationChange); getViewingChat(convId ?? '').then(setViewingChat); return () => { StorageUtils.offConversationChanged(handleConversationChange); }; - }, [convId]); + }, [convId, params?.params?.convId]); const setPending = (convId: string, pendingMsg: PendingMessage | null) => { // if pendingMsg is null, remove the key from the object diff --git a/examples/server/webui/src/utils/storage.ts b/examples/server/webui/src/utils/storage.ts index 1dfc9d9799311..2eff4d74a70e7 100644 --- a/examples/server/webui/src/utils/storage.ts +++ b/examples/server/webui/src/utils/storage.ts @@ -6,13 +6,13 @@ import { Conversation, Message, TimingReport } from './types'; import Dexie, { Table } from 'dexie'; const event = new EventTarget(); - -type CallbackConversationChanged = (convId: string) => void; +// Modify callback type to accept null for "clear all" events +type CallbackConversationChanged = (convId: string | null) => void; let onConversationChangedHandlers: [ CallbackConversationChanged, EventListener, ][] = []; -const dispatchConversationChange = (convId: string) => { +const dispatchConversationChange = (convId: string | null) => { event.dispatchEvent( new CustomEvent('conversationChange', { detail: { convId } }) ); @@ -167,18 +167,43 @@ const StorageUtils = { dispatchConversationChange(convId); }, + /** + * Added function to clear all conversation data. + */ + async clearAllConversations(): Promise { + try { + await db.transaction('rw', db.conversations, db.messages, async () => { + await db.conversations.clear(); // Clear conversations table + await db.messages.clear(); // Clear messages table + }); + console.log('All conversations cleared.'); + // Dispatch change with null to indicate everything was cleared + dispatchConversationChange(null); + } catch (error) { + console.error('Failed to clear all conversations:', error); + throw error; // Re-throw error for potential handling by the caller + } + }, + // event listeners onConversationChanged(callback: CallbackConversationChanged) { - const fn = (e: Event) => callback((e as CustomEvent).detail.convId); + // Ensure the event listener correctly handles the detail (string | null) + const fn = (e: Event) => + callback((e as CustomEvent).detail.convId as string | null); onConversationChangedHandlers.push([callback, fn]); event.addEventListener('conversationChange', fn); }, offConversationChanged(callback: CallbackConversationChanged) { - const fn = onConversationChangedHandlers.find(([cb, _]) => cb === callback); - if (fn) { - event.removeEventListener('conversationChange', fn[1]); + const handlerTuple = onConversationChangedHandlers.find( + ([cb]) => cb === callback + ); + if (handlerTuple) { + event.removeEventListener('conversationChange', handlerTuple[1]); + // Filter out the specific handler, don't reset the whole array + onConversationChangedHandlers = onConversationChangedHandlers.filter( + (tuple) => tuple[0] !== callback + ); } - onConversationChangedHandlers = []; }, // manage config