diff --git a/ui/__tests__/delete-collection.test.ts b/ui/__tests__/delete-collection.test.ts new file mode 100644 index 0000000..9839838 --- /dev/null +++ b/ui/__tests__/delete-collection.test.ts @@ -0,0 +1,84 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const deleteCollection = vi.fn(); +const chromaClient = vi.fn(() => ({ deleteCollection })); + +const loadHandler = async () => { + vi.resetModules(); + vi.doMock('chromadb', () => ({ + ChromaClient: chromaClient, + })); + + return (await import('@/pages/api/delete-collection')).default; +}; + +const createResponse = () => { + const res = { + setHeader: vi.fn(), + status: vi.fn(), + json: vi.fn(), + }; + + res.status.mockReturnValue(res); + + return res as unknown as NextApiResponse & typeof res; +}; + +describe('/api/delete-collection', () => { + afterEach(() => { + vi.clearAllMocks(); + delete process.env.CHROMA_PATH; + }); + + it('rejects non-DELETE requests', async () => { + const handler = await loadHandler(); + const req = { method: 'GET' } as NextApiRequest; + const res = createResponse(); + + await handler(req, res); + + expect(res.setHeader).toHaveBeenCalledWith('Allow', 'DELETE'); + expect(res.status).toHaveBeenCalledWith(405); + expect(res.json).toHaveBeenCalledWith({ error: 'Method not allowed' }); + expect(chromaClient).not.toHaveBeenCalled(); + }); + + it('deletes the default document collection via configured Chroma path', async () => { + const handler = await loadHandler(); + process.env.CHROMA_PATH = 'http://custom-chroma:8000'; + deleteCollection.mockResolvedValue(undefined); + + const req = { method: 'DELETE' } as NextApiRequest; + const res = createResponse(); + + await handler(req, res); + + expect(chromaClient).toHaveBeenCalledWith({ + path: 'http://custom-chroma:8000', + }); + expect(deleteCollection).toHaveBeenCalledWith({ + name: 'default-collection', + }); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: 'Uploaded document collection deleted.', + collection: 'default-collection', + }); + }); + + it('uses the Docker Chroma default when no path is configured', async () => { + const handler = await loadHandler(); + deleteCollection.mockResolvedValue(undefined); + + const req = { method: 'DELETE' } as NextApiRequest; + const res = createResponse(); + + await handler(req, res); + + expect(chromaClient).toHaveBeenCalledWith({ + path: 'http://chroma-server:8000', + }); + }); +}); diff --git a/ui/components/Chatbar/components/ChatbarSettings.tsx b/ui/components/Chatbar/components/ChatbarSettings.tsx index aed3369..bd361c6 100644 --- a/ui/components/Chatbar/components/ChatbarSettings.tsx +++ b/ui/components/Chatbar/components/ChatbarSettings.tsx @@ -12,6 +12,7 @@ import { Key } from '../../Settings/Key'; import { SidebarButton } from '../../Sidebar/SidebarButton'; import ChatbarContext from '../Chatbar.context'; import { ClearConversations } from './ClearConversations'; +import { ClearUploadedDocuments } from './ClearUploadedDocuments'; import { PluginKeys } from './PluginKeys'; export const ChatbarSettings = () => { @@ -44,6 +45,8 @@ export const ChatbarSettings = () => { + + {/* } diff --git a/ui/components/Chatbar/components/ClearUploadedDocuments.tsx b/ui/components/Chatbar/components/ClearUploadedDocuments.tsx new file mode 100644 index 0000000..27508d5 --- /dev/null +++ b/ui/components/Chatbar/components/ClearUploadedDocuments.tsx @@ -0,0 +1,76 @@ +import { IconCheck, IconFileX, IconX } from '@tabler/icons-react'; +import { FC, useState } from 'react'; +import toast from 'react-hot-toast'; + +import { useTranslation } from 'next-i18next'; + +import { SidebarButton } from '@/components/Sidebar/SidebarButton'; + +export const ClearUploadedDocuments: FC = () => { + const [isConfirming, setIsConfirming] = useState(false); + const [isClearing, setIsClearing] = useState(false); + + const { t } = useTranslation('sidebar'); + + const handleClearUploadedDocuments = async () => { + setIsClearing(true); + + try { + const response = await fetch('/api/delete-collection', { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + + toast.success('Uploaded documents cleared.'); + setIsConfirming(false); + } catch (error) { + console.error('Error clearing uploaded documents:', error); + toast.error('Unable to clear uploaded documents.'); + } finally { + setIsClearing(false); + } + }; + + return isConfirming ? ( +
+ + +
+ {isClearing ? t('Clearing...') : t('Clear uploaded files?')} +
+ +
+ { + e.stopPropagation(); + if (!isClearing) { + void handleClearUploadedDocuments(); + } + }} + /> + + { + e.stopPropagation(); + if (!isClearing) { + setIsConfirming(false); + } + }} + /> +
+
+ ) : ( + } + onClick={() => setIsConfirming(true)} + /> + ); +}; diff --git a/ui/pages/api/delete-collection.ts b/ui/pages/api/delete-collection.ts index 90fb745..c491c56 100644 --- a/ui/pages/api/delete-collection.ts +++ b/ui/pages/api/delete-collection.ts @@ -1,17 +1,30 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { ChromaClient, TransformersEmbeddingFunction } from "chromadb"; +import type { NextApiRequest, NextApiResponse } from 'next'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +import { ChromaClient } from 'chromadb'; + +const DEFAULT_CHROMA_PATH = 'http://chroma-server:8000'; +const DEFAULT_COLLECTION_NAME = 'default-collection'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { try { + if (req.method !== 'DELETE') { + res.setHeader('Allow', 'DELETE'); + return res.status(405).json({ error: 'Method not allowed' }); + } + const client = new ChromaClient({ - path: "http://localhost:8000", + path: process.env.CHROMA_PATH || DEFAULT_CHROMA_PATH, }); + await client.deleteCollection({ name: DEFAULT_COLLECTION_NAME }); - await client.deleteCollection({name: "default-collection"}) - - - res.status(200).json("Deleted collection."); + return res.status(200).json({ + message: 'Uploaded document collection deleted.', + collection: DEFAULT_COLLECTION_NAME, + }); } catch (error) { if (error instanceof Error) { console.error('Error message:', error.message); @@ -21,4 +34,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } res.status(500).json({ error: 'An unexpected error occurred :(' }); } -} \ No newline at end of file +}