fix: add API key input field to AI provider settings form
When selecting Anthropic or OpenAI as provider, the form now shows an inline API key input field that creates or replaces the key via the existing CRUD API. Includes key hint display and status indicator. Resolves AIIA-49 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -10,7 +10,9 @@ interface AISettings {
|
||||
}
|
||||
|
||||
interface ApiKeyInfo {
|
||||
id: string;
|
||||
provider: string;
|
||||
keyHint: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -25,6 +27,8 @@ export default function AISettingsForm() {
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKeyInfo[]>([]);
|
||||
const [apiKeyInput, setApiKeyInput] = useState('');
|
||||
const [savingKey, setSavingKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/settings/ai')
|
||||
@@ -107,12 +111,77 @@ export default function AISettingsForm() {
|
||||
const activeKey = apiKeys.find(
|
||||
(k) => k.provider === settings.provider && k.isActive,
|
||||
);
|
||||
const providerLabel = settings.provider === 'anthropic' ? 'Anthropic' : 'OpenAI';
|
||||
|
||||
async function handleSaveApiKey() {
|
||||
if (!apiKeyInput || apiKeyInput.length < 8) {
|
||||
setMessage({ type: 'error', text: 'API-Schlüssel muss mindestens 8 Zeichen lang sein.' });
|
||||
return;
|
||||
}
|
||||
setSavingKey(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
let res: Response;
|
||||
if (activeKey) {
|
||||
res = await fetch(`/api/settings/api-keys/${activeKey.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiKey: apiKeyInput }),
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/settings/api-keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: settings.provider, apiKey: apiKeyInput }),
|
||||
});
|
||||
}
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
setApiKeys((prev) => {
|
||||
if (activeKey) {
|
||||
return prev.map((k) => (k.id === activeKey.id ? { ...k, ...updated } : k));
|
||||
}
|
||||
return [...prev, updated];
|
||||
});
|
||||
setApiKeyInput('');
|
||||
setMessage({ type: 'success', text: `API-Schlüssel für ${providerLabel} gespeichert.` });
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setMessage({ type: 'error', text: data.error ?? 'Fehler beim Speichern des Schlüssels.' });
|
||||
}
|
||||
} catch {
|
||||
setMessage({ type: 'error', text: 'Netzwerkfehler beim Speichern des Schlüssels.' });
|
||||
} finally {
|
||||
setSavingKey(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm ${activeKey ? 'border-green-200 bg-green-50 text-green-700' : 'border-amber-200 bg-amber-50 text-amber-700'}`}>
|
||||
<span className={`inline-block w-2 h-2 rounded-full shrink-0 ${activeKey ? 'bg-green-500' : 'bg-amber-500'}`} />
|
||||
{activeKey
|
||||
? `API-Schlüssel für ${settings.provider === 'anthropic' ? 'Anthropic' : 'OpenAI'} ist konfiguriert.`
|
||||
: `Kein API-Schlüssel für ${settings.provider === 'anthropic' ? 'Anthropic' : 'OpenAI'} hinterlegt. Bitte unten unter „API-Schlüssel" einen Schlüssel hinzufügen.`}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm text-muted mb-1">API-Schlüssel</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={apiKeyInput}
|
||||
onChange={(e) => setApiKeyInput(e.target.value)}
|
||||
placeholder={activeKey ? `Aktuell: ${activeKey.keyHint} — neuen Schlüssel eingeben zum Ersetzen` : `${providerLabel} API-Schlüssel eingeben`}
|
||||
className="flex-1 rounded-lg border border-card-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted/50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveApiKey}
|
||||
disabled={savingKey || !apiKeyInput}
|
||||
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{savingKey ? '...' : activeKey ? 'Ersetzen' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm ${activeKey ? 'border-green-200 bg-green-50 text-green-700' : 'border-amber-200 bg-amber-50 text-amber-700'}`}>
|
||||
<span className={`inline-block w-2 h-2 rounded-full shrink-0 ${activeKey ? 'bg-green-500' : 'bg-amber-500'}`} />
|
||||
{activeKey
|
||||
? `API-Schlüssel für ${providerLabel} ist konfiguriert (${activeKey.keyHint}).`
|
||||
: `Kein API-Schlüssel für ${providerLabel} hinterlegt.`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user