diff --git a/.env.example b/.env.example index ae81a04..3e6995f 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ -BACKEND_URL=http://localhost:8000 \ No newline at end of file +BACKEND_URL=http://localhost:8000 +NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com \ No newline at end of file diff --git a/app/(main)/configurations/page.tsx b/app/(main)/configurations/page.tsx index 1e35c43..d9243f0 100644 --- a/app/(main)/configurations/page.tsx +++ b/app/(main)/configurations/page.tsx @@ -45,8 +45,8 @@ export default function ConfigLibraryPage() { Record >({}); const { sidebarCollapsed } = useApp(); - const { activeKey } = useAuth(); - const apiKey = activeKey?.key; + const { activeKey, isAuthenticated } = useAuth(); + const apiKey = activeKey?.key ?? ""; const [searchInput, setSearchInput] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); const [columnCount, setColumnCount] = useState(3); @@ -100,11 +100,11 @@ export default function ConfigLibraryPage() { useEffect(() => { const fetchEvaluationCounts = async () => { - if (!activeKey) return; + if (!isAuthenticated) return; try { const data = await apiFetch( "/api/evaluations", - activeKey.key, + apiKey, ); const jobs: EvalJob[] = Array.isArray(data) ? data : data.data || []; const counts: Record = {}; @@ -129,7 +129,7 @@ export default function ConfigLibraryPage() { await existing; return; } - if (!apiKey) return; + if (!isAuthenticated) return; const loadPromise = (async () => { const res = await apiFetch<{ @@ -144,7 +144,7 @@ export default function ConfigLibraryPage() { pendingVersionLoads.set(configId, loadPromise); await loadPromise; }, - [apiKey], + [apiKey, isAuthenticated], ); const loadSingleVersion = useCallback( @@ -152,7 +152,7 @@ export default function ConfigLibraryPage() { const key = `${configId}:${version}`; const existing = pendingSingleVersionLoads.get(key); if (existing) return existing; - if (!apiKey) return null; + if (!isAuthenticated) return null; const configPublic = configs.find((c) => c.id === configId) ?? @@ -179,7 +179,7 @@ export default function ConfigLibraryPage() { pendingSingleVersionLoads.set(key, loadPromise); return loadPromise; }, - [apiKey, configs], + [apiKey, configs, isAuthenticated], ); const handleCreateNew = () => { @@ -277,17 +277,7 @@ export default function ConfigLibraryPage() { ) : error ? (
-

{error}

- +

