Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete all threads #4446

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading