From 3d9ebb0444895c32b24714fd133ebc84df346721 Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Tue, 14 Jan 2025 13:48:10 +0700 Subject: [PATCH 1/7] add delete all threads --- web/helpers/atoms/Thread.atom.ts | 3 +- web/hooks/useDeleteThread.ts | 21 ++++++- web/screens/Settings/Advanced/index.tsx | 35 ++++++++++- .../ModalDeleteAllThreads/index.tsx | 60 +++++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index 6704f8e577..474dadebae 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -11,6 +11,7 @@ import { ModelParams } from '@/types/model' export enum ThreadModalAction { Clean = 'clean', Delete = 'delete', + DeleteAll = 'deleteAll', EditTitle = 'edit-title', } @@ -272,7 +273,7 @@ export const activeSettingInputBoxAtom = atomWithStorage( ) /** - * Whether thread thread is presenting a Modal or not + * Whether thread is presenting a Modal or not */ export const modalActionThreadAtom = atom<{ showModal: ThreadModalAction | undefined diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index 29b5096315..e78bcc51e1 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { ExtensionTypeEnum, ConversationalExtension } from '@janhq/core' +import { ExtensionTypeEnum, ConversationalExtension, Thread } from '@janhq/core' import { useAtom, useSetAtom } from 'jotai' @@ -67,7 +67,7 @@ export default function useDeleteThread() { [deleteMessages, threads, updateThread] ) - const deleteThread = async (threadId: string) => { + const deleteThread = async (threadId: string, allThreads: boolean = false) => { if (!threadId) { alert('No active thread') return @@ -96,8 +96,25 @@ export default function useDeleteThread() { } } + const deleteAllThreads = async (threads: Thread[]) => { + for (const thread of threads) { + await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.deleteThread(thread.id as string) + .catch(console.error) + + deleteThreadState(thread.id as string) + deleteMessages(thread.id as string) + } + + setThreads([]) + setCurrentPrompt('') + setActiveThreadId(undefined) + } + return { cleanThread, deleteThread, + deleteAllThreads, } } diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 3dbb56a86f..6d4bd9aa32 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -11,9 +11,10 @@ import { Tooltip, Checkbox, useClickOutside, + Button, } from '@janhq/joi' -import { useAtom, useAtomValue } from 'jotai' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { ChevronDownIcon } from 'lucide-react' import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react' @@ -39,6 +40,10 @@ import { quickAskEnabledAtom, } from '@/helpers/atoms/AppConfig.atom' +import { ThreadModalAction } from '@/helpers/atoms/Thread.atom' +import { modalActionThreadAtom } from '@/helpers/atoms/Thread.atom' +import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads' + type GPU = { id: string vram: number | null @@ -74,6 +79,7 @@ const Advanced = () => { const { readSettings, saveSettings } = useSettings() const { stopModel } = useActiveModel() const [open, setOpen] = useState(false) + const setModalActionThread = useSetAtom(modalActionThreadAtom) const selectedGpu = gpuList .filter((x) => gpusInUse.includes(x.id)) @@ -525,6 +531,33 @@ const Advanced = () => { {/* Factory Reset */} + +
+
+
+
+ Delete All Threads +
+
+

+ Delete all threads and associated chat history. +

+
+ +
+ + ) diff --git a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx new file mode 100644 index 0000000000..a8bac48964 --- /dev/null +++ b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx @@ -0,0 +1,60 @@ +import { useCallback, memo } from 'react' +import { Modal, ModalClose, Button } from '@janhq/joi' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import useDeleteThread from '@/hooks/useDeleteThread' +import { modalActionThreadAtom, setActiveThreadIdAtom, threadDataReadyAtom, ThreadModalAction, threadsAtom } from '@/helpers/atoms/Thread.atom' +import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' + +const ModalDeleteAllThreads = () => { + const { deleteAllThreads } = useDeleteThread() + const [modalActionThread, setModalActionThread] = useAtom(modalActionThreadAtom) + const [threads] = useAtom(threadsAtom) + const janDataFolderPath = useAtomValue(janDataFolderPathAtom) + + const onDeleteAllThreads = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + deleteAllThreads(threads) + }, + [deleteAllThreads, threads, setModalActionThread] + ) + + const onCloseModal = useCallback(() => { + setModalActionThread({ + showModal: undefined, + thread: undefined, + }) + }, [setModalActionThread]) + + return ( + +

+ Are you sure you want to delete all chat history? This will remove all {threads.length} conversation + threads in {janDataFolderPath}\threads and cannot be undone. +

+
+ e.stopPropagation()}> + + + + + +
+ + } + /> + ) +} + +export default memo(ModalDeleteAllThreads) \ No newline at end of file From 48427e524955814bfe0719cd74359e1d07ff8873 Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Tue, 14 Jan 2025 14:26:01 +0700 Subject: [PATCH 2/7] add testcase --- web/hooks/useDeleteThread.test.ts | 50 +++++++++++++++++++ web/hooks/useDeleteThread.ts | 7 ++- .../ModalDeleteAllThreads/index.tsx | 4 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/web/hooks/useDeleteThread.test.ts b/web/hooks/useDeleteThread.test.ts index 4a1b4e5a24..d4f72e33a5 100644 --- a/web/hooks/useDeleteThread.test.ts +++ b/web/hooks/useDeleteThread.test.ts @@ -7,6 +7,10 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai' import useDeleteThread from './useDeleteThread' import { extensionManager } from '@/extension/ExtensionManager' import { useCreateNewThread } from './useCreateNewThread' +import { Thread } from '@janhq/core/dist/types/types' +import { currentPromptAtom } from '@/containers/Providers/Jotai' +import { setActiveThreadIdAtom, deleteThreadStateAtom } from '@/helpers/atoms/Thread.atom' +import { deleteChatMessageAtom as deleteChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' // Mock the necessary dependencies // Mock dependencies jest.mock('jotai', () => ({ @@ -117,4 +121,50 @@ describe('useDeleteThread', () => { consoleErrorSpy.mockRestore() }) + + it('should delete all threads successfully', async () => { + const mockThreads = [ + { id: 'thread1', title: 'Thread 1' }, + { id: 'thread2', title: 'Thread 2' }, + ] + const mockSetThreads = jest.fn() + ;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads]) + + // Tạo các mock functions riêng biệt + const mockSetCurrentPrompt = jest.fn() + const mockSetActiveThreadId = jest.fn() + const mockDeleteMessages = jest.fn() + const mockDeleteThreadState = jest.fn() + + // Mock useSetAtom cho từng atom riêng biệt + let currentAtom: any + ;(useSetAtom as jest.Mock).mockImplementation((atom) => { + currentAtom = atom + if (currentAtom === currentPromptAtom) return mockSetCurrentPrompt + if (currentAtom === setActiveThreadIdAtom) return mockSetActiveThreadId + if (currentAtom === deleteChatMessagesAtom) return mockDeleteMessages + if (currentAtom === deleteThreadStateAtom) return mockDeleteThreadState + return jest.fn() + }) + + const mockDeleteThread = jest.fn().mockImplementation(() => ({ + catch: () => jest.fn, + })) + + extensionManager.get = jest.fn().mockReturnValue({ + deleteThread: mockDeleteThread, + }) + + const { result } = renderHook(() => useDeleteThread()) + + await act(async () => { + await result.current.deleteAllThreads(mockThreads as Thread[]) + }) + + expect(mockDeleteThread).toHaveBeenCalledTimes(2) + expect(mockDeleteThread).toHaveBeenCalledWith('thread1') + expect(mockDeleteThread).toHaveBeenCalledWith('thread2') + expect(mockSetThreads).toHaveBeenCalledWith([]) + expect(mockSetCurrentPrompt).toHaveBeenCalledWith('') + }) }) diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index e78bcc51e1..d604209df5 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -67,7 +67,7 @@ export default function useDeleteThread() { [deleteMessages, threads, updateThread] ) - const deleteThread = async (threadId: string, allThreads: boolean = false) => { + const deleteThread = async (threadId: string) => { if (!threadId) { alert('No active thread') return @@ -110,6 +110,11 @@ export default function useDeleteThread() { setThreads([]) setCurrentPrompt('') setActiveThreadId(undefined) + toaster({ + title: 'All threads successfully deleted.', + description: `All thread data has been successfully deleted.`, + type: 'success', + }) } return { diff --git a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx index a8bac48964..fe42a8fa77 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx @@ -1,8 +1,8 @@ import { useCallback, memo } from 'react' import { Modal, ModalClose, Button } from '@janhq/joi' -import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import useDeleteThread from '@/hooks/useDeleteThread' -import { modalActionThreadAtom, setActiveThreadIdAtom, threadDataReadyAtom, ThreadModalAction, threadsAtom } from '@/helpers/atoms/Thread.atom' +import { modalActionThreadAtom, ThreadModalAction, threadsAtom } from '@/helpers/atoms/Thread.atom' import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' const ModalDeleteAllThreads = () => { From 7d4f9e7f568c80788c0324c77b2970b441d587a9 Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Tue, 14 Jan 2025 14:43:35 +0700 Subject: [PATCH 3/7] add testcase --- web/hooks/useDeleteThread.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/web/hooks/useDeleteThread.test.ts b/web/hooks/useDeleteThread.test.ts index d4f72e33a5..8d616cb426 100644 --- a/web/hooks/useDeleteThread.test.ts +++ b/web/hooks/useDeleteThread.test.ts @@ -130,20 +130,14 @@ describe('useDeleteThread', () => { const mockSetThreads = jest.fn() ;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads]) - // Tạo các mock functions riêng biệt + // create mock functions const mockSetCurrentPrompt = jest.fn() - const mockSetActiveThreadId = jest.fn() - const mockDeleteMessages = jest.fn() - const mockDeleteThreadState = jest.fn() - // Mock useSetAtom cho từng atom riêng biệt + // mock useSetAtom for each atom let currentAtom: any ;(useSetAtom as jest.Mock).mockImplementation((atom) => { currentAtom = atom if (currentAtom === currentPromptAtom) return mockSetCurrentPrompt - if (currentAtom === setActiveThreadIdAtom) return mockSetActiveThreadId - if (currentAtom === deleteChatMessagesAtom) return mockDeleteMessages - if (currentAtom === deleteThreadStateAtom) return mockDeleteThreadState return jest.fn() }) From a75bc6d3006564a99ac532b8077522be6ee468ac Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Tue, 14 Jan 2025 15:09:24 +0700 Subject: [PATCH 4/7] fix lint --- web/screens/Settings/Advanced/index.tsx | 5 +---- .../ModalDeleteAllThreads/index.tsx | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 6d4bd9aa32..a78e395ba8 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -535,9 +535,7 @@ const Advanced = () => {
-
- Delete All Threads -
+
Delete All Threads

