diff --git a/app/api/collection-job/[job_id]/route.ts b/app/api/collection-job/[job_id]/route.ts new file mode 100644 index 0000000..3f72a59 --- /dev/null +++ b/app/api/collection-job/[job_id]/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + +// GET /api/collection/collection_job/[collection_job_id] - Get collection job status +export async function GET( + request: Request, + { params }: { params: Promise<{ collection_job_id: string }> } +) { + const { collection_job_id } = await params; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const response = await fetch( + `${backendUrl}/api/v1/collection_jobs/${collection_job_id}`, + { + headers: { + 'X-API-KEY': apiKey, + }, + } + ); + + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: error.message, data: null }, + { status: 500 } + ); + } +} diff --git a/app/api/collection/[collection_id]/route.ts b/app/api/collection/[collection_id]/route.ts new file mode 100644 index 0000000..3023541 --- /dev/null +++ b/app/api/collection/[collection_id]/route.ts @@ -0,0 +1,86 @@ +import { NextResponse } from 'next/server'; + +const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + +// GET /api/collection/[collection_id] - Get a specific collection +export async function GET( + request: Request, + { params }: { params: Promise<{ collection_id: string }> } +) { + const { collection_id } = await params; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const response = await fetch( + `${backendUrl}/api/v1/collections/${collection_id}?include_docs=true`, + { + headers: { + 'X-API-KEY': apiKey, + }, + } + ); + + const text = await response.text(); + const data = text ? JSON.parse(text) : {}; + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: error.message, data: null }, + { status: 500 } + ); + } +} + +// DELETE /api/collection/[collection_id] - Delete a collection +export async function DELETE( + request: Request, + { params }: { params: Promise<{ collection_id: string }> } +) { + const { collection_id } = await params; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const response = await fetch( + `${backendUrl}/api/v1/collections/${collection_id}`, + { + method: 'DELETE', + headers: { + 'X-API-KEY': apiKey, + }, + } + ); + + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true }; + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: error.message, data: null }, + { status: 500 } + ); + } +} diff --git a/app/api/collection/route.ts b/app/api/collection/route.ts new file mode 100644 index 0000000..edade40 --- /dev/null +++ b/app/api/collection/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; + + +export async function GET(request: Request) { + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const response = await fetch(`${backendUrl}/api/v1/collections/`, { + headers: { + 'X-API-KEY': apiKey, + }, + }); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : []; + + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + return NextResponse.json( + { success: false, error: error.message, data: null }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + // Get the API key from request headers + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + // Get the JSON body from the request + const body = await request.json(); + + // Get backend URL from environment variable + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + + // Forward the request to the actual backend + const response = await fetch(`${backendUrl}/api/v1/collections/`, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + + // Handle empty responses (204 No Content, etc.) + const text = await response.text(); + const data = text ? JSON.parse(text) : { success: true }; + + // Return the response with the same status code + if (!response.ok) { + return NextResponse.json(data, { status: response.status }); + } + + return NextResponse.json(data, { status: response.status }); + } catch (error: any) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error.message }, + { status: 500 } + ); + } +} diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 22f1d5d..5f12b54 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -73,6 +73,7 @@ export default function Sidebar({ collapsed, activeRoute = '/evaluations' }: Sid ] }, { name: 'Documents', route: '/document' }, + { name: 'Knowledge base', route: '/knowledge-base' }, // { name: 'Model Testing', route: '/model-testing', comingSoon: true }, // { name: 'Guardrails', route: '/guardrails', comingSoon: true }, // { name: 'Redteaming', route: '/redteaming', comingSoon: true }, diff --git a/app/knowledge-base/page.tsx b/app/knowledge-base/page.tsx new file mode 100644 index 0000000..ec350c5 --- /dev/null +++ b/app/knowledge-base/page.tsx @@ -0,0 +1,652 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { colors } from '@/app/lib/colors'; +import { formatDate } from '@/app/components/utils'; +import Sidebar from '@/app/components/Sidebar'; +import { APIKey, STORAGE_KEY } from '../keystore/page'; + +export interface Document { + id: string; + fname: string; + object_store_url: string; + signed_url?: string; + file_size?: number; + inserted_at?: string; + updated_at?: string; +} + +export interface Collection { + id: string; + name: string; + description: string; + inserted_at: string; + updated_at: string; + documents?: Document[]; +} + +export default function KnowledgeBasePage() { + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [collections, setCollections] = useState([]); + const [availableDocuments, setAvailableDocuments] = useState([]); + const [selectedCollection, setSelectedCollection] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showDocumentPicker, setShowDocumentPicker] = useState(false); + const [apiKey, setApiKey] = useState(null); + + // Form state + const [collectionName, setCollectionName] = useState(''); + const [collectionDescription, setCollectionDescription] = useState(''); + const [selectedDocuments, setSelectedDocuments] = useState>(new Set()); + + // Fetch collections + const fetchCollections = async () => { + if (!apiKey) return; + + setIsLoading(true); + try { + const response = await fetch('/api/collection', { + headers: { 'X-API-KEY': apiKey.key }, + }); + + if (response.ok) { + const result = await response.json(); + setCollections(result.data || []); + } + } catch (error) { + console.error('Error fetching collections:', error); + } finally { + setIsLoading(false); + } + }; + + // Fetch available documents + const fetchDocuments = async () => { + if (!apiKey) { + console.log('No API key available'); + return; + } + + try { + console.log('Fetching documents with API key:', apiKey.key ? 'present' : 'missing'); + + const response = await fetch('/api/document', { + headers: { 'X-API-KEY': apiKey.key }, + }); + + console.log('Document fetch response status:', response.status); + + if (response.ok) { + const result = await response.json(); + console.log('Raw document response:', result); + + // Handle both direct array and wrapped response + const documentList = Array.isArray(result) ? result : (result.data || []); + console.log('Processed document list:', documentList); + setAvailableDocuments(documentList); + } else { + const error = await response.json().catch(() => ({})); + console.error('Failed to fetch documents:', response.status, error); + } + } catch (error) { + console.error('Error fetching documents:', error); + } + }; + + // Fetch collection details with documents + const fetchCollectionDetails = async (collectionId: string) => { + if (!apiKey) return; + + setIsLoading(true); + try { + const response = await fetch(`/api/collection/${collectionId}`, { + headers: { 'X-API-KEY': apiKey.key }, + }); + + if (response.ok) { + const result = await response.json(); + setSelectedCollection(result.data); + } + } catch (error) { + console.error('Error fetching collection details:', error); + } finally { + setIsLoading(false); + } + }; + + // Create collection + const handleCreateCollection = async () => { + if (!apiKey) { + alert('No API key found'); + return; + } + + if (!collectionName.trim() || selectedDocuments.size === 0) { + alert('Please provide a name and select at least one document'); + return; + } + + setIsCreating(true); + try { + const response = await fetch('/api/collection', { + method: 'POST', + headers: { + 'X-API-KEY': apiKey.key, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: collectionName, + description: collectionDescription, + documents: Array.from(selectedDocuments), + batch_size: 1, + provider: 'openai', + }), + }); + + if (response.ok) { + const result = await response.json(); + alert(`Collection created! Job ID: ${result.data?.id || 'Unknown'}`); + setShowCreateModal(false); + setCollectionName(''); + setCollectionDescription(''); + setSelectedDocuments(new Set()); + fetchCollections(); + } else { + const error = await response.json(); + alert(`Failed to create collection: ${error.error || 'Unknown error'}`); + } + } catch (error) { + console.error('Error creating collection:', error); + alert('Failed to create collection'); + } finally { + setIsCreating(false); + } + }; + + // Delete collection + const handleDeleteCollection = async (collectionId: string) => { + if (!apiKey) return; + if (!confirm('Are you sure you want to delete this collection?')) return; + + try { + const response = await fetch(`/api/collection/${collectionId}`, { + method: 'DELETE', + headers: { 'X-API-KEY': apiKey.key }, + }); + + if (response.ok) { + alert('Collection deleted successfully'); + setSelectedCollection(null); + fetchCollections(); + } else { + alert('Failed to delete collection'); + } + } catch (error) { + console.error('Error deleting collection:', error); + alert('Failed to delete collection'); + } + }; + + // Toggle document selection + const toggleDocumentSelection = (documentId: string) => { + const newSelection = new Set(selectedDocuments); + if (newSelection.has(documentId)) { + newSelection.delete(documentId); + } else { + newSelection.add(documentId); + } + setSelectedDocuments(newSelection); + }; + + // Load API key from localStorage + useEffect(() => { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + const keys = JSON.parse(stored); + if (keys.length > 0) { + setApiKey(keys[0]); + } + } catch (e) { + console.error('Failed to load API key:', e); + } + } + }, []); + + useEffect(() => { + if (apiKey) { + fetchCollections(); + fetchDocuments(); + } + }, [apiKey]); + + return ( +
+ + + {/* Main Content */} +
+ {/* Header with Collapse Button */} +
+
+ +
+