{error}

) : configs.length === 0 ? (
(null); - const { activeKey: apiKey } = useAuth(); - - // Pagination state + const { activeKey: apiKey, isAuthenticated } = useAuth(); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; - // Fetch datasets from backend when API key is available useEffect(() => { - if (apiKey) { + if (isAuthenticated) { fetchDatasets(); } - }, [apiKey]); + }, [apiKey, isAuthenticated]); const fetchDatasets = async () => { - if (!apiKey) { - setError("No API key found. Please add an API key in the Keystore."); + if (!isAuthenticated) { + setError("Please log in to continue."); return; } @@ -63,23 +49,10 @@ export default function Datasets() { setError(null); try { - const response = await fetch("/api/evaluations/datasets", { - method: "GET", - headers: { - "X-API-KEY": apiKey.key, - }, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || - errorData.message || - `Failed to fetch datasets: ${response.status}`, - ); - } - - const data = await response.json(); + const data = await apiFetch( + "/api/evaluations/datasets", + apiKey?.key ?? "", + ); const datasetList = Array.isArray(data) ? data : data.data || []; setDatasets(datasetList); } catch (err: unknown) { @@ -117,49 +90,29 @@ export default function Datasets() { return; } - if (!apiKey) { - toast.error("No API key found. Please add an API key in the Keystore."); + if (!isAuthenticated) { + toast.error("Please log in to continue."); return; } setIsUploading(true); try { - // Prepare FormData for upload const formData = new FormData(); formData.append("file", selectedFile); formData.append("dataset_name", datasetName.trim()); formData.append("duplication_factor", duplicationFactor || "1"); - // Upload to backend - const response = await fetch("/api/evaluations/datasets", { + await apiFetch("/api/evaluations/datasets", apiKey?.key ?? "", { method: "POST", body: formData, - headers: { - "X-API-KEY": apiKey.key, - }, }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || - errorData.message || - `Upload failed with status ${response.status}`, - ); - } - - await response.json(); - // Refresh datasets list await fetchDatasets(); - // Reset form setSelectedFile(null); setDatasetName(""); setDuplicationFactor("1"); - - // Close modal setIsModalOpen(false); toast.success("Dataset uploaded successfully!"); @@ -174,8 +127,8 @@ export default function Datasets() { }; const handleDeleteDataset = async (datasetId: number) => { - if (!apiKey) { - toast.error("No API key found"); + if (!isAuthenticated) { + toast.error("Please log in to continue"); return; } @@ -185,23 +138,14 @@ export default function Datasets() { } try { - const response = await fetch(`/api/evaluations/datasets/${datasetId}`, { - method: "DELETE", - headers: { - "X-API-KEY": apiKey.key, + await apiFetch( + `/api/evaluations/datasets/${datasetId}`, + apiKey?.key ?? "", + { + method: "DELETE", }, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || - errorData.message || - `Delete failed with status ${response.status}`, - ); - } + ); - // Refresh datasets list await fetchDatasets(); toast.success("Dataset deleted successfully"); } catch (error) { @@ -212,7 +156,6 @@ export default function Datasets() { } }; - // Pagination calculations const indexOfLastItem = currentPage * itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage; const currentDatasets = datasets.slice(indexOfFirstItem, indexOfLastItem); @@ -221,26 +164,17 @@ export default function Datasets() { const paginate = (pageNumber: number) => setCurrentPage(pageNumber); return ( -
+
- {/* Sidebar */} - {/* Main Content */}
- {/* Content Area */} -
+
setIsModalOpen(true)} isLoading={isLoading} error={error} - apiKey={apiKey} + isAuthenticated={isAuthenticated} totalPages={totalPages} currentPage={currentPage} onPageChange={paginate} @@ -258,7 +192,6 @@ export default function Datasets() {
- {/* Upload Dataset Modal */} {isModalOpen && ( void; isLoading: boolean; error: string | null; - apiKey: APIKey | null; + isAuthenticated: boolean; totalPages: number; currentPage: number; onPageChange: (page: number) => void; @@ -300,7 +233,7 @@ function DatasetListing({ onUploadNew, isLoading, error, - apiKey, + isAuthenticated, totalPages, currentPage, onPageChange, @@ -362,43 +295,10 @@ function DatasetListing({

Loading datasets...

- ) : !apiKey ? ( -
- - - -

- No API key found -

-

- Please add an API key in the Keystore to manage datasets -

- - Go to Keystore - + ) : !isAuthenticated ? ( +
+

Login required

+

Please log in to manage datasets

) : error ? (
0 && totalPages > 1 && (
("uploading"); const abortUploadRef = useRef<(() => void) | null>(null); - const { activeKey: apiKey } = useAuth(); + const { activeKey: apiKey, isAuthenticated } = useAuth(); const { items: documents, @@ -72,7 +72,7 @@ export default function DocumentPage() { }; const handleUpload = async () => { - if (!apiKey || !selectedFile) return; + if (!isAuthenticated || !selectedFile) return; setIsUploading(true); setUploadProgress(0); @@ -84,7 +84,7 @@ export default function DocumentPage() { const { promise, abort } = uploadWithProgress<{ data?: { id: string } }>( "/api/document", - apiKey.key, + apiKey?.key ?? "", formData, (percent, phase) => { setUploadProgress(percent); @@ -121,8 +121,8 @@ export default function DocumentPage() { }; const handleDeleteDocument = async (documentId: string) => { - if (!apiKey) { - toast.error("No API key found"); + if (!isAuthenticated) { + toast.error("Please log in to continue"); return; } @@ -131,7 +131,7 @@ export default function DocumentPage() { } try { - await apiFetch(`/api/document/${documentId}`, apiKey.key, { + await apiFetch(`/api/document/${documentId}`, apiKey?.key ?? "", { method: "DELETE", }); @@ -150,13 +150,13 @@ export default function DocumentPage() { }; const handleSelectDocument = async (doc: Document) => { - if (!apiKey) return; + if (!isAuthenticated) return; setIsLoadingDocument(true); try { const data = await apiFetch<{ data?: Document }>( `/api/document/${doc.id}`, - apiKey.key, + apiKey?.key ?? "", ); const documentDetails: Document = data.data ?? (data as unknown as Document); @@ -201,7 +201,6 @@ export default function DocumentPage() { isLoading={isLoading} isLoadingMore={isLoadingMore} error={error} - apiKey={apiKey} scrollRef={scrollRef} />
diff --git a/app/(main)/evaluations/[id]/page.tsx b/app/(main)/evaluations/[id]/page.tsx index 300e61c..d2e583f 100644 --- a/app/(main)/evaluations/[id]/page.tsx +++ b/app/(main)/evaluations/[id]/page.tsx @@ -48,26 +48,16 @@ export default function EvaluationReport() { >(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const { apiKeys } = useAuth(); + const { apiKeys, isAuthenticated } = useAuth(); + const apiKey = apiKeys[0]?.key ?? ""; const { sidebarCollapsed, setSidebarCollapsed } = useApp(); - const [selectedKeyId, setSelectedKeyId] = useState(""); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [exportFormat, setExportFormat] = useState<"row" | "grouped">("row"); const [isResyncing, setIsResyncing] = useState(false); const [showNoTracesModal, setShowNoTracesModal] = useState(false); - useEffect(() => { - if (apiKeys.length > 0 && !selectedKeyId) { - setSelectedKeyId(apiKeys[0].id); - } - }, [apiKeys, selectedKeyId]); - - // Fetch job details const fetchJobDetails = useCallback(async () => { - if (!selectedKeyId || !jobId) return; - - const selectedKey = apiKeys.find((k) => k.id === selectedKeyId); - if (!selectedKey) return; + if (!isAuthenticated || !jobId) return; setIsLoading(true); setError(null); @@ -76,7 +66,7 @@ export default function EvaluationReport() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = await apiFetch( `/api/evaluations/${jobId}?export_format=${exportFormat}`, - selectedKey.key, + apiKey, ); if (data.success === false && data.error) { @@ -91,14 +81,10 @@ export default function EvaluationReport() { setJob(foundJob); if (foundJob.assistant_id) { - fetchAssistantConfig(foundJob.assistant_id, selectedKey.key); + fetchAssistantConfig(foundJob.assistant_id); } if (foundJob.config_id && foundJob.config_version) { - fetchConfigInfo( - foundJob.config_id, - foundJob.config_version, - selectedKey.key, - ); + fetchConfigInfo(foundJob.config_id, foundJob.config_version); } } catch (err: unknown) { setError( @@ -107,9 +93,9 @@ export default function EvaluationReport() { } finally { setIsLoading(false); } - }, [apiKeys, selectedKeyId, jobId, exportFormat]); + }, [apiKey, isAuthenticated, jobId, exportFormat]); - const fetchAssistantConfig = async (assistantId: string, apiKey: string) => { + const fetchAssistantConfig = async (assistantId: string) => { try { const result = await apiFetch<{ success: boolean; @@ -124,11 +110,7 @@ export default function EvaluationReport() { } }; - const fetchConfigInfo = async ( - configId: string, - configVersion: number, - apiKey: string, - ) => { + const fetchConfigInfo = async (configId: string, configVersion: number) => { try { await apiFetch(`/api/configs/${configId}`, apiKey); await apiFetch( @@ -141,8 +123,8 @@ export default function EvaluationReport() { }; useEffect(() => { - if (selectedKeyId && jobId) fetchJobDetails(); - }, [selectedKeyId, jobId, fetchJobDetails]); + if (isAuthenticated && jobId) fetchJobDetails(); + }, [isAuthenticated, jobId, fetchJobDetails]); // Export grouped format CSV const exportGroupedCSV = (traces: GroupedTraceItem[]) => { @@ -285,16 +267,14 @@ export default function EvaluationReport() { }; const handleResync = async () => { - if (!selectedKeyId || !jobId) return; - const selectedKey = apiKeys.find((k) => k.id === selectedKeyId); - if (!selectedKey) return; + if (!isAuthenticated || !jobId) return; setIsResyncing(true); try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = await apiFetch( `/api/evaluations/${jobId}?get_trace_info=true&resync_score=true&export_format=${exportFormat}`, - selectedKey.key, + apiKey, ); const foundJob = data.data || data; if (!foundJob) throw new Error("Evaluation job not found"); @@ -307,14 +287,9 @@ export default function EvaluationReport() { } setJob(foundJob); - if (foundJob.assistant_id) - fetchAssistantConfig(foundJob.assistant_id, selectedKey.key); + if (foundJob.assistant_id) fetchAssistantConfig(foundJob.assistant_id); if (foundJob.config_id && foundJob.config_version) - fetchConfigInfo( - foundJob.config_id, - foundJob.config_version, - selectedKey.key, - ); + fetchConfigInfo(foundJob.config_id, foundJob.config_version); toast.success("Metrics resynced successfully"); } catch (error: unknown) { toast.error( diff --git a/app/(main)/evaluations/page.tsx b/app/(main)/evaluations/page.tsx index df6c5a5..d7900f3 100644 --- a/app/(main)/evaluations/page.tsx +++ b/app/(main)/evaluations/page.tsx @@ -8,16 +8,17 @@ "use client"; import { useState, useEffect, useCallback, Suspense } from "react"; +import { apiFetch } from "@/app/lib/apiClient"; import { colors } from "@/app/lib/colors"; import { useSearchParams } from "next/navigation"; -import { Dataset } from "@/app/(main)/datasets/page"; +import { Dataset } from "@/app/lib/types/dataset"; import Sidebar from "@/app/components/Sidebar"; import PageHeader from "@/app/components/PageHeader"; import TabNavigation from "@/app/components/TabNavigation"; import { useToast } from "@/app/components/Toast"; import { useAuth } from "@/app/lib/context/AuthContext"; import { useApp } from "@/app/lib/context/AppContext"; -import { apiFetch } from "@/app/lib/apiClient"; +import { FeatureGateModal, LoginModal } from "@/app/components/auth"; import Loader from "@/app/components/Loader"; import DatasetsTab from "@/app/components/evaluations/DatasetsTab"; import EvaluationsTab from "@/app/components/evaluations/EvaluationsTab"; @@ -38,7 +39,9 @@ function SimplifiedEvalContent() { }); const { sidebarCollapsed } = useApp(); - const { activeKey } = useAuth(); + const { activeKey, isAuthenticated } = useAuth(); + const apiKey = activeKey?.key ?? ""; + const [showLoginModal, setShowLoginModal] = useState(false); const [mounted, setMounted] = useState(false); // Dataset creation state const [datasetName, setDatasetName] = useState(""); @@ -74,15 +77,12 @@ function SimplifiedEvalContent() { }, []); const loadStoredDatasets = useCallback(async () => { - if (!activeKey?.key) { - console.error("No selected API key found for loading datasets"); - return; - } + if (!isAuthenticated) return; setIsDatasetsLoading(true); try { const data = await apiFetch( "/api/evaluations/datasets", - activeKey.key, + apiKey, ); setStoredDatasets(Array.isArray(data) ? data : data.data || []); } catch (e) { @@ -90,11 +90,11 @@ function SimplifiedEvalContent() { } finally { setIsDatasetsLoading(false); } - }, [activeKey]); + }, [apiKey, isAuthenticated]); useEffect(() => { - if (activeKey?.key) loadStoredDatasets(); - }, [activeKey, loadStoredDatasets]); + if (isAuthenticated) loadStoredDatasets(); + }, [isAuthenticated, loadStoredDatasets]); const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -159,8 +159,8 @@ function SimplifiedEvalContent() { toast.error("Please enter a dataset name"); return; } - if (!activeKey?.key) { - toast.error("No API key selected. Please select one in the Keystore."); + if (!isAuthenticated) { + toast.error("Please log in to create datasets."); return; } @@ -178,7 +178,7 @@ function SimplifiedEvalContent() { const data = await apiFetch<{ dataset_id?: number }>( "/api/evaluations/datasets", - activeKey.key, + apiKey, { method: "POST", body: formData }, ); await loadStoredDatasets(); @@ -193,7 +193,7 @@ function SimplifiedEvalContent() { setDuplicationFactor("1"); toast.success("Dataset created successfully!"); - } catch (error) { + } catch (error: unknown) { toast.error( `Failed to create dataset: ${error instanceof Error ? error.message : "Unknown error"}`, ); @@ -203,8 +203,8 @@ function SimplifiedEvalContent() { }; const handleRunEvaluation = async () => { - if (!activeKey?.key) { - toast.error("Please select an API key first"); + if (!isAuthenticated) { + toast.error("Please log in to run evaluations."); return; } if (!selectedDatasetId) { @@ -229,7 +229,7 @@ function SimplifiedEvalContent() { config_version: selectedConfigVersion, }; - await apiFetch("/api/evaluations", activeKey.key, { + await apiFetch("/api/evaluations", apiKey, { method: "POST", body: JSON.stringify(payload), }); @@ -271,51 +271,18 @@ function SimplifiedEvalContent() { /> {/* Tab Content */} - {!mounted || !activeKey ? ( -
-
- - - -

- API key required -

-

- Add an API key in the Keystore to start creating datasets and - running evaluations -

- - Go to Keystore - -
-
+ {!mounted || !isAuthenticated ? ( + <> + setShowLoginModal(true)} + /> + setShowLoginModal(false)} + /> + ) : activeTab === "datasets" ? ( ) : ( (null); - const { activeKey: apiKey } = useAuth(); + const { activeKey: apiKey, isAuthenticated } = useAuth(); const [showAllDocs, setShowAllDocs] = useState(false); // Polling refs — persist across renders, no stale closures @@ -206,7 +221,7 @@ export default function KnowledgeBasePage() { ): Promise< Map > => { - if (!apiKey) return new Map(); + if (!isAuthenticated) return new Map(); const cache = JSON.parse(localStorage.getItem(CACHE_KEY) || "{}"); @@ -240,22 +255,18 @@ export default function KnowledgeBasePage() { const results = await Promise.all( Array.from(jobIdsToFetch).map(async (jobId) => { try { - const response = await fetch(`/api/collections/jobs/${jobId}`, { - headers: { "X-API-KEY": apiKey.key }, - }); - - if (response.ok) { - const result = await response.json(); - const jobData = result.data || result; - const collectionId = - jobData.collection?.id || jobData.collection_id || null; - - return { - jobId, - status: jobData.status || null, - collectionId: collectionId, - }; - } + const result = await apiFetch< + { data?: JobStatusData } & JobStatusData + >(`/api/collections/jobs/${jobId}`, apiKey?.key ?? ""); + const jobData = result.data || result; + const collectionId = + jobData.collection?.id || jobData.collection_id || null; + + return { + jobId, + status: jobData.status || null, + collectionId: collectionId, + }; } catch (error) { console.error("Error fetching job status:", error); } @@ -272,79 +283,75 @@ export default function KnowledgeBasePage() { return jobStatusMap; }; + // Fetch collections + const fetchCollections = async () => { - if (!apiKey) return; + if (!isAuthenticated) return; setIsLoading(true); try { - const response = await fetch("/api/collections", { - headers: { "X-API-KEY": apiKey.key }, - }); - - if (response.ok) { - const result = await response.json(); - const collections = result.data || []; + const result = await apiFetch( + "/api/collections", + apiKey?.key ?? "", + ); + const collections = ( + Array.isArray(result.data) ? result.data : [] + ) as Collection[]; + + // Pre-fetch job statuses only for collections that need it + const jobStatusMap = await preFetchJobStatuses(collections); + + // Enrich collections with cached names and live status + const enrichedCollections = await Promise.all( + collections.map((collection: Collection) => + enrichCollectionWithCache(collection, jobStatusMap), + ), + ); - // Pre-fetch job statuses only for collections that need it - const jobStatusMap = await preFetchJobStatuses(collections); + // Remove cache entries whose collection no longer exists on the backend + const liveIds = new Set( + enrichedCollections.map((c: Collection) => c.id), + ); + pruneStaleCache(liveIds); - // Enrich collections with cached names and live status - const enrichedCollections = await Promise.all( - collections.map((collection: Collection) => - enrichCollectionWithCache(collection, jobStatusMap), - ), + // Preserve optimistic entries not yet replaced by a real collection + setCollections((prev) => { + const fetchedJobIds = new Set( + enrichedCollections.map((c: Collection) => c.job_id).filter(Boolean), ); - - // Remove cache entries whose collection no longer exists on the backend - const liveIds = new Set( - enrichedCollections.map((c: Collection) => c.id), + const activeOptimistic = prev.filter( + (c) => + c.id.startsWith("optimistic-") && + (!c.job_id || !fetchedJobIds.has(c.job_id)), ); - pruneStaleCache(liveIds); - - // Preserve optimistic entries not yet replaced by a real collection - setCollections((prev) => { - const fetchedJobIds = new Set( - enrichedCollections - .map((c: Collection) => c.job_id) - .filter(Boolean), - ); - const activeOptimistic = prev.filter( - (c) => - c.id.startsWith("optimistic-") && - (!c.job_id || !fetchedJobIds.has(c.job_id)), - ); - // Sort by inserted_at in descending order (latest first) - const combined = [...activeOptimistic, ...enrichedCollections]; - return combined.sort( - (a, b) => - new Date(b.inserted_at).getTime() - - new Date(a.inserted_at).getTime(), + // Sort by inserted_at in descending order (latest first) + const combined = [...activeOptimistic, ...enrichedCollections]; + return combined.sort( + (a, b) => + new Date(b.inserted_at).getTime() - + new Date(a.inserted_at).getTime(), + ); + }); + + // If selectedCollection is optimistic and the real one just arrived, fetch full details + // Extract the logic outside the updater to avoid side effects + let replacementId: string | null = null; + setSelectedCollection((prev) => { + if (prev?.id.startsWith("optimistic-") && prev.job_id) { + const replacement = enrichedCollections.find( + (c: Collection) => c.job_id === prev.job_id, ); - }); - - // If selectedCollection is optimistic and the real one just arrived, fetch full details - // Extract the logic outside the updater to avoid side effects - let replacementId: string | null = null; - setSelectedCollection((prev) => { - if (prev?.id.startsWith("optimistic-") && prev.job_id) { - const replacement = enrichedCollections.find( - (c: Collection) => c.job_id === prev.job_id, - ); - if (replacement) { - replacementId = replacement.id; - // Don't set the replacement yet - let fetchCollectionDetails do it with full data - } + if (replacement) { + replacementId = replacement.id; + // Don't set the replacement yet - let fetchCollectionDetails do it with full data } - return prev; - }); - - // Fetch full details (including documents) for the replacement - if (replacementId) { - fetchCollectionDetails(replacementId); } - } else { - const error = await response.json().catch(() => ({})); - console.error("Failed to fetch collections:", response.status, error); + return prev; + }); + + // Fetch full details (including documents) for the replacement + if (replacementId) { + fetchCollectionDetails(replacementId); } } catch (error) { console.error("Error fetching collections:", error); @@ -355,31 +362,27 @@ export default function KnowledgeBasePage() { // Fetch available documents const fetchDocuments = async () => { - if (!apiKey) return; + if (!isAuthenticated) return; try { - const response = await fetch("/api/document", { - headers: { "X-API-KEY": apiKey.key }, - }); - - if (response.ok) { - const result = await response.json(); + const result = await apiFetch( + "/api/document", + apiKey?.key ?? "", + ); - // Handle both direct array and wrapped response - const documentList = Array.isArray(result) ? result : result.data || []; + // Handle both direct array and wrapped response + const documentList = Array.isArray(result) + ? result + : (result as DocumentResponse).data || []; - // Sort by inserted_at in descending order (latest first) - const sortedDocuments = documentList.sort( - (a: Document, b: Document) => - new Date(b.inserted_at || 0).getTime() - - new Date(a.inserted_at || 0).getTime(), - ); + // Sort by inserted_at in descending order (latest first) + const sortedDocuments = documentList.sort( + (a: Document, b: Document) => + new Date(b.inserted_at || 0).getTime() - + new Date(a.inserted_at || 0).getTime(), + ); - setAvailableDocuments(sortedDocuments); - } else { - const error = await response.json().catch(() => ({})); - console.error("Failed to fetch documents:", response.status, error); - } + setAvailableDocuments(sortedDocuments); } catch (error) { console.error("Error fetching documents:", error); } @@ -387,7 +390,7 @@ export default function KnowledgeBasePage() { // Fetch collection details with documents const fetchCollectionDetails = async (collectionId: string) => { - if (!apiKey) return; + if (!isAuthenticated) return; // Don't fetch optimistic collections from the server if (collectionId.startsWith("optimistic-")) { @@ -402,53 +405,44 @@ export default function KnowledgeBasePage() { setIsLoading(true); try { - const response = await fetch(`/api/collections/${collectionId}`, { - headers: { "X-API-KEY": apiKey.key }, - }); - - if (response.ok) { - const result = await response.json(); + const result = await apiFetch( + `/api/collections/${collectionId}`, + apiKey?.key ?? "", + ); - // Handle different response formats - const collectionData = result.data || result; + // Handle different response formats + const collectionData = (result.data as Collection) || result; - // Get cached data to find the job_id - const cached = getCollectionDataByCollectionId(collectionId); + // Get cached data to find the job_id + const cached = getCollectionDataByCollectionId(collectionId); - // If we have a job_id, fetch its status - let status = undefined; - if (cached.job_id) { - try { - const jobResponse = await fetch( - `/api/collections/jobs/${cached.job_id}`, - { - headers: { "X-API-KEY": apiKey.key }, - }, - ); - if (jobResponse.ok) { - const jobResult = await jobResponse.json(); - const jobData = jobResult.data || jobResult; - status = jobData.status || undefined; - } - } catch (error) { - console.error( - "Error fetching job status for collection details:", - error, - ); - } + // If we have a job_id, fetch its status + let status = undefined; + if (cached.job_id) { + try { + const jobResult = await apiFetch< + { data?: JobStatusData } & JobStatusData + >(`/api/collections/jobs/${cached.job_id}`, apiKey?.key ?? ""); + const jobData = jobResult.data || jobResult; + status = jobData.status || undefined; + } catch (error) { + console.error( + "Error fetching job status for collection details:", + error, + ); } + } - // Enrich the collection with cached name/description and live status - const enrichedCollection = { - ...collectionData, - name: cached.name || collectionData.name || "Untitled Collection", - description: cached.description || collectionData.description || "", - status: status, - job_id: cached.job_id, - }; + // Enrich the collection with cached name/description and live status + const enrichedCollection = { + ...collectionData, + name: cached.name || collectionData.name || "Untitled Collection", + description: cached.description || collectionData.description || "", + status: status, + job_id: cached.job_id, + }; - setSelectedCollection(enrichedCollection); - } + setSelectedCollection(enrichedCollection); } catch (error) { console.error("Error fetching collection details:", error); } finally { @@ -463,7 +457,7 @@ export default function KnowledgeBasePage() { pollingRef.current = setInterval(async () => { const currentApiKey = apiKeyRef.current; - if (!currentApiKey) return; + if (!currentApiKey && !isAuthenticated) return; const jobs = activeJobsRef.current; if (jobs.size === 0) { @@ -476,12 +470,9 @@ export default function KnowledgeBasePage() { for (const [collectionId, jobId] of Array.from(jobs)) { try { - const response = await fetch(`/api/collections/jobs/${jobId}`, { - headers: { "X-API-KEY": currentApiKey.key }, - }); - if (!response.ok) continue; - - const result = await response.json(); + const result = await apiFetch< + { data?: JobStatusData } & JobStatusData + >(`/api/collections/jobs/${jobId}`, currentApiKey?.key ?? ""); const jobData = result.data || result; const status = jobData.status || null; const realCollectionId = @@ -554,8 +545,8 @@ export default function KnowledgeBasePage() { // Create knowledge base const handleCreateClick = async () => { - if (!apiKey) { - alert("No API key found"); + if (!isAuthenticated) { + alert("Please log in to continue"); return; } @@ -599,60 +590,51 @@ export default function KnowledgeBasePage() { setSelectedCollection(optimisticCollection); try { - const response = await fetch("/api/collections", { - method: "POST", - headers: { - "X-API-KEY": apiKey.key, - "Content-Type": "application/json", + const result = await apiFetch( + "/api/collections", + apiKey?.key ?? "", + { + method: "POST", + body: JSON.stringify({ + name: nameAtCreation, + description: descriptionAtCreation, + documents: docsAtCreation, + provider: "openai", + }), }, - body: JSON.stringify({ - name: nameAtCreation, - description: descriptionAtCreation, - documents: docsAtCreation, - provider: "openai", - }), - }); - - if (response.ok) { - const result = await response.json(); - const jobId = result.data?.job_id; + ); - if (jobId) { - saveCollectionData(jobId, nameAtCreation, descriptionAtCreation); + const jobId = result.data?.job_id; - // Attach job_id to the optimistic entry so polling picks it up - setCollections((prev) => - prev.map((c) => - c.id === optimisticId ? { ...c, job_id: jobId } : c, - ), - ); - setSelectedCollection((prev) => - prev?.id === optimisticId ? { ...prev, job_id: jobId } : prev, - ); + if (jobId) { + saveCollectionData(jobId, nameAtCreation, descriptionAtCreation); - // Register for polling immediately — don't wait for the next collections render - activeJobsRef.current.set(optimisticId, jobId); - startPolling(); - } else { - console.error( - "No job ID found in response - cannot save name to cache", - ); - } + // Attach job_id to the optimistic entry so polling picks it up + setCollections((prev) => + prev.map((c) => + c.id === optimisticId ? { ...c, job_id: jobId } : c, + ), + ); + setSelectedCollection((prev) => + prev?.id === optimisticId ? { ...prev, job_id: jobId } : prev, + ); - // Refresh the real list from the backend (replaces the optimistic entry once the backend knows about it) - await fetchCollections(); + // Register for polling immediately — don't wait for the next collections render + activeJobsRef.current.set(optimisticId, jobId); + startPolling(); } else { - const error = await response.json().catch(() => ({})); - alert( - `Failed to create knowledge base: ${error.error || "Unknown error"}`, + console.error( + "No job ID found in response - cannot save name to cache", ); - // Remove the optimistic entry on failure - setCollections((prev) => prev.filter((c) => c.id !== optimisticId)); - setSelectedCollection(null); } + + // Refresh the real list from the backend (replaces the optimistic entry once the backend knows about it) + await fetchCollections(); } catch (error) { console.error("Error creating knowledge base:", error); - alert("Failed to create knowledge base"); + alert( + `Failed to create knowledge base: ${error instanceof Error ? error.message : "Unknown error"}`, + ); setCollections((prev) => prev.filter((c) => c.id !== optimisticId)); setSelectedCollection(null); } finally { @@ -662,14 +644,14 @@ export default function KnowledgeBasePage() { // Delete collection - show confirmation modal const handleDeleteCollection = (collectionId: string) => { - if (!apiKey) return; + if (!isAuthenticated) return; setCollectionToDelete(collectionId); setShowConfirmDelete(true); }; // Confirm and execute delete const handleConfirmDelete = async () => { - if (!collectionToDelete || !apiKey) return; + if (!collectionToDelete || !isAuthenticated) return; setShowConfirmDelete(false); const collectionId = collectionToDelete; @@ -689,76 +671,38 @@ export default function KnowledgeBasePage() { ); try { - const response = await fetch(`/api/collections/${collectionId}`, { - method: "DELETE", - headers: { "X-API-KEY": apiKey.key }, - }); + const result = await apiFetch( + `/api/collections/${collectionId}`, + apiKey?.key ?? "", + { method: "DELETE" }, + ); - if (response.ok) { - const result = await response.json(); - const jobId = result.data?.job_id; - - if (jobId) { - // Poll the delete job status - const pollDeleteStatus = async () => { - const currentApiKey = apiKeyRef.current; - if (!currentApiKey) return; - - try { - const jobResponse = await fetch( - `/api/collections/jobs/${jobId}`, - { - headers: { "X-API-KEY": currentApiKey.key }, - }, - ); + const jobId = result.data?.job_id; - if (jobResponse.ok) { - const jobResult = await jobResponse.json(); - const jobData = jobResult.data || jobResult; - const status = jobData.status; - const statusLower = status?.toLowerCase(); - - if (statusLower === "successful") { - // Job completed successfully - remove from UI and clean up cache - deleteCollectionFromCache(collectionId); - setCollections((prev) => - prev.filter((c) => c.id !== collectionId), - ); - setSelectedCollection(null); - } else if (statusLower === "failed") { - // Job failed - restore original collection - alert("Failed to delete collection"); - if (originalCollection) { - setCollections((prev) => - prev.map((c) => - c.id === collectionId ? originalCollection : c, - ), - ); - setSelectedCollection((prev) => - prev?.id === collectionId ? originalCollection : prev, - ); - } - } else { - // Still processing - keep status as "deleting" and poll again - setTimeout(pollDeleteStatus, 2000); // Poll every 2 seconds - } - } else { - // Failed to get job status - alert("Failed to check delete status"); - if (originalCollection) { - setCollections((prev) => - prev.map((c) => - c.id === collectionId ? originalCollection : c, - ), - ); - setSelectedCollection((prev) => - prev?.id === collectionId ? originalCollection : prev, - ); - } - } - } catch (error) { - console.error("Error polling delete status:", error); - alert("Failed to check delete status"); + if (jobId) { + // Poll the delete job status + const pollDeleteStatus = async () => { + const currentApiKey = apiKeyRef.current; + if (!currentApiKey) return; + + try { + const jobResult = await apiFetch< + { data?: JobStatusData } & JobStatusData + >(`/api/collections/jobs/${jobId}`, currentApiKey?.key ?? ""); + const jobData = jobResult.data || jobResult; + const status = jobData.status; + const statusLower = status?.toLowerCase(); + + if (statusLower === "successful") { + // Job completed successfully - remove from UI and clean up cache + deleteCollectionFromCache(collectionId); + setCollections((prev) => + prev.filter((c) => c.id !== collectionId), + ); + setSelectedCollection(null); + } else if (statusLower === "failed") { + // Job failed - restore original collection + alert("Failed to delete collection"); if (originalCollection) { setCollections((prev) => prev.map((c) => @@ -769,28 +713,33 @@ export default function KnowledgeBasePage() { prev?.id === collectionId ? originalCollection : prev, ); } + } else { + // Still processing - keep status as "deleting" and poll again + setTimeout(pollDeleteStatus, 2000); // Poll every 2 seconds } - }; + } catch (error) { + console.error("Error polling delete status:", error); + alert("Failed to check delete status"); + if (originalCollection) { + setCollections((prev) => + prev.map((c) => + c.id === collectionId ? originalCollection : c, + ), + ); + setSelectedCollection((prev) => + prev?.id === collectionId ? originalCollection : prev, + ); + } + } + }; - // Start polling - pollDeleteStatus(); - } else { - // No job_id returned, assume immediate success - deleteCollectionFromCache(collectionId); - setCollections((prev) => prev.filter((c) => c.id !== collectionId)); - setSelectedCollection(null); - } + // Start polling + pollDeleteStatus(); } else { - alert("Failed to delete collection"); - // Restore the original collection on failure - if (originalCollection) { - setCollections((prev) => - prev.map((c) => (c.id === collectionId ? originalCollection : c)), - ); - setSelectedCollection((prev) => - prev?.id === collectionId ? originalCollection : prev, - ); - } + // No job_id returned, assume immediate success + deleteCollectionFromCache(collectionId); + setCollections((prev) => prev.filter((c) => c.id !== collectionId)); + setSelectedCollection(null); } } catch (error) { console.error("Error deleting collection:", error); @@ -807,6 +756,23 @@ export default function KnowledgeBasePage() { } }; + // Fetch document details and set preview + const fetchAndPreviewDoc = async (doc: Document) => { + setPreviewDoc(doc); + if (isAuthenticated) { + try { + const data = await apiFetch( + `/api/document/${doc.id}`, + apiKey?.key ?? "", + ); + const documentDetails = (data.data || data) as Document; + setPreviewDoc(documentDetails); + } catch (err) { + console.error("Failed to fetch document details:", err); + } + } + }; + // Toggle document selection const toggleDocumentSelection = (documentId: string) => { const newSelection = new Set(selectedDocuments); @@ -819,7 +785,7 @@ export default function KnowledgeBasePage() { }; useEffect(() => { - if (apiKey) { + if (isAuthenticated) { fetchCollections(); fetchDocuments(); } @@ -828,7 +794,7 @@ export default function KnowledgeBasePage() { // Keep apiKeyRef in sync so polling always has the current key useEffect(() => { apiKeyRef.current = apiKey; - }, [apiKey]); + }, [apiKey, isAuthenticated]); // Keep fetchCollectionsRef in sync so polling always has the current function useEffect(() => { @@ -855,8 +821,8 @@ export default function KnowledgeBasePage() { } }); - if (newJobAdded && apiKey) startPolling(); - }, [collections, apiKey]); + if (newJobAdded && isAuthenticated) startPolling(); + }, [collections, isAuthenticated]); // Reset showAllDocs when selectedCollection changes useEffect(() => { @@ -874,10 +840,7 @@ export default function KnowledgeBasePage() { }, []); return ( -
+
{/* Main Content */} @@ -890,21 +853,19 @@ export default function KnowledgeBasePage() { {/* Content Area - Split View */}
{/* Left Panel - Collections List */} -
+
{/* Create Button */}
- +
@@ -917,124 +878,68 @@ export default function KnowledgeBasePage() { No knowledge bases yet. Create your first one!
) : ( -
- {collections.map((collection) => ( -
{ - setShowCreateForm(false); - setShowDocumentPicker(false); - fetchCollectionDetails(collection.id); - }} - className={`border rounded-lg p-3 cursor-pointer transition-colors ${ - selectedCollection?.id === collection.id ? "" : "" - }`} - style={{ - backgroundColor: - selectedCollection?.id === collection.id - ? "hsl(202, 100%, 95%)" - : colors.bg.primary, - borderColor: - selectedCollection?.id === collection.id - ? "hsl(202, 100%, 50%)" - : colors.border, - }} - > -
-
-
- - + {collections.map((collection) => { + const isSelected = selectedCollection?.id === collection.id; + return ( +
+
+
+ - -

- {collection.name} -

+

+ {collection.name} +

+
+ {collection.description && ( +

+ {collection.description} +

+ )} +

+ {formatDate(collection.inserted_at)} +

- {collection.description && ( -

{ + e.stopPropagation(); + handleDeleteCollection(collection.id); + }} + className="p-1.5 rounded-md border border-red-200 bg-white text-red-500 hover:bg-red-50 transition-colors shrink-0 cursor-pointer" + title="Delete Knowledge Base" > - {collection.description} -

+ + )} -

- {formatDate(collection.inserted_at)} -

- {!collection.id.startsWith("optimistic-") && ( - - )} -
-
- ))} + + ); + })}
)}
- {/* Right Panel - Create Form or Preview */}
{showCreateForm ? ( - /* Create Form */
-

+

Create Knowledge Base

{/* Name Input */}
- - setCollectionName(e.target.value)} + onChange={setCollectionName} 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 */}
-