Delete all threads and associated chat history. @@ -557,7 +555,6 @@ const Advanced = () => {

-
) diff --git a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx index fe42a8fa77..39d7d7a1a5 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx @@ -2,12 +2,18 @@ import { useCallback, memo } from 'react' import { Modal, ModalClose, Button } from '@janhq/joi' import { useAtom, useAtomValue } from 'jotai' import useDeleteThread from '@/hooks/useDeleteThread' -import { modalActionThreadAtom, ThreadModalAction, threadsAtom } from '@/helpers/atoms/Thread.atom' import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' +import { + modalActionThreadAtom, + ThreadModalAction, + threadsAtom, +} from '@/helpers/atoms/Thread.atom' const ModalDeleteAllThreads = () => { const { deleteAllThreads } = useDeleteThread() - const [modalActionThread, setModalActionThread] = useAtom(modalActionThreadAtom) + const [modalActionThread, setModalActionThread] = useAtom( + modalActionThreadAtom + ) const [threads] = useAtom(threadsAtom) const janDataFolderPath = useAtomValue(janDataFolderPathAtom) @@ -16,7 +22,7 @@ const ModalDeleteAllThreads = () => { e.stopPropagation() deleteAllThreads(threads) }, - [deleteAllThreads, threads, setModalActionThread] + [deleteAllThreads, threads] ) const onCloseModal = useCallback(() => { @@ -34,8 +40,10 @@ const ModalDeleteAllThreads = () => { content={