+ Knowledge Base +

+

+ Manage your knowledge bases for RAG +

+
+
+
+ + {/* Content Area - Split View */} +
+ {/* Left Panel - Collections List */} +
+ {/* Create Button */} +
+ +
+ + {/* Collections List */} +
+ {isLoading && collections.length === 0 ? ( +
+ Loading collections... +
+ ) : collections.length === 0 ? ( +
+ No collections yet. Create your first one! +
+ ) : ( +
+ {collections.map((collection) => ( +
fetchCollectionDetails(collection.id)} + className="p-4 rounded-lg border cursor-pointer transition-all" + style={{ + backgroundColor: selectedCollection?.id === collection.id ? colors.bg.secondary : 'transparent', + borderColor: selectedCollection?.id === collection.id ? colors.border : 'transparent', + }} + > +
+
+

+ {collection.name} +

+ {collection.description && ( +

+ {collection.description} +

+ )} +

+ Created: {formatDate(collection.inserted_at)} +

+
+
+
+ ))} +
+ )} +
+
+ + {/* Right Panel - Preview */} +
+ {selectedCollection ? ( + <> + {/* Preview Header */} +
+
+
+

+ {selectedCollection.name} +

+ {selectedCollection.description && ( +

+ {selectedCollection.description} +

+ )} +
+ +
+ + {/* Metadata */} +
+
+
+ Created +
+
+ {formatDate(selectedCollection.inserted_at)} +
+
+
+
+ Last Updated +
+
+ {formatDate(selectedCollection.updated_at)} +
+
+
+
+ + {/* Documents in Collection */} +
+

+ Documents ({selectedCollection.documents?.length || 0}) +

+ {selectedCollection.documents && selectedCollection.documents.length > 0 ? ( +
+ {selectedCollection.documents.map((doc) => ( +
+
+
+

+ {doc.fname} +

+

+ ID: {doc.id.substring(0, 8)}... +

+
+
+
+ ))} +
+ ) : ( +
+ No documents in this collection +
+ )} +
+ + ) : ( +
+ Select a collection to view details +
+ )} +
+
+
+ + {/* Create Collection Modal */} + {showCreateModal && ( +
+
+

+ Create Knowledge Base +

+ + {/* Name Input */} +
+ + setCollectionName(e.target.value)} + placeholder="Enter collection name" + className="w-full px-4 py-2 rounded-md border text-sm" + style={{ + borderColor: colors.border, + backgroundColor: colors.bg.secondary, + color: colors.text.primary, + }} + /> +
+ + {/* Description Input */} +
+ +