Skip to content
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
84 changes: 84 additions & 0 deletions ui/__tests__/delete-collection.test.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
3 changes: 3 additions & 0 deletions ui/components/Chatbar/components/ChatbarSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -44,6 +45,8 @@ export const ChatbarSettings = () => {

<Import onImport={handleImportConversations} />

<ClearUploadedDocuments />

{/* <SidebarButton
text={t('Export data')}
icon={<IconFileExport size={18} />}
Expand Down
76 changes: 76 additions & 0 deletions ui/components/Chatbar/components/ClearUploadedDocuments.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [isClearing, setIsClearing] = useState<boolean>(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 ? (
<div className="flex w-full cursor-pointer items-center rounded-lg py-3 px-3 hover:bg-gray-500/10">
<IconFileX size={18} />

<div className="ml-3 flex-1 text-left text-[12.5px] leading-3 text-white">
{isClearing ? t('Clearing...') : t('Clear uploaded files?')}
</div>

<div className="flex w-[40px]">
<IconCheck
className="ml-auto mr-1 min-w-[20px] text-neutral-400 hover:text-neutral-100"
size={18}
onClick={(e) => {
e.stopPropagation();
if (!isClearing) {
void handleClearUploadedDocuments();
}
}}
/>

<IconX
className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100"
size={18}
onClick={(e) => {
e.stopPropagation();
if (!isClearing) {
setIsConfirming(false);
}
}}
/>
</div>
</div>
) : (
<SidebarButton
text={t('Clear uploaded files')}
icon={<IconFileX size={18} />}
onClick={() => setIsConfirming(true)}
/>
);
};
31 changes: 22 additions & 9 deletions ui/pages/api/delete-collection.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -21,4 +34,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
res.status(500).json({ error: 'An unexpected error occurred :(' });
}
}
}