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 {
|
export enum ThreadModalAction {
|
||||||
Clean = 'clean',
|
Clean = 'clean',
|
||||||
Delete = 'delete',
|
Delete = 'delete',
|
||||||
|
DeleteAll = 'deleteAll',
|
||||||
EditTitle = 'edit-title',
|
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<{
|
export const modalActionThreadAtom = atom<{
|
||||||
showModal: ThreadModalAction | undefined
|
showModal: ThreadModalAction | undefined
|
||||||
|
|||||||
@ -7,6 +7,10 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
|||||||
import useDeleteThread from './useDeleteThread'
|
import useDeleteThread from './useDeleteThread'
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import { useCreateNewThread } from './useCreateNewThread'
|
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 the necessary dependencies
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock('jotai', () => ({
|
jest.mock('jotai', () => ({
|
||||||
@ -117,4 +121,44 @@ describe('useDeleteThread', () => {
|
|||||||
|
|
||||||
consoleErrorSpy.mockRestore()
|
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 { useCallback } from 'react'
|
||||||
|
|
||||||
import { ExtensionTypeEnum, ConversationalExtension } from '@janhq/core'
|
import { ExtensionTypeEnum, ConversationalExtension, Thread } from '@janhq/core'
|
||||||
|
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
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 {
|
return {
|
||||||
cleanThread,
|
cleanThread,
|
||||||
deleteThread,
|
deleteThread,
|
||||||
|
deleteAllThreads,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,4 +140,13 @@ describe('Advanced', () => {
|
|||||||
expect(screen.getByTestId(/reset-button/i)).toBeInTheDocument()
|
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,
|
Tooltip,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
useClickOutside,
|
useClickOutside,
|
||||||
|
Button,
|
||||||
} from '@janhq/joi'
|
} from '@janhq/joi'
|
||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { ChevronDownIcon } from 'lucide-react'
|
import { ChevronDownIcon } from 'lucide-react'
|
||||||
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
||||||
|
|
||||||
@ -27,6 +28,8 @@ import { useActiveModel } from '@/hooks/useActiveModel'
|
|||||||
import { useConfigurations } from '@/hooks/useConfigurations'
|
import { useConfigurations } from '@/hooks/useConfigurations'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
|
||||||
|
import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads'
|
||||||
|
|
||||||
import DataFolder from './DataFolder'
|
import DataFolder from './DataFolder'
|
||||||
import FactoryReset from './FactoryReset'
|
import FactoryReset from './FactoryReset'
|
||||||
|
|
||||||
@ -39,6 +42,10 @@ import {
|
|||||||
quickAskEnabledAtom,
|
quickAskEnabledAtom,
|
||||||
} from '@/helpers/atoms/AppConfig.atom'
|
} from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
|
import { ThreadModalAction } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
import { modalActionThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
type GPU = {
|
type GPU = {
|
||||||
id: string
|
id: string
|
||||||
vram: number | null
|
vram: number | null
|
||||||
@ -74,6 +81,7 @@ const Advanced = () => {
|
|||||||
const { readSettings, saveSettings } = useSettings()
|
const { readSettings, saveSettings } = useSettings()
|
||||||
const { stopModel } = useActiveModel()
|
const { stopModel } = useActiveModel()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const setModalActionThread = useSetAtom(modalActionThreadAtom)
|
||||||
|
|
||||||
const selectedGpu = gpuList
|
const selectedGpu = gpuList
|
||||||
.filter((x) => gpusInUse.includes(x.id))
|
.filter((x) => gpusInUse.includes(x.id))
|
||||||
@ -523,6 +531,31 @@ const Advanced = () => {
|
|||||||
</div>
|
</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 */}
|
{/* Factory Reset */}
|
||||||
<FactoryReset />
|
<FactoryReset />
|
||||||
</div>
|
</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