From 1469e36f3feae3c07e34ce81acc4ae2b8aa2bf84 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Sat, 3 Jan 2026 14:24:32 +1000 Subject: [PATCH] refactor: simplify API keys page - just paste .env and save --- .../app/dashboard/settings/api-keys/page.tsx | 552 ++++-------------- 1 file changed, 128 insertions(+), 424 deletions(-) diff --git a/apps/web/src/app/dashboard/settings/api-keys/page.tsx b/apps/web/src/app/dashboard/settings/api-keys/page.tsx index d6f0dfd..d531b75 100644 --- a/apps/web/src/app/dashboard/settings/api-keys/page.tsx +++ b/apps/web/src/app/dashboard/settings/api-keys/page.tsx @@ -1,47 +1,22 @@ 'use client'; import type { AIProvider } from '@tpmjs/types/agent'; -import { SUPPORTED_PROVIDERS } from '@tpmjs/types/agent'; import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { AppHeader } from '~/components/AppHeader'; interface ApiKeyInfo { - provider: AIProvider; + provider: string; keyHint: string | null; createdAt: string; updatedAt: string; } -const PROVIDER_DISPLAY_NAMES: Record = { - OPENAI: 'OpenAI', - ANTHROPIC: 'Anthropic', - GOOGLE: 'Google', - GROQ: 'Groq', - MISTRAL: 'Mistral', -}; - -const PROVIDER_DESCRIPTIONS: Record = { - OPENAI: 'GPT-4o, GPT-4 Turbo, GPT-3.5 Turbo', - ANTHROPIC: 'Claude 3.5 Sonnet, Claude 3 Opus, Claude 3.5 Haiku', - GOOGLE: 'Gemini 2.0 Flash, Gemini 1.5 Pro, Gemini 1.5 Flash', - GROQ: 'Llama 3.3 70B, Llama 3.1 8B, Mixtral 8x7B', - MISTRAL: 'Mistral Large, Mistral Small', -}; - -const PROVIDER_LINKS: Record = { - OPENAI: { url: 'https://platform.openai.com/api-keys', label: 'platform.openai.com' }, - ANTHROPIC: { url: 'https://console.anthropic.com/settings/keys', label: 'console.anthropic.com' }, - GOOGLE: { url: 'https://aistudio.google.com/apikey', label: 'aistudio.google.com' }, - GROQ: { url: 'https://console.groq.com/keys', label: 'console.groq.com' }, - MISTRAL: { url: 'https://console.mistral.ai/api-keys', label: 'console.mistral.ai' }, -}; - -// Map common env var names to our provider enum -const ENV_VAR_TO_PROVIDER: Record = { +// Map env var names to provider codes +const ENV_VAR_MAP: Record = { OPENAI_API_KEY: 'OPENAI', ANTHROPIC_API_KEY: 'ANTHROPIC', GOOGLE_API_KEY: 'GOOGLE', @@ -55,35 +30,10 @@ export default function ApiKeysPage(): React.ReactElement { const [apiKeys, setApiKeys] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - - // Per-provider input state - const [keyInputs, setKeyInputs] = useState>({ - OPENAI: '', - ANTHROPIC: '', - GOOGLE: '', - GROQ: '', - MISTRAL: '', - }); - const [savingProviders, setSavingProviders] = useState>(new Set()); - const [savedProviders, setSavedProviders] = useState>(new Set()); - const [deletingProvider, setDeletingProvider] = useState(null); - - // .env paste state const [envText, setEnvText] = useState(''); - const [parsedKeys, setParsedKeys] = useState<{ provider: AIProvider; key: string }[]>([]); - const [importStatus, setImportStatus] = useState<'idle' | 'importing' | 'done'>('idle'); - const [importResults, setImportResults] = useState< - { provider: AIProvider; success: boolean; error?: string }[] - >([]); - - // Auto-save debounce refs - const saveTimeoutRefs = useRef>({ - OPENAI: null, - ANTHROPIC: null, - GOOGLE: null, - GROQ: null, - MISTRAL: null, - }); + const [isSaving, setIsSaving] = useState(false); + const [saveResult, setSaveResult] = useState<{ success: number; failed: number } | null>(null); + const [deletingKey, setDeletingKey] = useState(null); const fetchApiKeys = useCallback(async () => { try { @@ -111,209 +61,90 @@ export default function ApiKeysPage(): React.ReactElement { fetchApiKeys(); }, [fetchApiKeys]); - // Parse .env content - // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Simple env parsing with multiple conditionals - const parseEnvContent = useCallback((text: string) => { - const lines = text.split('\n'); - const keys: { provider: AIProvider; key: string }[] = []; + const parseAndSave = useCallback(async () => { + const lines = envText.split('\n'); + const keysToSave: { provider: AIProvider; apiKey: string }[] = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; - // Handle KEY=VALUE format (with optional quotes) - const match = trimmed.match(/^([A-Z_]+)\s*=\s*["']?([^"'\s]+)["']?$/); + const match = trimmed.match(/^([A-Z_]+)\s*=\s*["']?([^"'\n]+)["']?$/); if (match) { - const envVar = match[1]; - const value = match[2]; + const [, envVar, value] = match; if (!envVar || !value) continue; - const provider = ENV_VAR_TO_PROVIDER[envVar]; + const provider = ENV_VAR_MAP[envVar]; if (provider) { - // Check if this provider is already in our list - const existingIndex = keys.findIndex((k) => k.provider === provider); - if (existingIndex >= 0) { - keys[existingIndex] = { provider, key: value }; - } else { - keys.push({ provider, key: value }); - } + keysToSave.push({ provider, apiKey: value.trim() }); } } } - return keys; - }, []); - - // Update parsed keys when env text changes - useEffect(() => { - const keys = parseEnvContent(envText); - setParsedKeys(keys); - }, [envText, parseEnvContent]); - - // Save a single key - const saveKey = useCallback(async (provider: AIProvider, apiKey: string) => { - setSavingProviders((prev) => new Set(prev).add(provider)); - setSavedProviders((prev) => { - const next = new Set(prev); - next.delete(provider); - return next; - }); - - try { - const response = await fetch('/api/user/api-keys', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ provider, apiKey }), - }); - - const result = await response.json(); - - if (result.success) { - setApiKeys((prev) => { - const existing = prev.findIndex((k) => k.provider === provider); - if (existing >= 0) { - const updated = [...prev]; - updated[existing] = result.data; - return updated; - } - return [...prev, result.data]; - }); - setSavedProviders((prev) => new Set(prev).add(provider)); - // Clear input after successful save - setKeyInputs((prev) => ({ ...prev, [provider]: '' })); - // Clear saved indicator after 2 seconds - setTimeout(() => { - setSavedProviders((prev) => { - const next = new Set(prev); - next.delete(provider); - return next; - }); - }, 2000); - return { success: true }; - } - return { success: false, error: result.error }; - } catch (err) { - console.error('Failed to save API key:', err); - return { success: false, error: 'Network error' }; - } finally { - setSavingProviders((prev) => { - const next = new Set(prev); - next.delete(provider); - return next; - }); - } - }, []); - - // Handle input change with debounced auto-save - const handleKeyInputChange = useCallback( - (provider: AIProvider, value: string) => { - setKeyInputs((prev) => ({ ...prev, [provider]: value })); - - // Clear existing timeout - const existingTimeout = saveTimeoutRefs.current[provider]; - if (existingTimeout) { - clearTimeout(existingTimeout); - } - - // Set new timeout for auto-save (1.5 seconds after typing stops) - if (value.trim()) { - saveTimeoutRefs.current[provider] = setTimeout(() => { - saveKey(provider, value.trim()); - }, 1500); - } - }, - [saveKey] - ); - - // Handle blur - save immediately - const handleKeyInputBlur = useCallback( - (provider: AIProvider) => { - const value = keyInputs[provider].trim(); - if (value) { - // Clear any pending timeout - const pendingTimeout = saveTimeoutRefs.current[provider]; - if (pendingTimeout) { - clearTimeout(pendingTimeout); - saveTimeoutRefs.current[provider] = null; - } - saveKey(provider, value); - } - }, - [keyInputs, saveKey] - ); - - // Import all parsed keys - const handleImportAll = useCallback(async () => { - if (parsedKeys.length === 0) return; - - setImportStatus('importing'); - setImportResults([]); - - const results: { provider: AIProvider; success: boolean; error?: string }[] = []; - - for (const { provider, key } of parsedKeys) { - const result = await saveKey(provider, key); - results.push({ - provider, - success: result.success, - error: result.success ? undefined : result.error, - }); - } - - setImportResults(results); - setImportStatus('done'); - setEnvText(''); - setParsedKeys([]); - - // Clear results after 3 seconds - setTimeout(() => { - setImportResults([]); - setImportStatus('idle'); - }, 3000); - }, [parsedKeys, saveKey]); - - const handleDeleteKey = useCallback(async (provider: AIProvider) => { - if ( - !confirm(`Are you sure you want to delete your ${PROVIDER_DISPLAY_NAMES[provider]} API key?`) - ) { + if (keysToSave.length === 0) { + setSaveResult({ success: 0, failed: 0 }); return; } - setDeletingProvider(provider); - try { - const response = await fetch(`/api/user/api-keys/${provider}`, { - method: 'DELETE', - }); + setIsSaving(true); + let success = 0; + let failed = 0; - const result = await response.json(); - - if (result.success) { - setApiKeys((prev) => prev.filter((k) => k.provider !== provider)); - } else { - throw new Error(result.error || 'Failed to delete API key'); + for (const { provider, apiKey } of keysToSave) { + try { + const response = await fetch('/api/user/api-keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ provider, apiKey }), + }); + const result = await response.json(); + if (result.success) { + success++; + } else { + failed++; + } + } catch { + failed++; } - } catch (err) { - console.error('Failed to delete API key:', err); - alert(err instanceof Error ? err.message : 'Failed to delete API key'); - } finally { - setDeletingProvider(null); } - }, []); - const hasKey = (provider: AIProvider) => apiKeys.some((k) => k.provider === provider); - const getKeyInfo = (provider: AIProvider) => apiKeys.find((k) => k.provider === provider); + setSaveResult({ success, failed }); + setIsSaving(false); + setEnvText(''); + fetchApiKeys(); + + setTimeout(() => setSaveResult(null), 3000); + }, [envText, fetchApiKeys]); + + const handleDelete = useCallback( + async (provider: string) => { + if (!confirm(`Delete this API key?`)) return; + + setDeletingKey(provider); + try { + const response = await fetch(`/api/user/api-keys/${provider}`, { + method: 'DELETE', + }); + const result = await response.json(); + if (result.success) { + setApiKeys((prev) => prev.filter((k) => k.provider !== provider)); + } + } catch (err) { + console.error('Failed to delete:', err); + } finally { + setDeletingKey(null); + } + }, + [] + ); if (isLoading) { return (
-
-
-
-
- {[1, 2, 3].map((i) => ( -
- ))} -
+
+
+
+
@@ -324,13 +155,9 @@ export default function ApiKeysPage(): React.ReactElement { return (
-
-
- -

Error

-

{error}

- -
+
+

{error}

+
); @@ -339,207 +166,84 @@ export default function ApiKeysPage(): React.ReactElement { return (
-
- {/* Header */} -
- +
+
+ -
-

API Keys

-

- Manage your AI provider API keys for agents -

-
+

API Keys

- {/* Quick Import Section */} -
-

Quick Import

-

- Paste your .env file or environment variables below to import multiple keys at once. -

- + {/* Paste .env */} +