Skip to content

Commit a2313c8

Browse files
committed
feat: Implement Clear All Conversations functionality
Adds a "Clear All Conversations" button to the Sidebar component. Key changes: - Sidebar: Adds UI button, confirmation dialog, and uses context to check for active generation before allowing deletion. Navigates to '/' on success. Updates useEffect dependency. - Storage: Implements `clearAllConversations` method using Dexie transaction. Modifies event system (CallbackConversationChanged, etc.) to handle `string | null` for signalling "clear all" events. Fixes `offConversationChanged` listener removal. - AppContext: Updates event listener (`handleConversationChange`) to accept `string | null` and refresh state correctly on `null`. Fixes `exhaustive-deps` warning.
1 parent c94085d commit a2313c8

File tree

6 files changed

+121
-18
lines changed

6 files changed

+121
-18
lines changed
1.06 KB
Binary file not shown.

examples/server/webui/package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/server/webui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"remark-math": "^6.0.0",
3434
"tailwindcss": "^4.1.1",
3535
"textlinestream": "^1.1.1",
36-
"vite-plugin-singlefile": "^2.0.3"
36+
"vite-plugin-singlefile": "^2.0.3",
37+
"webui": "file:"
3738
},
3839
"devDependencies": {
3940
"@eslint/js": "^9.17.0",

examples/server/webui/src/components/Sidebar.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,70 @@ import { classNames } from '../utils/misc';
33
import { Conversation } from '../utils/types';
44
import StorageUtils from '../utils/storage';
55
import { useNavigate, useParams } from 'react-router';
6+
import { useAppContext } from '../utils/app.context';
67

78
export default function Sidebar() {
89
const params = useParams();
910
const navigate = useNavigate();
11+
const { pendingMessages } = useAppContext();
1012

1113
const [conversations, setConversations] = useState<Conversation[]>([]);
1214
const [currConv, setCurrConv] = useState<Conversation | null>(null);
1315

16+
// Handler function for the clear all button
17+
const handleClearAll = async () => {
18+
const isAnyGenerating = Object.keys(pendingMessages).length > 0;
19+
if (isAnyGenerating) {
20+
alert(
21+
'Cannot clear conversations while message generation is in progress. Please wait or stop the generation.'
22+
);
23+
return; // Stop the function here
24+
}
25+
// Show confirmation dialog to the user
26+
const isConfirmed = window.confirm(
27+
'Are you sure you want to delete ALL conversations? This action cannot be undone.'
28+
);
29+
if (isConfirmed) {
30+
try {
31+
// Call the storage utility function to clear data
32+
await StorageUtils.clearAllConversations();
33+
// Navigate to the home/new conversation page after clearing
34+
// The onConversationChanged listener will handle updating the 'conversations' state automatically
35+
navigate('/');
36+
} catch (error) {
37+
console.error('Failed to clear conversations:', error);
38+
alert('Failed to clear conversations. See console for details.');
39+
}
40+
}
41+
};
42+
1443
useEffect(() => {
1544
StorageUtils.getOneConversation(params.convId ?? '').then(setCurrConv);
1645
}, [params.convId]);
1746

1847
useEffect(() => {
1948
const handleConversationChange = async () => {
49+
// Always refresh the full list
2050
setConversations(await StorageUtils.getAllConversations());
51+
52+
// Check if the currently selected conversation still exists after a change (deletion/clear all)
53+
if (currConv?.id) {
54+
// Check if there *was* a selected conversation
55+
const stillExists = await StorageUtils.getOneConversation(currConv.id);
56+
if (!stillExists) {
57+
// If the current conv was deleted/cleared, update the local state for highlighting
58+
setCurrConv(null);
59+
// Navigation happens via handleClearAll or if user manually deletes and stays on the page
60+
}
61+
}
2162
};
2263
StorageUtils.onConversationChanged(handleConversationChange);
2364
handleConversationChange();
2465
return () => {
2566
StorageUtils.offConversationChanged(handleConversationChange);
2667
};
27-
}, []);
68+
// Dependency added to re-check existence if currConv changes while mounted
69+
}, [currConv]); // Changed dependency from [] to [currConv]
2870

2971
return (
3072
<>
@@ -41,7 +83,7 @@ export default function Sidebar() {
4183
aria-label="close sidebar"
4284
className="drawer-overlay"
4385
></label>
44-
<div className="flex flex-col bg-base-200 min-h-full max-w-64 py-4 px-4">
86+
<div className="flex flex-col bg-base-200 min-h-full max-w-[100%] py-4 px-4">
4587
<div className="flex flex-row items-center justify-between mb-4 mt-4">
4688
<h2 className="font-bold ml-4">Conversations</h2>
4789

@@ -77,18 +119,42 @@ export default function Sidebar() {
77119
<div
78120
key={conv.id}
79121
className={classNames({
80-
'btn btn-ghost justify-start font-normal': true,
122+
// 'btn btn-ghost justify-start font-normal w-full overflow-hidden',
123+
'btn btn-ghost justify-start font-normal': true,
81124
'btn-active': conv.id === currConv?.id,
125+
// Additional styles for active conversation
126+
'border-1 border-blue-400': conv.id === currConv?.id,
82127
})}
83128
onClick={() => navigate(`/chat/${conv.id}`)}
84129
dir="auto"
85130
>
86131
<span className="truncate">{conv.name}</span>
87132
</div>
88133
))}
89-
<div className="text-center text-xs opacity-40 mt-auto mx-4">
134+
<div className="text-center text-xs opacity-40 mt-auto mx-4 pb-2 ">
90135
Conversations are saved to browser's IndexedDB
91136
</div>
137+
{/* Clear All Button - Added */}
138+
{conversations.length > 0 && ( // Only show if there are conversations to clear
139+
<button
140+
className="btn btn-outline btn-error btn-sm w-full mb-3 pb-1"
141+
onClick={handleClearAll}
142+
title="Conversations are saved to browser's IndexedDB"
143+
>
144+
Clear All
145+
<svg
146+
xmlns="http://www.w3.org/2000/svg"
147+
width="16"
148+
height="16"
149+
fill="currentColor"
150+
className="bi bi-trash ml-2"
151+
viewBox="0 0 16 16"
152+
>
153+
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z" />
154+
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z" />
155+
</svg>
156+
</button>
157+
)}
92158
</div>
93159
</div>
94160
</>

