'use client'; import * as React from 'react'; import { upload } from '@vercel/blob/client'; import AdminHeader from '@/components/AdminHeader'; import FileUploadPanel from '@/components/FileUploadPanel'; import QABuilderPanel from '@/components/QABuilderPanel'; import { KBFile, KBPair } from '@/lib/kb-data'; const DIRECT_UPLOAD_LIMIT_BYTES = 3.5 * 1024 * 1024; function makeUploadPath(file: File): string { const safeName = file.name .replace(/[/\\?%*:|"<>]/g, '-') .replace(/\s+/g, ' ') .trim(); return `uploads/${Date.now()}-${safeName}`; } async function readJsonResponse(res: Response) { const text = await res.text(); try { return text ? JSON.parse(text) : {}; } catch { throw new Error(text || `Request failed (${res.status})`); } } export default function AdminPage() { const [files, setFiles] = React.useState([]); const [qaList, setQaList] = React.useState([]); const [lastUpdated, setLastUpdated] = React.useState('Just now'); // Ids currently being deleted, so the cards can show a "Removing…" state. const [deletingFileIds, setDeletingFileIds] = React.useState>(new Set()); const [deletingQaIds, setDeletingQaIds] = React.useState>(new Set()); // Load the knowledge base from the server on mount. React.useEffect(() => { (async () => { try { const [filesRes, qaRes] = await Promise.all([ fetch('/api/documents'), fetch('/api/qa'), ]); const [filesData, qaData] = await Promise.all( [filesRes, qaRes].map(async (res) => { const data = await readJsonResponse(res); if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`); return data; }) ); setFiles(filesData.files ?? []); setQaList(qaData.qa ?? []); } catch (e) { console.error('Failed to load knowledge base', e); } const now = new Date(); setLastUpdated(now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); })(); }, []); // Update Last Updated Timestamp helper const triggerUpdateTimestamp = () => { const now = new Date(); setLastUpdated(now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); }; // 1. Files Upload and Management — parse + embed happens server-side. const handleUploadFile = async (file: File) => { // Let failures reject so the upload panel can surface an error state. let res: Response; if (file.size > DIRECT_UPLOAD_LIMIT_BYTES) { const blob = await upload(makeUploadPath(file), file, { access: 'private', handleUploadUrl: '/api/documents/upload', multipart: true, contentType: file.type || 'application/octet-stream', }); res = await fetch('/api/documents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ blobPathname: blob.pathname, name: file.name, size: file.size, }), }); } else { const form = new FormData(); form.append('file', file); res = await fetch('/api/documents', { method: 'POST', body: form }); } const data = await readJsonResponse(res); if (!res.ok) throw new Error(data.error || `Upload failed (${res.status})`); if (!data.file) throw new Error('Upload returned no file'); setFiles((prev) => { const others = prev.filter((f) => f.id !== data.file.id); return [...others, data.file]; }); triggerUpdateTimestamp(); }; const handleDeleteFile = async (id: string) => { setDeletingFileIds((prev) => new Set(prev).add(id)); try { const res = await fetch(`/api/documents/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`Delete failed (${res.status})`); setFiles((prev) => prev.filter((f) => f.id !== id)); triggerUpdateTimestamp(); } catch (e) { console.error('Delete failed', e); } finally { setDeletingFileIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); } }; // 2. Custom Q&A Management const handleAddQA = async (question: string, answer: string, category: string, prioritize: boolean) => { try { const res = await fetch('/api/qa', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, answer, category, prioritize }), }); const data = await res.json(); if (data.qa) { setQaList((prev) => [...prev, data.qa]); triggerUpdateTimestamp(); } } catch (e) { console.error('Add Q&A failed', e); } }; const handleUpdateQA = async (id: string, question: string, answer: string, category: string, prioritize: boolean) => { try { const res = await fetch(`/api/qa/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, answer, category, prioritize }), }); const data = await res.json(); if (data.qa) { setQaList((prev) => prev.map((qa) => (qa.id === id ? data.qa : qa))); triggerUpdateTimestamp(); } } catch (e) { console.error('Update Q&A failed', e); } }; const handleDeleteQA = async (id: string) => { setDeletingQaIds((prev) => new Set(prev).add(id)); try { const res = await fetch(`/api/qa/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`Delete failed (${res.status})`); setQaList((prev) => prev.filter((qa) => qa.id !== id)); triggerUpdateTimestamp(); } catch (e) { console.error('Delete Q&A failed', e); } finally { setDeletingQaIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); } }; // Calculating counters const totalFiles = files.length; const readyFilesCount = files.filter((f) => f.status === 'Ready').length; const totalQAs = qaList.length; return (
{/* Decorative dark aurora hints behind the panels */}
{/* Main Container */}
{/* Admin header */} {/* Core Workspace Panels split side-by-side or stacked on mobile */}
{/* Left column: File loader + its index metrics */}
{/* Index metrics — sits directly below Upload documents */}

Total Documents

{totalFiles}

Indexed (Ready)

{readyFilesCount}

Custom Q&As

{totalQAs}

Last Sync

Updated {lastUpdated}

{/* Right panel: Custom Q&A compiler */}
); }