Skip to content

Commit

Permalink
feat: delete all threads (#4446)
Browse files Browse the repository at this point in the history
* add delete all threads

* add testcase

* add testcase

* fix lint

* fix linter

* fix linter

* change position Delete All Threads
  • Loading branch information
bxdoan authored Jan 15, 2025
1 parent 154e9cc commit ffec1cf
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 3 deletions.
3 changes: 2 additions & 1 deletion web/helpers/atoms/Thread.atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ModelParams } from '@/types/model'
export enum ThreadModalAction {
Clean = 'clean',
Delete = 'delete',
DeleteAll = 'deleteAll',
EditTitle = 'edit-title',
}

Expand Down Expand Up @@ -272,7 +273,7 @@ export const activeSettingInputBoxAtom = atomWithStorage<boolean>(
)

/**
* Whether thread thread is presenting a Modal or not
* Whether thread is presenting a Modal or not
*/
export const modalActionThreadAtom = atom<{
showModal: ThreadModalAction | undefined
Expand Down
44 changes: 44 additions & 0 deletions web/hooks/useDeleteThread.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -117,4 +121,44 @@ 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])

// create mock functions
const mockSetCurrentPrompt = jest.fn()

// mock useSetAtom for each atom
let currentAtom: any
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
currentAtom = atom
if (currentAtom === currentPromptAtom) return mockSetCurrentPrompt
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('')
})
})
23 changes: 22 additions & 1 deletion web/hooks/useDeleteThread.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -96,8 +96,29 @@ export default function useDeleteThread() {
}
}

const deleteAllThreads = async (threads: Thread[]) => {
for (const thread of threads) {
await extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.deleteThread(thread.id as string)
.catch(console.error)
deleteThreadState(thread.id as string)
deleteMessages(thread.id as string)
}

setThreads([])
setCurrentPrompt('')
setActiveThreadId(undefined)
toaster({
title: 'All threads successfully deleted.',
description: `All thread data has been successfully deleted.`,
type: 'success',
})
}

return {
cleanThread,
deleteThread,
deleteAllThreads,
}
}
9 changes: 9 additions & 0 deletions web/screens/Settings/Advanced/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,13 @@ describe('Advanced', () => {
expect(screen.getByTestId(/reset-button/i)).toBeInTheDocument()
})
})

it('renders DeleteAllThreads component', async () => {
render(<Advanced />)
await waitFor(() => {
const elements = screen.getAllByText('Delete All Threads')
expect(elements.length).toBeGreaterThan(0)
expect(screen.getByTestId('delete-all-threads-button')).toBeInTheDocument()
})
})
})
35 changes: 34 additions & 1 deletion web/screens/Settings/Advanced/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -27,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'

Expand All @@ -39,6 +42,10 @@ import {
quickAskEnabledAtom,
} from '@/helpers/atoms/AppConfig.atom'

import { ThreadModalAction } from '@/helpers/atoms/Thread.atom'

import { modalActionThreadAtom } from '@/helpers/atoms/Thread.atom'

type GPU = {
id: string
vram: number | null
Expand Down Expand Up @@ -74,6 +81,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))
Expand Down Expand Up @@ -523,6 +531,31 @@ const Advanced = () => {
</div>
)}

{/* Delete All Threads */}
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
<div className="space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Delete All Threads</h6>
</div>
<p className="whitespace-pre-wrap font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
Delete all threads and associated chat history.
</p>
</div>
<Button
data-testid="delete-all-threads-button"
theme="destructive"
onClick={() => {
setModalActionThread({
showModal: ThreadModalAction.DeleteAll,
thread: undefined,
})
}}
>
Delete All Threads
</Button>
</div>
<ModalDeleteAllThreads />

{/* Factory Reset */}
<FactoryReset />
</div>
Expand Down
73 changes: 73 additions & 0 deletions web/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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,
threadsAtom,
} from '@/helpers/atoms/Thread.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<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
deleteAllThreads(threads)
},
[deleteAllThreads, threads]
)

const onCloseModal = useCallback(() => {
setModalActionThread({
showModal: undefined,
thread: undefined,
})
}, [setModalActionThread])

return (
<Modal
title="Delete All Threads"
onOpenChange={onCloseModal}
open={modalActionThread.showModal === ThreadModalAction.DeleteAll}
content={
<div>
<p className="text-[hsla(var(--text-secondary))]">
Are you sure you want to delete all chat history? This will remove{' '}
all {threads.length} conversation threads in{' '}
<span className="font-mono">{janDataFolderPath}\threads</span> and
cannot be undone.
</p>
<div className="mt-4 flex justify-end gap-x-2">
<ModalClose asChild onClick={(e) => e.stopPropagation()}>
<Button theme="ghost">Cancel</Button>
</ModalClose>
<ModalClose asChild>
<Button
autoFocus
theme="destructive"
onClick={onDeleteAllThreads}
>
Delete
</Button>
</ModalClose>
</div>
</div>
}
/>
)
}

export default memo(ModalDeleteAllThreads)

0 comments on commit ffec1cf

Please sign in to comment.