examples/server/webui/src/utils/app.context.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,22 @@ export const AppContextProvider = ({
8989
useEffect(() => {
9090
// also reset the canvas data
9191
setCanvasData(null);
92-
const handleConversationChange = async (changedConvId: string) => {
93-
if (changedConvId !== convId) return;
94-
setViewingChat(await getViewingChat(changedConvId));
92+
const handleConversationChange = async (changedConvId: string | null) => {
93+
// Refresh if the change affects the current viewing conversation OR if all conversations were cleared (null)
94+
if (changedConvId === convId || changedConvId === null) {
95+
// Re-fetch data for the current URL's convId (which might be undefined now)
96+
const currentUrlConvId = params?.params?.convId;
97+
// Ensure getViewingChat can handle potential undefined/null input if needed, or provide fallback like ''
98+
setViewingChat(await getViewingChat(currentUrlConvId ?? '')); // Use currentUrlConvId
99+
}
100+
// Otherwise, ignore changes for conversations not being viewed.
95101
};
96102
StorageUtils.onConversationChanged(handleConversationChange);
97103
getViewingChat(convId ?? '').then(setViewingChat);
98104
return () => {
99105
StorageUtils.offConversationChanged(handleConversationChange);
100106
};
101-
}, [convId]);
107+
}, [convId, params?.params?.convId]);
102108

103109
const setPending = (convId: string, pendingMsg: PendingMessage | null) => {
104110
// if pendingMsg is null, remove the key from the object

examples/server/webui/src/utils/storage.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { Conversation, Message, TimingReport } from './types';
66
import Dexie, { Table } from 'dexie';
77

88
const event = new EventTarget();
9-
10-
type CallbackConversationChanged = (convId: string) => void;
9+
// Modify callback type to accept null for "clear all" events
10+
type CallbackConversationChanged = (convId: string | null) => void;
1111
let onConversationChangedHandlers: [
1212
CallbackConversationChanged,
1313
EventListener,
1414
][] = [];
15-
const dispatchConversationChange = (convId: string) => {
15+
const dispatchConversationChange = (convId: string | null) => {
1616
event.dispatchEvent(
1717
new CustomEvent('conversationChange', { detail: { convId } })
1818
);
@@ -167,18 +167,43 @@ const StorageUtils = {
167167
dispatchConversationChange(convId);
168168
},
169169

170+
/**
171+
* Added function to clear all conversation data.
172+
*/
173+
async clearAllConversations(): Promise<void> {
174+
try {
175+
await db.transaction('rw', db.conversations, db.messages, async () => {
176+
await db.conversations.clear(); // Clear conversations table
177+
await db.messages.clear(); // Clear messages table
178+
});
179+
console.log('All conversations cleared.');
180+
// Dispatch change with null to indicate everything was cleared
181+
dispatchConversationChange(null);
182+
} catch (error) {
183+
console.error('Failed to clear all conversations:', error);
184+
throw error; // Re-throw error for potential handling by the caller
185+
}
186+
},
187+
170188
// event listeners
171189
onConversationChanged(callback: CallbackConversationChanged) {
172-
const fn = (e: Event) => callback((e as CustomEvent).detail.convId);
190+
// Ensure the event listener correctly handles the detail (string | null)
191+
const fn = (e: Event) =>
192+
callback((e as CustomEvent).detail.convId as string | null);
173193
onConversationChangedHandlers.push([callback, fn]);
174194
event.addEventListener('conversationChange', fn);
175195
},
176196
offConversationChanged(callback: CallbackConversationChanged) {
177-
const fn = onConversationChangedHandlers.find(([cb, _]) => cb === callback);
178-
if (fn) {
179-
event.removeEventListener('conversationChange', fn[1]);
197+
const handlerTuple = onConversationChangedHandlers.find(
198+
([cb]) => cb === callback
199+
);
200+
if (handlerTuple) {
201+
event.removeEventListener('conversationChange', handlerTuple[1]);
202+
// Filter out the specific handler, don't reset the whole array
203+
onConversationChangedHandlers = onConversationChangedHandlers.filter(
204+
(tuple) => tuple[0] !== callback
205+
);
180206
}
181-
onConversationChangedHandlers = [];
182207
},
183208

184209
// manage config

0 commit comments

Comments
 (0)