From e1dc8e3c0866b29a7a9ff468c7712c3c8ca82f33 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:16:17 +0530 Subject: [PATCH 1/3] feat(*): api management centralize store --- app/components/ConfigModal.tsx | 378 ++++++++++++++-------- app/configurations/page.tsx | 27 +- app/configurations/prompt-editor/page.tsx | 235 ++++++++------ app/datasets/page.tsx | 27 +- app/document/page.tsx | 29 +- app/evaluations/[id]/page.tsx | 24 +- app/evaluations/page.tsx | 24 +- app/keystore/page.tsx | 31 +- app/knowledge-base/page.tsx | 24 +- app/layout.tsx | 8 +- app/lib/configTypes.ts | 1 - app/lib/context/AppContext.tsx | 28 ++ app/lib/context/AuthContext.tsx | 52 +++ app/lib/useConfigs.ts | 23 +- app/settings/credentials/page.tsx | 19 +- app/speech-to-text/page.tsx | 20 +- app/text-to-speech/page.tsx | 20 +- 17 files changed, 539 insertions(+), 431 deletions(-) create mode 100644 app/lib/context/AppContext.tsx create mode 100644 app/lib/context/AuthContext.tsx diff --git a/app/components/ConfigModal.tsx b/app/components/ConfigModal.tsx index 7c8eb224..6b022ec0 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 5e0124b8..c46e3fb7 100644 --- a/app/configurations/page.tsx +++ b/app/configurations/page.tsx @@ -9,7 +9,7 @@ */ "use client" -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Sidebar from '@/app/components/Sidebar'; import { colors } from '@/app/lib/colors'; @@ -17,10 +17,14 @@ import { useConfigs, SavedConfig } from '@/app/lib/useConfigs'; 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 } = useConfigs(); const [searchQuery, setSearchQuery] = useState(''); const [evaluationCounts, setEvaluationCounts] = useState>({}); @@ -28,20 +32,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 @@ -59,7 +53,7 @@ export default function ConfigLibraryPage() { }; fetchEvaluationCounts(); - }, []); + }, [activeKey]); // Filter configs based on search query const filteredConfigs = configGroups.filter((group) => @@ -82,7 +76,6 @@ export default function ConfigLibraryPage() {
- {/* Header */}
(false); const initialLoadComplete = !isLoading && savedConfigs.length >= 0; // Current working state - const [currentContent, setCurrentContent] = useState('You are a helpful AI assistant.\nYou provide clear and concise answers.\nYou are polite and professional.'); - const [currentConfigBlob, setCurrentConfigBlob] = useState(defaultConfig); - const [currentConfigName, setCurrentConfigName] = useState(''); - const [selectedConfigId, setSelectedConfigId] = useState(''); // Selected version ID - const [currentConfigParentId, setCurrentConfigParentId] = useState(''); // Parent config ID for evaluation + const [currentContent, setCurrentContent] = useState( + "You are a helpful AI assistant.\nYou provide clear and concise answers.\nYou are polite and professional.", + ); + const [currentConfigBlob, setCurrentConfigBlob] = + useState(defaultConfig); + const [currentConfigName, setCurrentConfigName] = useState(""); + const [selectedConfigId, setSelectedConfigId] = useState(""); // Selected version ID + const [currentConfigParentId, setCurrentConfigParentId] = + useState(""); // Parent config ID for evaluation const [currentConfigVersion, setCurrentConfigVersion] = useState(0); // Version number for evaluation - const [provider, setProvider] = useState('openai'); + const [provider, setProvider] = useState("openai"); const [temperature, setTemperature] = useState(0.7); const [tools, setTools] = useState([]); // UI state const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [commitMessage, setCommitMessage] = useState(''); + const [commitMessage, setCommitMessage] = useState(""); const [showHistorySidebar, setShowHistorySidebar] = useState(true); // Default open, or from URL param const [showConfigPane, setShowConfigPane] = useState(true); // Config pane collapse state // History viewing state - const [selectedVersion, setSelectedVersion] = useState(null); + const [selectedVersion, setSelectedVersion] = useState( + null, + ); const [compareWith, setCompareWith] = useState(null); - // Get API key from localStorage - const getApiKey = (): string | null => { - try { - const stored = localStorage.getItem('kaapi_api_keys'); - if (stored) { - const keys = JSON.parse(stored); - return keys.length > 0 ? keys[0].key : null; - } - } catch (e) { - console.error('Failed to get API key:', e); - } - return null; - }; + const getApiKey = (): string | null => activeKey?.key ?? null; // Handle URL query params after configs are loaded useEffect(() => { @@ -103,13 +105,13 @@ function PromptEditorContent() { // If new config is requested, reset to defaults if (isNewConfig) { - setCurrentContent(''); + setCurrentContent(""); setCurrentConfigBlob(defaultConfig); - setProvider('openai'); + setProvider("openai"); setTemperature(0.7); - setSelectedConfigId(''); - setCurrentConfigName(''); - setCurrentConfigParentId(''); + setSelectedConfigId(""); + setCurrentConfigName(""); + setCurrentConfigParentId(""); setCurrentConfigVersion(0); setTools([]); return; @@ -123,14 +125,17 @@ function PromptEditorContent() { if (urlVersion) { // Find specific version targetConfig = savedConfigs.find( - c => c.config_id === urlConfigId && c.version === parseInt(urlVersion) + (c) => + c.config_id === urlConfigId && c.version === parseInt(urlVersion), ); } else { // Find latest version for this config - const configVersions = savedConfigs.filter(c => c.config_id === urlConfigId); + const configVersions = savedConfigs.filter( + (c) => c.config_id === urlConfigId, + ); if (configVersions.length > 0) { targetConfig = configVersions.reduce((latest, current) => - current.version > latest.version ? current : latest + current.version > latest.version ? current : latest, ); } } @@ -165,7 +170,14 @@ function PromptEditorContent() { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialLoadComplete, savedConfigs, urlConfigId, urlVersion, showHistory, isNewConfig]); + }, [ + initialLoadComplete, + savedConfigs, + urlConfigId, + urlVersion, + showHistory, + isNewConfig, + ]); // Detect unsaved changes useEffect(() => { @@ -175,7 +187,7 @@ function PromptEditorContent() { return; } - const selectedConfig = savedConfigs.find(c => c.id === selectedConfigId); + const selectedConfig = savedConfigs.find((c) => c.id === selectedConfigId); if (!selectedConfig) { setHasUnsavedChanges(true); return; @@ -197,19 +209,26 @@ function PromptEditorContent() { }); setHasUnsavedChanges(promptChanged || configChanged); - }, [selectedConfigId, currentContent, currentConfigBlob, provider, temperature, tools, savedConfigs]); - + }, [ + selectedConfigId, + currentContent, + currentConfigBlob, + provider, + temperature, + tools, + savedConfigs, + ]); // Save current configuration const handleSaveConfig = async () => { if (!currentConfigName.trim()) { - toast.error('Please enter a configuration name'); + toast.error("Please enter a configuration name"); return; } const apiKey = getApiKey(); if (!apiKey) { - toast.error('No API key found. Please add an API key in the Keystore.'); + toast.error("No API key found. Please add an API key in the Keystore."); return; } @@ -236,7 +255,7 @@ function PromptEditorContent() { const configBlob: ConfigBlob = { completion: { provider: currentConfigBlob.completion.provider, - type: currentConfigBlob.completion.type || 'text', // Default to 'text' + type: currentConfigBlob.completion.type || "text", // Default to 'text' params: { model: currentConfigBlob.completion.params.model, instructions: currentContent, // Store prompt as instructions @@ -251,7 +270,9 @@ function PromptEditorContent() { }; // Check if updating existing config (same name exists) - const existingConfig = savedConfigs.find(c => c.name === currentConfigName.trim()); + const existingConfig = savedConfigs.find( + (c) => c.name === currentConfigName.trim(), + ); if (existingConfig) { // Create new version for existing config @@ -260,37 +281,44 @@ function PromptEditorContent() { commit_message: commitMessage.trim() || `Updated prompt and config`, }; - const response = await fetch(`/api/configs/${existingConfig.config_id}/versions`, { - method: 'POST', - headers: { - 'X-API-KEY': apiKey, - 'Content-Type': 'application/json', + const response = await fetch( + `/api/configs/${existingConfig.config_id}/versions`, + { + method: "POST", + headers: { + "X-API-KEY": apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify(versionCreate), }, - body: JSON.stringify(versionCreate), - }); + ); const data = await response.json(); if (!data.success) { - toast.error(`Failed to create version: ${data.error || 'Unknown error'}`); + toast.error( + `Failed to create version: ${data.error || "Unknown error"}`, + ); return; } - toast.success(`Configuration "${currentConfigName}" updated! New version created.`); + toast.success( + `Configuration "${currentConfigName}" updated! New version created.`, + ); } else { // Create new config const configCreate: ConfigCreate = { name: currentConfigName.trim(), description: `${provider} configuration with prompt`, config_blob: configBlob, - commit_message: commitMessage.trim() || 'Initial version', + commit_message: commitMessage.trim() || "Initial version", }; - const response = await fetch('/api/configs', { - method: 'POST', + const response = await fetch("/api/configs", { + method: "POST", headers: { - 'X-API-KEY': apiKey, - 'Content-Type': 'application/json', + "X-API-KEY": apiKey, + "Content-Type": "application/json", }, body: JSON.stringify(configCreate), }); @@ -298,11 +326,15 @@ function PromptEditorContent() { const data = await response.json(); if (!data.success || !data.data) { - toast.error(`Failed to create config: ${data.error || 'Unknown error'}`); + toast.error( + `Failed to create config: ${data.error || "Unknown error"}`, + ); return; } - toast.success(`Configuration "${currentConfigName}" created successfully!`); + toast.success( + `Configuration "${currentConfigName}" created successfully!`, + ); } // Invalidate config cache and refresh from shared hook @@ -311,10 +343,10 @@ function PromptEditorContent() { // Reset unsaved changes flag and commit message after successful save setHasUnsavedChanges(false); - setCommitMessage(''); + setCommitMessage(""); } catch (e) { - console.error('Failed to save config:', e); - toast.error('Failed to save configuration. Please try again.'); + console.error("Failed to save config:", e); + toast.error("Failed to save configuration. Please try again."); } finally { setIsSaving(false); } @@ -324,19 +356,19 @@ function PromptEditorContent() { const handleLoadConfigById = (configId: string) => { if (!configId) { // Reset to new config - setCurrentContent(''); + setCurrentContent(""); setCurrentConfigBlob(defaultConfig); - setProvider('openai'); + setProvider("openai"); setTemperature(0.7); - setSelectedConfigId(''); - setCurrentConfigName(''); - setCurrentConfigParentId(''); + setSelectedConfigId(""); + setCurrentConfigName(""); + setCurrentConfigParentId(""); setCurrentConfigVersion(0); setTools([]); return; } - const config = savedConfigs.find(c => c.id === configId); + const config = savedConfigs.find((c) => c.id === configId); if (!config) return; setCurrentContent(config.promptContent); @@ -362,11 +394,16 @@ function PromptEditorContent() { setTools(config.tools || []); }; - return ( -
+
- +
([]); 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 6eebcd04..8570a8e7 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 9e146861..a073dbf2 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 36b016ee..14111737 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 0bdfee5e..bd1ec602 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 5c34c1b6..2eece0db 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 74e51e74..48db9820 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 e63bbe30..b5d4a101 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 00000000..d8bdee1e --- /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 00000000..822cd718 --- /dev/null +++ b/app/lib/context/AuthContext.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { createContext, useContext, useState, useCallback } from 'react'; +import type { APIKey } from '@/app/keystore/page'; + +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(() => { + if (typeof window === 'undefined') return []; + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) return JSON.parse(stored); + } catch { /* ignore malformed data */ } + return []; + }); + + 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/useConfigs.ts b/app/lib/useConfigs.ts index 76180888..23851120 100644 --- a/app/lib/useConfigs.ts +++ b/app/lib/useConfigs.ts @@ -17,6 +17,7 @@ import { ConfigVersionListResponse, ConfigVersionResponse, } from './configTypes'; +import { useAuth } from '@/app/lib/context/AuthContext'; // ============ TYPES ============ @@ -64,21 +65,6 @@ const CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes - cache is considered stale // ============ HELPER FUNCTIONS ============ -// Get API key from localStorage -const getApiKey = (): string | null => { - if (typeof window === 'undefined') return null; - try { - const stored = localStorage.getItem('kaapi_api_keys'); - if (stored) { - const keys = JSON.parse(stored); - return keys.length > 0 ? keys[0].key : null; - } - } catch (e) { - console.error('Failed to get API key:', e); - } - return null; -}; - // Flatten config version for UI const flattenConfigVersion = ( config: ConfigPublic, @@ -206,6 +192,7 @@ export interface UseConfigsResult { } export function useConfigs(): UseConfigsResult { + const { activeKey } = useAuth(); const [configs, setConfigs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -219,7 +206,7 @@ export function useConfigs(): UseConfigsResult { // Prevent concurrent fetches if (fetchInProgress.current) return; - const apiKey = getApiKey(); + const apiKey = activeKey?.key ?? null; if (!apiKey) { setError('No API key found. Please add an API key in the Keystore.'); setIsLoading(false); @@ -357,9 +344,7 @@ export function useConfigs(): UseConfigsResult { setIsLoading(false); fetchInProgress.current = false; } - }, []); - - // Store refetch in ref for background validation + }, [activeKey]); refetchRef.current = fetchConfigs; useEffect(() => { diff --git a/app/settings/credentials/page.tsx b/app/settings/credentials/page.tsx index 5e1be100..5b9d18b5 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 20c85c7c..3937d167 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 47081f6e..b3e59461 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; From 98a90189ec3e845a68dfe78e1414abf4ead1b606 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:38:30 +0530 Subject: [PATCH 2/3] fix(*): update timeAgo function --- app/lib/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 33274983..5ceace0d 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -2,7 +2,12 @@ import { Credential, ProviderDef } from "@/app/lib/types/credentials"; import { formatDistanceToNow } from "date-fns"; 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( From 443d05b1603974cabad588d1a83c02c1574c79f5 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:29:06 +0530 Subject: [PATCH 3/3] fix(*): fix the hydration issue --- app/lib/context/AuthContext.tsx | 53 ++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/app/lib/context/AuthContext.tsx b/app/lib/context/AuthContext.tsx index 822cd718..d6aafc12 100644 --- a/app/lib/context/AuthContext.tsx +++ b/app/lib/context/AuthContext.tsx @@ -1,9 +1,15 @@ -'use client'; +"use client"; -import { createContext, useContext, useState, useCallback } from 'react'; -import type { APIKey } from '@/app/keystore/page'; +import { APIKey } from "@/app/keystore/page"; +import { + createContext, + useContext, + useState, + useCallback, + useEffect, +} from "react"; -const STORAGE_KEY = 'kaapi_api_keys'; +const STORAGE_KEY = "kaapi_api_keys"; interface AuthContextValue { apiKeys: APIKey[]; @@ -16,14 +22,19 @@ interface AuthContextValue { const AuthContext = createContext(null); export function AuthProvider({ children }: { children: React.ReactNode }) { - const [apiKeys, setApiKeys] = useState(() => { - if (typeof window === 'undefined') return []; + 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); - if (stored) return JSON.parse(stored); - } catch { /* ignore malformed data */ } - return []; - }); + // 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); @@ -34,12 +45,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }, []); - 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 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} ); @@ -47,6 +72,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { export function useAuth() { const ctx = useContext(AuthContext); - if (!ctx) throw new Error('useAuth must be used within AuthProvider'); + if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; }