| 9 | type ConnectionStatus = "idle" | "checking" | "ok" | "error"; |
| 10 | |
| 11 | export function ApiSettings() { |
| 12 | const { settings, updateSettings, resetSettings } = useChatStore(); |
| 13 | const [showKey, setShowKey] = useState(false); |
| 14 | const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>("idle"); |
| 15 | const [latencyMs, setLatencyMs] = useState<number | null>(null); |
| 16 | |
| 17 | async function checkConnection() { |
| 18 | setConnectionStatus("checking"); |
| 19 | setLatencyMs(null); |
| 20 | const start = Date.now(); |
| 21 | try { |
| 22 | const res = await fetch(`${settings.apiUrl}/health`, { signal: AbortSignal.timeout(5000) }); |
| 23 | const ms = Date.now() - start; |
| 24 | setLatencyMs(ms); |
| 25 | setConnectionStatus(res.ok ? "ok" : "error"); |
| 26 | } catch { |
| 27 | setConnectionStatus("error"); |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | const statusIcon = { |
| 32 | idle: null, |
| 33 | checking: <Loader2 className="w-4 h-4 animate-spin text-surface-400" />, |
| 34 | ok: <CheckCircle className="w-4 h-4 text-green-400" />, |
| 35 | error: <XCircle className="w-4 h-4 text-red-400" />, |
| 36 | }[connectionStatus]; |
| 37 | |
| 38 | const statusText = { |
| 39 | idle: "Not checked", |
| 40 | checking: "Checking...", |
| 41 | ok: latencyMs !== null ? `Connected — ${latencyMs}ms` : "Connected", |
| 42 | error: "Connection failed", |
| 43 | }[connectionStatus]; |
| 44 | |
| 45 | return ( |
| 46 | <div> |
| 47 | <SectionHeader title="API & Authentication" onReset={() => resetSettings("api")} /> |
| 48 | |
| 49 | <SettingRow |
| 50 | label="API key" |
| 51 | description="Your Anthropic API key. Stored locally and never sent to third parties." |
| 52 | stack |
| 53 | > |
| 54 | <div className="flex gap-2"> |
| 55 | <div className="relative flex-1"> |
| 56 | <input |
| 57 | type={showKey ? "text" : "password"} |
| 58 | value={settings.apiKey} |
| 59 | onChange={(e) => updateSettings({ apiKey: e.target.value })} |
| 60 | placeholder="sk-ant-..." |
| 61 | className={cn( |
| 62 | "w-full bg-surface-800 border border-surface-700 rounded-md px-3 py-1.5 pr-10 text-sm", |
| 63 | "text-surface-200 placeholder-surface-600 focus:outline-none focus:ring-1 focus:ring-brand-500 font-mono" |
| 64 | )} |
| 65 | /> |
| 66 | <button |
| 67 | onClick={() => setShowKey((v) => !v)} |
| 68 | className="absolute right-2 top-1/2 -translate-y-1/2 text-surface-500 hover:text-surface-300 transition-colors" |