- Are you sure you want to delete all chat history? This will remove all {threads.length} conversation - threads in {janDataFolderPath}\threads and cannot be undone. + Are you sure you want to delete all chat history? This will remove{' '} + all {threads.length} conversation threads in{' '} + {janDataFolderPath}\threads and + cannot be undone.

e.stopPropagation()}> From c288c37d571b3eca479ee8cf2515582cb2f5cb1e Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Tue, 14 Jan 2025 15:18:44 +0700 Subject: [PATCH 5/7] fix linter --- web/hooks/useDeleteThread.ts | 1 - web/screens/Settings/Advanced/index.tsx | 4 +++- .../Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index d604209df5..59aa3a83b7 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -102,7 +102,6 @@ export default function useDeleteThread() { .get(ExtensionTypeEnum.Conversational) ?.deleteThread(thread.id as string) .catch(console.error) - deleteThreadState(thread.id as string) deleteMessages(thread.id as string) } diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index a78e395ba8..345b806233 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -28,6 +28,8 @@ import { useActiveModel } from '@/hooks/useActiveModel' import { useConfigurations } from '@/hooks/useConfigurations' import { useSettings } from '@/hooks/useSettings' +import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads' + import DataFolder from './DataFolder' import FactoryReset from './FactoryReset' @@ -41,8 +43,8 @@ import { } from '@/helpers/atoms/AppConfig.atom' import { ThreadModalAction } from '@/helpers/atoms/Thread.atom' + import { modalActionThreadAtom } from '@/helpers/atoms/Thread.atom' -import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads' type GPU = { id: string diff --git a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx index 39d7d7a1a5..c06dfc43a3 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx @@ -1,8 +1,13 @@ import { useCallback, memo } from 'react' + import { Modal, ModalClose, Button } from '@janhq/joi' + import { useAtom, useAtomValue } from 'jotai' + import useDeleteThread from '@/hooks/useDeleteThread' + import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' + import { modalActionThreadAtom, ThreadModalAction, @@ -65,4 +70,4 @@ const ModalDeleteAllThreads = () => { ) } -export default memo(ModalDeleteAllThreads) \ No newline at end of file +export default memo(ModalDeleteAllThreads) From a853458df40fa490c4e674f52f2aa607eafe1126 Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Tue, 14 Jan 2025 15:37:50 +0700 Subject: [PATCH 6/7] fix linter --- web/screens/Settings/Advanced/index.test.tsx | 9 +++++++++ web/screens/Settings/Advanced/index.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx index 6141fb44c9..7a0f3ade54 100644 --- a/web/screens/Settings/Advanced/index.test.tsx +++ b/web/screens/Settings/Advanced/index.test.tsx @@ -140,4 +140,13 @@ describe('Advanced', () => { expect(screen.getByTestId(/reset-button/i)).toBeInTheDocument() }) }) + + it('renders DeleteAllThreads component', async () => { + render() + await waitFor(() => { + const elements = screen.getAllByText('Delete All Threads') + expect(elements.length).toBeGreaterThan(0) + expect(screen.getByTestId('delete-all-threads-button')).toBeInTheDocument() + }) + }) }) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 345b806233..5faf302cb1 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -544,7 +544,7 @@ const Advanced = () => {

)} - {/* Factory Reset */} - - + {/* Delete All Threads */}
@@ -557,6 +555,9 @@ const Advanced = () => {
+ + {/* Factory Reset */} +
)