diff --git a/messages/de.json b/messages/de.json index 86a741a62..faa6af6bf 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1069,6 +1069,10 @@ "savedShort": "(gespeichert)", "saveCta": "Speichern", "testCta": "Verbindung testen", + "removeKeyCta": "Gespeicherten Key entfernen", + "keyRemoved": "Gespeicherter Key entfernt", + "removeKeyConfirmTitle": "Gespeicherten API-Key entfernen?", + "removeKeyConfirmBody": "Insights für diesen Provider fallen auf den Admin-/Codex-Provider zurück, bis du einen neuen Key speicherst.", "saved": "Gespeichert", "saveFailed": "Speichern fehlgeschlagen", "testFailed": "Test fehlgeschlagen", diff --git a/messages/en.json b/messages/en.json index f18307680..f542a39f2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1069,6 +1069,10 @@ "savedShort": "(saved)", "saveCta": "Save", "testCta": "Test connection", + "removeKeyCta": "Remove saved key", + "keyRemoved": "Saved key removed", + "removeKeyConfirmTitle": "Remove saved API key?", + "removeKeyConfirmBody": "Insights for this provider will fall back to the admin/Codex provider until you save a new key.", "saved": "Saved", "saveFailed": "Save failed", "testFailed": "Test failed", diff --git a/src/components/settings/ai-section.tsx b/src/components/settings/ai-section.tsx index 6981433ad..043e4afb5 100644 --- a/src/components/settings/ai-section.tsx +++ b/src/components/settings/ai-section.tsx @@ -4,6 +4,17 @@ import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, RefreshCw, Save, Sparkles, Trash2 } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -639,6 +650,48 @@ function UserAIProviderSubsection() { }, }); + // v1.5 phase-5 — "Remove saved key" closes the v1.4.6 deferred polish. + // Sends an explicit `null` for whichever provider key is stored so the + // PATCH route clears it (the route already handles the empty-string / + // null case). Provider/model/baseUrl are left alone so the user can + // keep their selection while just rotating the secret. + const removeKeyMutation = useMutation({ + mutationFn: async (target: "openaiKey" | "anthropicKey" | "localKey") => { + const res = await fetch("/api/user/ai-provider", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ [target]: null }), + }); + const json = await res.json(); + if (!res.ok) throw new Error(json.error || t("settings.ai.saveFailed")); + }, + onSuccess: () => { + setSaveMsg(t("settings.ai.keyRemoved")); + setSaveOk(true); + queryClient.invalidateQueries({ queryKey: ["user", "ai-provider"] }); + queryClient.invalidateQueries({ queryKey: ["insights"] }); + }, + onError: (e) => { + setSaveMsg( + e instanceof Error ? e.message : t("settings.ai.errorGeneric"), + ); + setSaveOk(false); + }, + }); + + /** + * Maps the currently selected provider to its stored-key flag and the + * PATCH-body field name. Returns null when there is no saved key for + * the active provider — the "Remove saved key" button hides itself. + */ + const removableKey: "openaiKey" | "anthropicKey" | "localKey" | null = (() => { + if (provider === "OPENAI" && data?.hasOpenaiKey) return "openaiKey"; + if (provider === "ANTHROPIC" && data?.hasAnthropicKey) + return "anthropicKey"; + if (provider === "LOCAL" && data?.hasLocalKey) return "localKey"; + return null; + })(); + async function runTest() { setTesting(true); setTestMsg(null); @@ -862,6 +915,44 @@ function UserAIProviderSubsection() { )} {t("settings.ai.testCta")} + {removableKey && ( + + + + + + + + {t("settings.ai.removeKeyConfirmTitle")} + + + {t("settings.ai.removeKeyConfirmBody")} + + + + {t("common.cancel")} + removeKeyMutation.mutate(removableKey)} + > + {t("settings.ai.removeKeyCta")} + + + + + )} {saveMsg && (