import { useState, useEffect, useCallback } from "react"; import { agentNativePath } from "../api-path.js"; interface EnvKeyStatus { key: string; label: string; required: boolean; configured: boolean; } interface ApiKeySettingsProps { /** Path to the settings page (used for linking). Default: "/settings" */ settingsPath?: string; } /** * Reusable component that shows the status of configured API keys * and lets users enter missing ones. Fetches from /_agent-native/env-status * and saves via POST /_agent-native/env-vars. */ export function ApiKeySettings({ settingsPath: _settingsPath = "/settings", }: ApiKeySettingsProps) { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [values, setValues] = useState>({}); const [saving, setSaving] = useState(false); const [saveResult, setSaveResult] = useState<{ ok: boolean; message: string; } | null>(null); const fetchStatus = useCallback(async () => { try { setLoading(true); setError(null); const res = await fetch(agentNativePath("/_agent-native/env-status")); if (!res.ok) { throw new Error(`Failed to fetch env status: ${res.status}`); } const data: EnvKeyStatus[] = await res.json(); setKeys(data); } catch (err) { setError( err instanceof Error ? err.message : "Failed to load API key status", ); } finally { setLoading(false); } }, []); useEffect(() => { fetchStatus(); }, [fetchStatus]); const handleValueChange = (key: string, value: string) => { setValues((prev) => ({ ...prev, [key]: value })); setSaveResult(null); }; const handleSave = async () => { const vars = Object.entries(values) .filter(([, v]) => v.trim() !== "") .map(([key, value]) => ({ key, value: value.trim() })); if (vars.length === 0) return; setSaving(true); setSaveResult(null); try { const res = await fetch(agentNativePath("/_agent-native/env-vars"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vars }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `Save failed: ${res.status}`); } const data = await res.json(); setSaveResult({ ok: true, message: `Saved ${data.saved?.length ?? 0} key(s)`, }); setValues({}); // Refresh status await fetchStatus(); } catch (err) { setSaveResult({ ok: false, message: err instanceof Error ? err.message : "Save failed", }); } finally { setSaving(false); } }; const pendingCount = Object.values(values).filter( (v) => v.trim() !== "", ).length; if (loading) { return (

Loading API key status...

); } if (error) { return (

{error}

); } if (keys.length === 0) { return null; } const configuredKeys = keys.filter((k) => k.configured); const unconfiguredKeys = keys.filter((k) => !k.configured); return (

API Keys

{configuredKeys.length} of {keys.length} configured

{keys.map((k) => (
{k.configured ? ( ) : ( )} {k.label} {k.required && ( required )} {k.key}
{!k.configured && ( handleValueChange(k.key, e.target.value)} style={styles.input} autoComplete="off" /> )}
))}
{unconfiguredKeys.length > 0 && (
)} {saveResult && (

{saveResult.message}

)}
); } const styles: Record = { container: { padding: "20px", maxWidth: "600px", }, heading: { fontSize: "16px", fontWeight: 600, margin: "0 0 4px 0", }, subtitle: { fontSize: "13px", opacity: 0.6, margin: "0 0 16px 0", }, loadingText: { fontSize: "13px", opacity: 0.5, }, errorText: { fontSize: "13px", color: "#ef4444", }, successText: { fontSize: "13px", color: "#22c55e", marginTop: "8px", }, retryButton: { marginTop: "8px", padding: "6px 12px", fontSize: "13px", border: "1px solid hsl(var(--border))", borderRadius: "6px", background: "transparent", color: "inherit", cursor: "pointer", }, keyList: { display: "flex", flexDirection: "column", gap: "8px", }, keyCard: { padding: "10px 12px", borderRadius: "8px", border: "1px solid hsl(var(--border))", background: "hsl(var(--card))", }, keyHeader: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: "8px", }, keyLabel: { fontSize: "13px", fontWeight: 500, display: "flex", alignItems: "center", gap: "6px", }, keyName: { fontSize: "11px", opacity: 0.4, fontFamily: "monospace", }, checkmark: { color: "#22c55e", fontSize: "14px", }, unconfiguredDot: { color: "hsl(var(--muted-foreground) / 0.55)", fontSize: "8px", }, requiredBadge: { fontSize: "10px", padding: "1px 5px", borderRadius: "4px", background: "rgba(239,68,68,0.15)", color: "#ef4444", fontWeight: 500, }, input: { width: "100%", marginTop: "8px", padding: "7px 10px", fontSize: "13px", borderRadius: "6px", border: "1px solid hsl(var(--input))", background: "hsl(var(--background))", color: "inherit", outline: "none", boxSizing: "border-box" as const, }, actions: { marginTop: "12px", display: "flex", justifyContent: "flex-end", }, saveButton: { padding: "7px 16px", fontSize: "13px", fontWeight: 500, borderRadius: "6px", border: "none", background: "hsl(var(--primary))", color: "hsl(var(--primary-foreground))", cursor: "pointer", }, };