diff --git a/app/components/ConfigModal.tsx b/app/components/ConfigModal.tsx index 7c8eb22..6b022ec 100644 --- a/app/components/ConfigModal.tsx +++ b/app/components/ConfigModal.tsx @@ -3,10 +3,13 @@ * Shows assistant config, model, temperature, instructions, tools, and vector stores */ -"use client" -import React, { useState, useEffect } from 'react'; -import { colors } from '@/app/lib/colors'; -import { EvalJob, AssistantConfig } from './types'; +"use client"; + +import React, { useState, useEffect } from "react"; +import { colors } from "@/app/lib/colors"; +import { EvalJob, AssistantConfig } from "./types"; +import { useAuth } from "@/app/lib/context/AuthContext"; +import { Tool } from "@/app/lib/configTypes"; interface ConfigModalProps { isOpen: boolean; @@ -23,12 +26,19 @@ interface ConfigVersionInfo { temperature?: number; tools?: { type: string; [key: string]: unknown }[]; provider?: string; - type?: 'text' | 'stt' | 'tts'; + type?: "text" | "stt" | "tts"; knowledge_base_ids?: string[]; } -export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: ConfigModalProps) { - const [configVersionInfo, setConfigVersionInfo] = useState(null); +export default function ConfigModal({ + isOpen, + onClose, + job, + assistantConfig, +}: ConfigModalProps) { + const { activeKey } = useAuth(); + const [configVersionInfo, setConfigVersionInfo] = + useState(null); const [isLoadingConfig, setIsLoadingConfig] = useState(false); // Fetch full config version details when modal opens @@ -41,42 +51,36 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C const fetchConfigVersionInfo = async () => { setIsLoadingConfig(true); try { - // Get API key from localStorage - const stored = localStorage.getItem('kaapi_api_keys'); - if (!stored) { - console.error('No API key found'); - return; - } - const keys = JSON.parse(stored); - const apiKey = keys.length > 0 ? keys[0].key : null; + const apiKey = activeKey?.key; if (!apiKey) { - console.error('No API key found'); + console.error("No API key found"); return; } // Fetch config name first const configResponse = await fetch(`/api/configs/${job.config_id}`, { - headers: { 'X-API-KEY': apiKey }, + headers: { "X-API-KEY": apiKey }, }); if (!configResponse.ok) { - console.error('Failed to fetch config info'); + console.error("Failed to fetch config info"); return; } const configData = await configResponse.json(); - const configName = configData.success && configData.data ? configData.data.name : null; + const configName = + configData.success && configData.data ? configData.data.name : null; // Fetch full version details including config_blob const versionResponse = await fetch( `/api/configs/${job.config_id}/versions/${job.config_version}`, { - headers: { 'X-API-KEY': apiKey }, - } + headers: { "X-API-KEY": apiKey }, + }, ); if (!versionResponse.ok) { - console.error('Failed to fetch version details'); + console.error("Failed to fetch version details"); return; } @@ -96,10 +100,12 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C // 2. Check tools array for knowledge_base_ids if (params.tools) { const toolKbIds = params.tools - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .filter((tool: any) => Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .flatMap((tool: any) => tool.knowledge_base_ids); + .filter( + (tool: Tool) => + Array.isArray(tool.knowledge_base_ids) && + tool.knowledge_base_ids.length > 0, + ) + .flatMap((tool: Tool) => tool.knowledge_base_ids); knowledgeBaseIds.push(...toolKbIds); } @@ -107,32 +113,44 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C const uniqueKbIds = [...new Set(knowledgeBaseIds)]; setConfigVersionInfo({ - name: configName || 'Unknown Config', + name: configName || "Unknown Config", version: job.config_version!, model: params.model, instructions: params.instructions, temperature: params.temperature, tools: params.tools, provider: blob?.completion?.provider, - type: blob?.completion?.type || 'text', - knowledge_base_ids: uniqueKbIds.length > 0 ? uniqueKbIds : undefined, + type: blob?.completion?.type || "text", + knowledge_base_ids: + uniqueKbIds.length > 0 ? uniqueKbIds : undefined, }); } } catch (error) { - console.error('Error fetching config version info:', error); + console.error("Error fetching config version info:", error); } finally { setIsLoadingConfig(false); } }; fetchConfigVersionInfo(); - }, [isOpen, job.config_id, job.config_version]); + }, [isOpen, job.config_id, job.config_version, activeKey]); if (!isOpen) return null; - const ConfigField = ({ label, children }: { label: string; children: React.ReactNode }) => ( + const ConfigField = ({ + label, + children, + }: { + label: string; + children: React.ReactNode; + }) => (
-
{label}
+
+ {label} +
{children}
); @@ -152,7 +170,10 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C const Tag = ({ children }: { children: React.ReactNode }) => ( {children} @@ -165,20 +186,36 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C >
e.stopPropagation()} > {/* Header */} -
+
-

- {configVersionInfo?.name || 'Configuration'} +

+ {configVersionInfo?.name || "Configuration"} {configVersionInfo?.version && ( - v{configVersionInfo.version} + + v{configVersionInfo.version} + )}

{configVersionInfo?.provider && ( -

{configVersionInfo.provider}

+

+ {configVersionInfo.provider} +

)}
@@ -196,132 +243,201 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C
{isLoadingConfig ? (
-
-

Loading configuration...

+
+

+ Loading configuration... +

) : ( <> {assistantConfig?.name && ( -
{assistantConfig.name}
+
+ {assistantConfig.name} +
)} {job.assistant_id && ( -
{job.assistant_id}
+
+ {job.assistant_id} +
)} - {configVersionInfo?.model || assistantConfig?.model || job.config?.model || 'N/A'} + + {configVersionInfo?.model || + assistantConfig?.model || + job.config?.model || + "N/A"} + - {(configVersionInfo?.temperature !== undefined || assistantConfig?.temperature !== undefined || job.config?.temperature !== undefined) && ( + {(configVersionInfo?.temperature !== undefined || + assistantConfig?.temperature !== undefined || + job.config?.temperature !== undefined) && ( {configVersionInfo?.temperature !== undefined ? configVersionInfo.temperature - : (assistantConfig?.temperature !== undefined ? assistantConfig.temperature : job.config?.temperature)} + : assistantConfig?.temperature !== undefined + ? assistantConfig.temperature + : job.config?.temperature} )} - {configVersionInfo?.knowledge_base_ids && configVersionInfo.knowledge_base_ids.length > 0 && ( - - {configVersionInfo.knowledge_base_ids.join('\n')} - - )} - - {(configVersionInfo?.instructions || assistantConfig?.instructions || job.config?.instructions) && ( + {configVersionInfo?.knowledge_base_ids && + configVersionInfo.knowledge_base_ids.length > 0 && ( + + + {configVersionInfo.knowledge_base_ids.join("\n")} + + + )} + + {(configVersionInfo?.instructions || + assistantConfig?.instructions || + job.config?.instructions) && ( - {configVersionInfo?.instructions || assistantConfig?.instructions || job.config?.instructions} + {configVersionInfo?.instructions || + assistantConfig?.instructions || + job.config?.instructions} )} - {(Array.isArray(configVersionInfo?.tools) && configVersionInfo.tools.length > 0) && ( - -
-
+ {Array.isArray(configVersionInfo?.tools) && + configVersionInfo.tools.length > 0 && ( + +
+
+ {configVersionInfo.tools.map((tool, idx) => ( + {tool.type} + ))} +
{configVersionInfo.tools.map((tool, idx) => ( - {tool.type} + + {Array.isArray(tool.knowledge_base_ids) && + tool.knowledge_base_ids.length > 0 && ( +
+
+ Knowledge Base IDs ({tool.type}) +
+ + {tool.knowledge_base_ids.join("\n")} + +
+ )} + {tool.max_num_results !== undefined && ( +
+
+ Max Results ({tool.type}) +
+
+ {String(tool.max_num_results)} +
+
+ )} +
))}
- {configVersionInfo.tools.map((tool, idx) => ( - - {Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0 && ( -
-
- Knowledge Base IDs ({tool.type}) -
- {tool.knowledge_base_ids.join('\n')} -
- )} - {tool.max_num_results !== undefined && ( -
-
- Max Results ({tool.type}) + + )} + + {Array.isArray(job.config?.tools) && + job.config.tools.length > 0 && + !configVersionInfo?.tools?.length && ( + +
+
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {job.config.tools.map((tool: any, idx) => ( + {tool.type} + ))} +
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {job.config.tools.map((tool: any, idx: number) => ( + + {Array.isArray(tool.knowledge_base_ids) && + tool.knowledge_base_ids.length > 0 && ( +
+
+ Knowledge Base IDs ({tool.type}) +
+ + {tool.knowledge_base_ids.join("\n")} + +
+ )} + {tool.max_num_results !== undefined && ( +
+
+ Max Results ({tool.type}) +
+
+ {String(tool.max_num_results)} +
-
{String(tool.max_num_results)}
-
- )} - - ))} -
- - )} - - {Array.isArray(job.config?.tools) && job.config.tools.length > 0 && !configVersionInfo?.tools?.length && ( - -
+ )} + + ))} +
+
+ )} + + {Array.isArray(assistantConfig?.knowledge_base_ids) && + assistantConfig.knowledge_base_ids.length > 0 && ( + + + {assistantConfig.knowledge_base_ids.join("\n")} + + + )} + + {Array.isArray(job.config?.include) && + job.config.include.length > 0 && ( +
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - {job.config.tools.map((tool: any, idx) => ( - {tool.type} + {job.config.include.map((item, idx) => ( + {item} ))}
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - {job.config.tools.map((tool: any, idx: number) => ( - - {Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0 && ( -
-
- Knowledge Base IDs ({tool.type}) -
- {tool.knowledge_base_ids.join('\n')} -
- )} - {tool.max_num_results !== undefined && ( -
-
- Max Results ({tool.type}) -
-
{String(tool.max_num_results)}
-
- )} -
- ))} -
-
- )} - - {Array.isArray(assistantConfig?.knowledge_base_ids) && assistantConfig.knowledge_base_ids.length > 0 && ( - - {assistantConfig.knowledge_base_ids.join('\n')} - - )} - - {Array.isArray(job.config?.include) && job.config.include.length > 0 && ( - -
- {job.config.include.map((item, idx) => ( - {item} - ))} -
-
- )} + + )} )}
diff --git a/app/configurations/page.tsx b/app/configurations/page.tsx index 852b29e..a362468 100644 --- a/app/configurations/page.tsx +++ b/app/configurations/page.tsx @@ -19,10 +19,14 @@ import { SavedConfig } from '@/app/lib/types/configs'; import ConfigCard from '@/app/components/ConfigCard'; import { LoaderBox } from '@/app/components/Loader'; import { EvalJob } from '@/app/components/types'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; +import { apiFetch } from '@/app/lib/apiClient'; export default function ConfigLibraryPage() { const router = useRouter(); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); + const { activeKey } = useAuth(); const { configGroups, isLoading, error, refetch, isCached, loadMoreConfigs, hasMoreConfigs, isLoadingMore } = useConfigs({ pageSize: 10 }); const [searchQuery, setSearchQuery] = useState(''); const [evaluationCounts, setEvaluationCounts] = useState>({}); @@ -30,20 +34,10 @@ export default function ConfigLibraryPage() { // Fetch evaluation counts for each config useEffect(() => { const fetchEvaluationCounts = async () => { - try { - const stored = localStorage.getItem('kaapi_api_keys'); - if (!stored) return; - - const keys = JSON.parse(stored); - if (keys.length === 0) return; - - const response = await fetch('/api/evaluations', { - headers: { 'X-API-KEY': keys[0].key }, - }); - - if (!response.ok) return; + if (!activeKey) return; - const data = await response.json(); + try { + const data = await apiFetch('/api/evaluations', activeKey.key); const jobs: EvalJob[] = Array.isArray(data) ? data : (data.data || []); // Count evaluations per config_id @@ -61,7 +55,7 @@ export default function ConfigLibraryPage() { }; fetchEvaluationCounts(); - }, []); + }, [activeKey]); // Filter configs based on search query const filteredConfigs = configGroups.filter((group) => @@ -84,7 +78,6 @@ export default function ConfigLibraryPage() {
- {/* Header */}
(null); + const getApiKey = (): string | null => activeKey?.key ?? null; + // Populate the editor from a fully-loaded SavedConfig const applyConfig = React.useCallback( (config: SavedConfig, selectInHistory?: boolean) => { @@ -141,7 +146,9 @@ function PromptEditorContent() { setCurrentConfigVersion(config.version); setTools(config.tools || []); setExpandedConfigs((prev) => - prev.has(currentConfigParentId) ? prev : new Set([...prev, currentConfigParentId]), + prev.has(currentConfigParentId) + ? prev + : new Set([...prev, currentConfigParentId]), ); if (selectInHistory) setSelectedVersion(config); }, @@ -274,7 +281,6 @@ function PromptEditorContent() { savedConfigs, ]); - // Save current configuration const handleSaveConfig = async () => { if (!currentConfigName.trim()) { diff --git a/app/datasets/page.tsx b/app/datasets/page.tsx index 0e2c32b..2e6b298 100644 --- a/app/datasets/page.tsx +++ b/app/datasets/page.tsx @@ -7,9 +7,11 @@ "use client" import { useState, useEffect } from 'react'; -import { APIKey, STORAGE_KEY } from '../keystore/page'; -import Sidebar from '../components/Sidebar'; -import { useToast } from '../components/Toast'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; +import type { APIKey } from '@/app/keystore/page'; +import Sidebar from '@/app/components/Sidebar'; +import { useToast } from '@/app/components/Toast'; // Backend response interface export interface Dataset { @@ -28,7 +30,7 @@ export const DATASETS_STORAGE_KEY = 'kaapi_datasets'; export default function Datasets() { const toast = useToast(); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const [isModalOpen, setIsModalOpen] = useState(false); const [datasets, setDatasets] = useState([]); const [selectedFile, setSelectedFile] = useState(null); @@ -37,27 +39,12 @@ export default function Datasets() { const [isUploading, setIsUploading] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [apiKey, setApiKey] = useState(null); + const { activeKey: apiKey } = useAuth(); // Pagination state const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; - // 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); - } - } - }, []); - // Fetch datasets from backend when API key is available useEffect(() => { if (apiKey) { diff --git a/app/document/page.tsx b/app/document/page.tsx index 6eebcd0..8570a8e 100644 --- a/app/document/page.tsx +++ b/app/document/page.tsx @@ -1,10 +1,12 @@ "use client"; import { useState, useEffect } from 'react'; -import { APIKey, STORAGE_KEY } from '../keystore/page'; -import Sidebar from '../components/Sidebar'; -import { useToast } from '../components/Toast'; -import { formatDate } from '../components/utils'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; +import type { APIKey } from '@/app/keystore/page'; +import Sidebar from '@/app/components/Sidebar'; +import { useToast } from '@/app/components/Toast'; +import { formatDate } from '@/app/components/utils'; // Backend response interface export interface Document { @@ -19,7 +21,7 @@ export interface Document { export default function DocumentPage() { const toast = useToast(); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const [isModalOpen, setIsModalOpen] = useState(false); const [documents, setDocuments] = useState([]); const [selectedDocument, setSelectedDocument] = useState(null); @@ -28,27 +30,12 @@ export default function DocumentPage() { const [isUploading, setIsUploading] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [apiKey, setApiKey] = useState(null); + const { activeKey: apiKey } = useAuth(); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage] = useState(10); - // 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); - } - } - }, []); - // Fetch documents from backend when API key is available useEffect(() => { if (apiKey) { diff --git a/app/evaluations/[id]/page.tsx b/app/evaluations/[id]/page.tsx index 9e14686..a073dbf 100644 --- a/app/evaluations/[id]/page.tsx +++ b/app/evaluations/[id]/page.tsx @@ -8,7 +8,8 @@ import { useState, useEffect, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; -import { APIKey, STORAGE_KEY } from "@/app/keystore/page"; +import { useAuth } from "@/app/lib/context/AuthContext"; +import { useApp } from "@/app/lib/context/AppContext"; import { EvalJob, AssistantConfig, @@ -39,9 +40,9 @@ export default function EvaluationReport() { >(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [apiKeys, setApiKeys] = useState([]); + const { apiKeys } = useAuth(); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const [selectedKeyId, setSelectedKeyId] = useState(""); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [exportFormat, setExportFormat] = useState<"row" | "grouped">("row"); const [isResyncing, setIsResyncing] = useState(false); @@ -64,21 +65,12 @@ export default function EvaluationReport() { return `"${sanitized}"`; }; - // Load API keys from localStorage + // Set initial selected key from context useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - const keys = JSON.parse(stored); - setApiKeys(keys); - if (keys.length > 0) { - setSelectedKeyId(keys[0].id); - } - } catch (e) { - console.error("Failed to load API keys:", e); - } + if (apiKeys.length > 0 && !selectedKeyId) { + setSelectedKeyId(apiKeys[0].id); } - }, []); + }, [apiKeys, selectedKeyId]); // Fetch job details const fetchJobDetails = useCallback(async () => { diff --git a/app/evaluations/page.tsx b/app/evaluations/page.tsx index 36b016e..1411173 100644 --- a/app/evaluations/page.tsx +++ b/app/evaluations/page.tsx @@ -10,11 +10,12 @@ import { useState, useEffect, useCallback, Suspense } from 'react'; import { colors } from '@/app/lib/colors'; import { useSearchParams } from 'next/navigation' -import { APIKey, STORAGE_KEY } from '@/app/keystore/page'; import { Dataset } from '@/app/datasets/page'; import Sidebar from '@/app/components/Sidebar'; 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 Loader from '@/app/components/Loader'; import DatasetsTab from '@/app/components/evaluations/DatasetsTab'; import EvaluationsTab from '@/app/components/evaluations/EvaluationsTab'; @@ -32,8 +33,8 @@ function SimplifiedEvalContent() { return (tabParam === 'evaluations' || tabParam === 'datasets') ? tabParam as Tab : 'datasets'; }); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [apiKeys, setApiKeys] = useState([]); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); + const { apiKeys } = useAuth(); const [selectedKeyId, setSelectedKeyId] = useState(''); // Dataset creation state @@ -62,21 +63,12 @@ function SimplifiedEvalContent() { }); const [isEvaluating, setIsEvaluating] = useState(false); - // Load API keys + // Set initial selected key from context useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - const keys = JSON.parse(stored); - setApiKeys(keys); - if (keys.length > 0) { - setSelectedKeyId(keys[0].id); - } - } catch (e) { - console.error('Failed to load API keys:', e); - } + if (apiKeys.length > 0 && !selectedKeyId) { + setSelectedKeyId(apiKeys[0].id); } - }, []); + }, [apiKeys, selectedKeyId]); // Fetch datasets from backend const loadStoredDatasets = useCallback(async () => { diff --git a/app/keystore/page.tsx b/app/keystore/page.tsx index 0bdfee5..bd1ec60 100644 --- a/app/keystore/page.tsx +++ b/app/keystore/page.tsx @@ -8,6 +8,8 @@ import { useState, useEffect } from 'react'; import Sidebar from '../components/Sidebar'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; export interface APIKey { id: string; @@ -20,35 +22,14 @@ export interface APIKey { export const STORAGE_KEY = 'kaapi_api_keys'; export default function KaapiKeystore() { - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const [isModalOpen, setIsModalOpen] = useState(false); - const [apiKeys, setApiKeys] = useState([]); + const { apiKeys, addKey, removeKey: removeApiKey } = useAuth(); const [newKeyLabel, setNewKeyLabel] = useState(''); const [newKeyValue, setNewKeyValue] = useState(''); const [newKeyProvider, setNewKeyProvider] = useState('Kaapi'); const [visibleKeys, setVisibleKeys] = useState>(new Set()); - // Load API keys from localStorage on mount - useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - // eslint-disable-next-line react-hooks/set-state-in-effect - setApiKeys(JSON.parse(stored)); - } catch (e) { - console.error('Failed to load API keys:', e); - } - } - }, []); - - // Save API keys to localStorage whenever they change - useEffect(() => { - if (apiKeys.length > 0) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(apiKeys)); - } else { - localStorage.removeItem(STORAGE_KEY); - } - }, [apiKeys]); const providers = ['Kaapi']; @@ -66,7 +47,7 @@ export default function KaapiKeystore() { createdAt: new Date().toISOString(), }; - setApiKeys([...apiKeys, newKey]); + addKey(newKey); setNewKeyLabel(''); setNewKeyValue(''); setNewKeyProvider('Kaapi'); @@ -76,7 +57,7 @@ export default function KaapiKeystore() { }; const handleDeleteKey = (id: string) => { - setApiKeys(apiKeys.filter(key => key.id !== id)); + removeApiKey(id); setVisibleKeys(prev => { const next = new Set(prev); next.delete(id); diff --git a/app/knowledge-base/page.tsx b/app/knowledge-base/page.tsx index 5c34c1b..2eece0d 100644 --- a/app/knowledge-base/page.tsx +++ b/app/knowledge-base/page.tsx @@ -4,7 +4,8 @@ import { useState, useEffect, useRef } 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'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; export interface Document { id: string; @@ -29,7 +30,7 @@ export interface Collection { } export default function KnowledgeBasePage() { - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const [collections, setCollections] = useState([]); const [availableDocuments, setAvailableDocuments] = useState([]); const [selectedCollection, setSelectedCollection] = useState(null); @@ -41,11 +42,11 @@ export default function KnowledgeBasePage() { const [collectionToDelete, setCollectionToDelete] = useState(null); const [showDocPreviewModal, setShowDocPreviewModal] = useState(false); const [previewDoc, setPreviewDoc] = useState(null); - const [apiKey, setApiKey] = useState(null); + const { activeKey: apiKey } = useAuth(); const [showAllDocs, setShowAllDocs] = useState(false); // Polling refs — persist across renders, no stale closures - const apiKeyRef = useRef(null); + const apiKeyRef = useRef(null); const activeJobsRef = useRef>(new Map()); // collectionId → jobId const pollingRef = useRef | null>(null); const fetchCollectionsRef = useRef<(() => Promise) | null>(null); @@ -730,21 +731,6 @@ export default function KnowledgeBasePage() { 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(); diff --git a/app/layout.tsx b/app/layout.tsx index 74e51e7..48db982 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { ToastProvider } from "./components/Toast"; +import { ToastProvider } from "@/app/components/Toast"; +import { AuthProvider } from "@/app/lib/context/AuthContext"; +import { AppProvider } from "@/app/lib/context/AppContext"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -29,7 +31,9 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > - {children} + + {children} + diff --git a/app/lib/configTypes.ts b/app/lib/configTypes.ts index e63bbe3..b5d4a10 100644 --- a/app/lib/configTypes.ts +++ b/app/lib/configTypes.ts @@ -1,6 +1,5 @@ /** * TypeScript types for Config Management API - * Based on CONFIG_MGMT.md specification */ // Config Blob Structure diff --git a/app/lib/context/AppContext.tsx b/app/lib/context/AppContext.tsx new file mode 100644 index 0000000..d8bdee1 --- /dev/null +++ b/app/lib/context/AppContext.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { createContext, useContext, useState } from 'react'; + +interface AppContextValue { + sidebarCollapsed: boolean; + setSidebarCollapsed: (collapsed: boolean) => void; + toggleSidebar: () => void; +} + +const AppContext = createContext(null); + +export function AppProvider({ children }: { children: React.ReactNode }) { + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const toggleSidebar = () => setSidebarCollapsed(prev => !prev); + + return ( + + {children} + + ); +} + +export function useApp() { + const ctx = useContext(AppContext); + if (!ctx) throw new Error('useApp must be used within AppProvider'); + return ctx; +} diff --git a/app/lib/context/AuthContext.tsx b/app/lib/context/AuthContext.tsx new file mode 100644 index 0000000..d6aafc1 --- /dev/null +++ b/app/lib/context/AuthContext.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { APIKey } from "@/app/keystore/page"; +import { + createContext, + useContext, + useState, + useCallback, + useEffect, +} from "react"; + +const STORAGE_KEY = "kaapi_api_keys"; + +interface AuthContextValue { + apiKeys: APIKey[]; + activeKey: APIKey | null; + addKey: (key: APIKey) => void; + removeKey: (id: string) => void; + setKeys: (keys: APIKey[]) => void; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [apiKeys, setApiKeys] = useState([]); + + // Initialize from localStorage after hydration to avoid SSR mismatch. + // setState in effect is intentional here — this is a one-time external storage read. + useEffect(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + // eslint-disable-next-line react-hooks/set-state-in-effect + if (stored) setApiKeys(JSON.parse(stored)); + } catch { + /* ignore malformed data */ + } + }, []); + + const persist = useCallback((keys: APIKey[]) => { + setApiKeys(keys); + if (keys.length > 0) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(keys)); + } else { + localStorage.removeItem(STORAGE_KEY); + } + }, []); + + const addKey = useCallback( + (key: APIKey) => persist([...apiKeys, key]), + [apiKeys, persist], + ); + const removeKey = useCallback( + (id: string) => persist(apiKeys.filter((k) => k.id !== id)), + [apiKeys, persist], + ); + const setKeys = useCallback((keys: APIKey[]) => persist(keys), [persist]); + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used within AuthProvider"); + return ctx; +} diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 6875401..fe00e63 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -10,7 +10,12 @@ import { import { SavedConfig, ConfigGroup } from './types/configs'; export function timeAgo(dateStr: string): string { - return formatDistanceToNow(new Date(dateStr), { addSuffix: true }); + const date = + dateStr.includes("Z") || dateStr.includes("+") + ? new Date(dateStr) + : new Date(dateStr + "Z"); + + return formatDistanceToNow(date, { addSuffix: true }); } export function getExistingForProvider( diff --git a/app/settings/credentials/page.tsx b/app/settings/credentials/page.tsx index 5e1be10..5b9d18b 100644 --- a/app/settings/credentials/page.tsx +++ b/app/settings/credentials/page.tsx @@ -11,7 +11,8 @@ import { useState, useEffect } from "react"; import Sidebar from "@/app/components/Sidebar"; import { colors } from "@/app/lib/colors"; import { useToast } from "@/app/components/Toast"; -import { APIKey, STORAGE_KEY } from "@/app/keystore/page"; +import { useAuth } from "@/app/lib/context/AuthContext"; +import { useApp } from "@/app/lib/context/AppContext"; import { PROVIDERS, Credential, @@ -25,8 +26,8 @@ import Link from "next/link"; export default function CredentialsPage() { const toast = useToast(); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [apiKeys, setApiKeys] = useState([]); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); + const { apiKeys } = useAuth(); const [selectedProvider, setSelectedProvider] = useState( PROVIDERS[0], ); @@ -40,18 +41,6 @@ export default function CredentialsPage() { const [existingCredential, setExistingCredential] = useState(null); - // Load API keys from localStorage - useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - setApiKeys(JSON.parse(stored)); - } catch { - /* ignore */ - } - } - }, []); - // Load credentials once we have an API key useEffect(() => { if (apiKeys.length === 0) return; diff --git a/app/speech-to-text/page.tsx b/app/speech-to-text/page.tsx index 20c85c7..3937d16 100644 --- a/app/speech-to-text/page.tsx +++ b/app/speech-to-text/page.tsx @@ -13,7 +13,9 @@ import TabNavigation from '@/app/components/TabNavigation'; import StatusBadge from '@/app/components/StatusBadge'; import Loader, { LoaderBox } from '@/app/components/Loader'; import { useToast } from '@/app/components/Toast'; -import { APIKey, STORAGE_KEY } from '@/app/keystore/page'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; +import type { APIKey } from '@/app/keystore/page'; import WaveformVisualizer from '@/app/components/speech-to-text/WaveformVisualizer'; import { computeWordDiff } from '@/app/components/speech-to-text/TranscriptionDiffViewer'; import ErrorModal from '@/app/components/ErrorModal'; @@ -317,9 +319,9 @@ export default function SpeechToTextPage() { const toast = useToast(); const [activeTab, setActiveTab] = useState('datasets'); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const [leftPanelWidth] = useState(450); - const [apiKeys, setApiKeys] = useState([]); + const { apiKeys } = useAuth(); const [languages, setLanguages] = useState([]); const [datasetName, setDatasetName] = useState(''); const [datasetDescription, setDatasetDescription] = useState(''); @@ -351,18 +353,6 @@ export default function SpeechToTextPage() { const [errorModalOpen, setErrorModalOpen] = useState(false); const [errorModalMessage, setErrorModalMessage] = useState(''); - // Load API keys - useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - setApiKeys(JSON.parse(stored)); - } catch (e) { - console.error('Failed to load API keys:', e); - } - } - }, []); - // Load languages const loadLanguages = async () => { if (apiKeys.length === 0) return; diff --git a/app/text-to-speech/page.tsx b/app/text-to-speech/page.tsx index 47081f6..b3e5946 100644 --- a/app/text-to-speech/page.tsx +++ b/app/text-to-speech/page.tsx @@ -13,7 +13,9 @@ import TabNavigation from '@/app/components/TabNavigation'; import Loader, { LoaderBox } from '@/app/components/Loader'; import { getStatusColor } from '@/app/components/utils'; import { useToast } from '@/app/components/Toast'; -import { APIKey, STORAGE_KEY } from '@/app/keystore/page'; +import { useAuth } from '@/app/lib/context/AuthContext'; +import { useApp } from '@/app/lib/context/AppContext'; +import type { APIKey } from '@/app/keystore/page'; import ErrorModal from '@/app/components/ErrorModal'; type Tab = 'datasets' | 'evaluations'; @@ -231,11 +233,11 @@ export default function TextToSpeechPage() { const [activeTab, setActiveTab] = useState('datasets'); // UI State - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const { sidebarCollapsed, setSidebarCollapsed } = useApp(); const leftPanelWidth = 450; // API Keys - const [apiKeys, setApiKeys] = useState([]); + const { apiKeys } = useAuth(); // Languages const [languages, setLanguages] = useState([]); @@ -270,18 +272,6 @@ export default function TextToSpeechPage() { const [errorModalOpen, setErrorModalOpen] = useState(false); const [errorModalMessage, setErrorModalMessage] = useState(''); - // Load API keys - useEffect(() => { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - setApiKeys(JSON.parse(stored)); - } catch (e) { - console.error('Failed to load API keys:', e); - } - } - }, []); - // Load languages const loadLanguages = async () => { if (apiKeys.length === 0) return;