feat: delete all threads (#4446)
* add delete all threads * add testcase * add testcase * fix lint * fix linter * fix linter * change position Delete All Threads
This commit is contained in:
parent
154e9cc7fc
commit
ffec1cfde3
@ -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<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
|
||||
|
||||
@ -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,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('')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
@ -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))
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user