mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(settings/ai): drop unreleased OpenAI presets + i18n the AI section
P15: drop `gpt-5` (unreleased) and `o3-mini` from `MODEL_PRESETS["OPENAI"]`.
gpt-5 would surface as model_not_found from OpenAI; o3-mini uses the
o-series param contract (max_completion_tokens, no temperature override)
which the current OpenAIClient body shape doesn't speak — so picking
either preset just produced a confusing 400. Keep gpt-4o, gpt-4o-mini,
gpt-4-turbo. Power users can still type any model via the Custom… option.
P16: pipe ~30 hardcoded German strings in ai-section.tsx through
`t("settings.ai.…")` and add the matching keys to both messages/en.json
(canonical English) and messages/de.json. Behaviour unchanged — the
Connect/Disconnect/Save/Test/Provider/Model labels and the connect-help
copy now respect the user's locale toggle. The provider-test flow keeps
its own DE strings localized too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dcc697c33d
commit
89b5b80493
3 files changed
+151
-47
No files matched your search
@@ -1025,6 +1025,46 @@
|
||||
"regenerateSuccess": "Reports wurden neu generiert",
|
||||
"regenerateRateLimit": "Bitte warte — maximal 2 Analysen pro Stunde.",
|
||||
"lastGeneratedAt": "Zuletzt generiert",
|
||||
"ai": {
|
||||
"title": "KI-Insights",
|
||||
"description": "Anbieter, Modell, Key.",
|
||||
"chatgptConnectedBadge": "ChatGPT verbunden",
|
||||
"adminAiActiveBadge": "Admin-KI aktiv",
|
||||
"connectionExpiredBadge": "Verbindung abgelaufen",
|
||||
"chatgptConnectedHeading": "ChatGPT verbunden",
|
||||
"chatgptConnectedBody": "Insights werden über dein ChatGPT-Abo generiert — keine zusätzlichen Kosten.",
|
||||
"connectedSince": "Verbunden seit {when}.",
|
||||
"disconnect": "Trennen",
|
||||
"connectChatgptHeading": "Mit ChatGPT verbinden",
|
||||
"connectChatgptBody": "Verbinde dein ChatGPT Pro/Max-Konto um KI-gestützte Gesundheitsanalysen basierend auf aktuellen medizinischen Leitlinien zu erhalten. Keine zusätzlichen API-Kosten.",
|
||||
"connectChatgptCta": "Mit ChatGPT verbinden",
|
||||
"oauthNotConfigured": "ChatGPT-OAuth ist auf dieser Instanz nicht konfiguriert — nutze stattdessen einen eigenen API-Key unten.",
|
||||
"adminFallback": "Alternativ nutzt HealthLog den vom Administrator konfigurierten KI-Anbieter.",
|
||||
"personalProviderHeading": "KI-Provider (persönlich)",
|
||||
"personalProviderBody": "Eigener KI-Anbieter überschreibt die Admin-Einstellung. Leer lassen für Standard. Für ChatGPT-Pro/Max-OAuth nutze den Verbinden-Button oben.",
|
||||
"providerLabel": "Provider",
|
||||
"providerOptionDefault": "— Standard (Admin/Codex) —",
|
||||
"providerOptionOpenai": "OpenAI (API-Key)",
|
||||
"providerOptionAnthropic": "Anthropic (Claude)",
|
||||
"providerOptionLocal": "Lokal (OpenAI-kompatibel)",
|
||||
"modelLabel": "Modell",
|
||||
"modelOptionDefault": "— Standard —",
|
||||
"modelOptionCustom": "Eigenes…",
|
||||
"customModelLabel": "Eigener Modellname",
|
||||
"openaiKeyLabel": "OpenAI API-Key",
|
||||
"openaiKeyHelp": "Eigener Key — keine Verbindung zu deinem ChatGPT-Abo. Für das Abo nutze den Verbinden-Button oben.",
|
||||
"anthropicKeyLabel": "Anthropic API-Key",
|
||||
"baseUrlLabel": "Base URL",
|
||||
"localKeyLabel": "API-Key (optional)",
|
||||
"savedPreview": "(gespeichert {preview})",
|
||||
"savedShort": "(gespeichert)",
|
||||
"saveCta": "Speichern",
|
||||
"testCta": "Verbindung testen",
|
||||
"saved": "Gespeichert",
|
||||
"saveFailed": "Speichern fehlgeschlagen",
|
||||
"testFailed": "Test fehlgeschlagen",
|
||||
"errorGeneric": "Fehler"
|
||||
},
|
||||
"withings": "Withings",
|
||||
"withingsDescription": "Verbinde deine Withings-Waage und Blutdruckmessgeräte.",
|
||||
"withingsCredentials": "API-Zugangsdaten",
|
||||
|
||||
@@ -1025,6 +1025,46 @@
|
||||
"regenerateSuccess": "Reports regenerated successfully",
|
||||
"regenerateRateLimit": "Please wait — max 2 analyses per hour.",
|
||||
"lastGeneratedAt": "Last generated",
|
||||
"ai": {
|
||||
"title": "AI Insights",
|
||||
"description": "Provider, model, key.",
|
||||
"chatgptConnectedBadge": "ChatGPT connected",
|
||||
"adminAiActiveBadge": "Admin AI active",
|
||||
"connectionExpiredBadge": "Connection expired",
|
||||
"chatgptConnectedHeading": "ChatGPT connected",
|
||||
"chatgptConnectedBody": "Insights are generated through your ChatGPT subscription — no extra cost.",
|
||||
"connectedSince": "Connected since {when}.",
|
||||
"disconnect": "Disconnect",
|
||||
"connectChatgptHeading": "Connect with ChatGPT",
|
||||
"connectChatgptBody": "Link your ChatGPT Pro/Max account to get AI-powered health analyses based on current medical guidelines. No additional API costs.",
|
||||
"connectChatgptCta": "Connect with ChatGPT",
|
||||
"oauthNotConfigured": "ChatGPT OAuth is not configured on this instance — use your own API key below instead.",
|
||||
"adminFallback": "Alternatively, HealthLog will use the AI provider configured by the administrator.",
|
||||
"personalProviderHeading": "AI provider (personal)",
|
||||
"personalProviderBody": "Your own AI provider overrides the admin setting. Leave blank for the default. For ChatGPT Pro/Max OAuth use the connect button above.",
|
||||
"providerLabel": "Provider",
|
||||
"providerOptionDefault": "— Default (Admin/Codex) —",
|
||||
"providerOptionOpenai": "OpenAI (API key)",
|
||||
"providerOptionAnthropic": "Anthropic (Claude)",
|
||||
"providerOptionLocal": "Local (OpenAI-compatible)",
|
||||
"modelLabel": "Model",
|
||||
"modelOptionDefault": "— Default —",
|
||||
"modelOptionCustom": "Custom…",
|
||||
"customModelLabel": "Custom model name",
|
||||
"openaiKeyLabel": "OpenAI API key",
|
||||
"openaiKeyHelp": "Your own key — not connected to your ChatGPT subscription. For the subscription use the connect button above.",
|
||||
"anthropicKeyLabel": "Anthropic API key",
|
||||
"baseUrlLabel": "Base URL",
|
||||
"localKeyLabel": "API key (optional)",
|
||||
"savedPreview": "(saved {preview})",
|
||||
"savedShort": "(saved)",
|
||||
"saveCta": "Save",
|
||||
"testCta": "Test connection",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Save failed",
|
||||
"testFailed": "Test failed",
|
||||
"errorGeneric": "Error"
|
||||
},
|
||||
"withings": "Withings",
|
||||
"withingsDescription": "Connect your Withings scale and blood pressure monitors.",
|
||||
"withingsCredentials": "API Credentials",
|
||||
|
||||
@@ -46,7 +46,11 @@ interface UserAIProvider {
|
||||
* the underlying API speaks (e.g. preview models, fine-tunes).
|
||||
*/
|
||||
const MODEL_PRESETS: Record<string, ReadonlyArray<string>> = {
|
||||
OPENAI: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-5", "o3-mini"],
|
||||
// P15: dropped `gpt-5` (not a released model — would surface as
|
||||
// model_not_found) and `o3-mini` (uses the o-series param contract:
|
||||
// `max_completion_tokens` instead of `max_tokens`, no `temperature`
|
||||
// override — would 400 with the current OpenAIClient body shape).
|
||||
OPENAI: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
|
||||
ANTHROPIC: [
|
||||
"claude-sonnet-4-6",
|
||||
"claude-opus-4-7",
|
||||
@@ -234,17 +238,17 @@ function InsightsSettingsCard({
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{settings?.codexStatus === "connected" && (
|
||||
<Badge className="border-dracula-green/30 bg-dracula-green/15 text-dracula-green">
|
||||
ChatGPT verbunden
|
||||
{t("settings.ai.chatgptConnectedBadge")}
|
||||
</Badge>
|
||||
)}
|
||||
{settings?.codexStatus !== "connected" && settings?.hasAdminKey && (
|
||||
<Badge className="border-dracula-purple/30 bg-dracula-purple/15 text-dracula-purple">
|
||||
Admin-KI aktiv
|
||||
{t("settings.ai.adminAiActiveBadge")}
|
||||
</Badge>
|
||||
)}
|
||||
{settings?.codexStatus === "expired" && (
|
||||
<Badge className="border-dracula-orange/30 bg-dracula-orange/15 text-dracula-orange">
|
||||
Verbindung abgelaufen
|
||||
{t("settings.ai.connectionExpiredBadge")}
|
||||
</Badge>
|
||||
)}
|
||||
{settings?.lastInsightAt && (
|
||||
@@ -264,15 +268,17 @@ function InsightsSettingsCard({
|
||||
{settings?.codexStatus === "connected" ? (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">ChatGPT verbunden</p>
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.ai.chatgptConnectedHeading")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Insights werden über dein ChatGPT-Abo generiert — keine
|
||||
zusätzlichen Kosten.
|
||||
{t("settings.ai.chatgptConnectedBody")}
|
||||
{settings.codexConnectedAt && (
|
||||
<>
|
||||
{" "}
|
||||
Verbunden seit {formatDateTime(settings.codexConnectedAt)}
|
||||
.
|
||||
{t("settings.ai.connectedSince", {
|
||||
when: formatDateTime(settings.codexConnectedAt),
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
@@ -292,17 +298,17 @@ function InsightsSettingsCard({
|
||||
) : (
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
Trennen
|
||||
{t("settings.ai.disconnect")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Mit ChatGPT verbinden</p>
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.ai.connectChatgptHeading")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Verbinde dein ChatGPT Pro/Max-Konto um KI-gestützte
|
||||
Gesundheitsanalysen basierend auf aktuellen medizinischen
|
||||
Leitlinien zu erhalten. Keine zusätzlichen API-Kosten.
|
||||
{t("settings.ai.connectChatgptBody")}
|
||||
</p>
|
||||
</div>
|
||||
{settings?.codexOauthConfigured ? (
|
||||
@@ -312,18 +318,16 @@ function InsightsSettingsCard({
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
Mit ChatGPT verbinden
|
||||
{t("settings.ai.connectChatgptCta")}
|
||||
</Button>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs italic">
|
||||
ChatGPT-OAuth ist auf dieser Instanz nicht konfiguriert —
|
||||
nutze stattdessen einen eigenen API-Key unten.
|
||||
{t("settings.ai.oauthNotConfigured")}
|
||||
</p>
|
||||
)}
|
||||
{settings?.hasAdminKey && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Alternativ nutzt HealthLog den vom Administrator
|
||||
konfigurierten KI-Anbieter.
|
||||
{t("settings.ai.adminFallback")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -388,6 +392,7 @@ function InsightsSettingsCard({
|
||||
}
|
||||
|
||||
function UserAIProviderSubsection() {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const [provider, setProvider] = useState<string>("");
|
||||
const [modelChoice, setModelChoice] = useState<string>("");
|
||||
@@ -470,10 +475,10 @@ function UserAIProviderSubsection() {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error || "Save failed");
|
||||
if (!res.ok) throw new Error(json.error || t("settings.ai.saveFailed"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
setSaveMsg("Gespeichert");
|
||||
setSaveMsg(t("settings.ai.saved"));
|
||||
setSaveOk(true);
|
||||
setAnthropicKey("");
|
||||
setLocalKey("");
|
||||
@@ -482,7 +487,7 @@ function UserAIProviderSubsection() {
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
setSaveMsg(e instanceof Error ? e.message : "Fehler");
|
||||
setSaveMsg(e instanceof Error ? e.message : t("settings.ai.errorGeneric"));
|
||||
setSaveOk(false);
|
||||
},
|
||||
});
|
||||
@@ -517,7 +522,9 @@ function UserAIProviderSubsection() {
|
||||
);
|
||||
setTestOk(true);
|
||||
} catch (e) {
|
||||
setTestMsg(e instanceof Error ? e.message : "Test fehlgeschlagen");
|
||||
setTestMsg(
|
||||
e instanceof Error ? e.message : t("settings.ai.testFailed"),
|
||||
);
|
||||
setTestOk(false);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
@@ -529,32 +536,36 @@ function UserAIProviderSubsection() {
|
||||
return (
|
||||
<div className="bg-muted/50 mt-2 rounded-lg p-4">
|
||||
<div className="mb-3">
|
||||
<p className="text-sm font-medium">KI-Provider (persönlich)</p>
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.ai.personalProviderHeading")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Eigener KI-Anbieter überschreibt die Admin-Einstellung. Leer lassen
|
||||
für Standard. Für ChatGPT-Pro/Max-OAuth nutze den Verbinden-Button
|
||||
oben.
|
||||
{t("settings.ai.personalProviderBody")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor="ai-provider-select">Provider</Label>
|
||||
<Label htmlFor="ai-provider-select">
|
||||
{t("settings.ai.providerLabel")}
|
||||
</Label>
|
||||
<select
|
||||
id="ai-provider-select"
|
||||
value={provider}
|
||||
onChange={(e) => handleProviderChange(e.target.value)}
|
||||
className="bg-background border-input mt-1 h-9 w-full rounded-md border px-2 text-sm"
|
||||
>
|
||||
<option value="">— Standard (Admin/Codex) —</option>
|
||||
<option value="OPENAI">OpenAI (API-Key)</option>
|
||||
<option value="ANTHROPIC">Anthropic (Claude)</option>
|
||||
<option value="LOCAL">Lokal (OpenAI-kompatibel)</option>
|
||||
<option value="">{t("settings.ai.providerOptionDefault")}</option>
|
||||
<option value="OPENAI">{t("settings.ai.providerOptionOpenai")}</option>
|
||||
<option value="ANTHROPIC">
|
||||
{t("settings.ai.providerOptionAnthropic")}
|
||||
</option>
|
||||
<option value="LOCAL">{t("settings.ai.providerOptionLocal")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="ai-model-select">Modell</Label>
|
||||
<Label htmlFor="ai-model-select">{t("settings.ai.modelLabel")}</Label>
|
||||
<select
|
||||
id="ai-model-select"
|
||||
value={modelChoice}
|
||||
@@ -562,21 +573,25 @@ function UserAIProviderSubsection() {
|
||||
disabled={!provider}
|
||||
className="bg-background border-input mt-1 h-9 w-full rounded-md border px-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
<option value="">— Standard —</option>
|
||||
<option value="">{t("settings.ai.modelOptionDefault")}</option>
|
||||
{presets.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
{provider && (
|
||||
<option value={CUSTOM_MODEL_SENTINEL}>Eigenes…</option>
|
||||
<option value={CUSTOM_MODEL_SENTINEL}>
|
||||
{t("settings.ai.modelOptionCustom")}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{modelChoice === CUSTOM_MODEL_SENTINEL && provider && (
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="ai-model-custom">Eigener Modellname</Label>
|
||||
<Label htmlFor="ai-model-custom">
|
||||
{t("settings.ai.customModelLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-model-custom"
|
||||
value={customModel}
|
||||
@@ -596,10 +611,12 @@ function UserAIProviderSubsection() {
|
||||
{provider === "OPENAI" && (
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="ai-openai-key">
|
||||
OpenAI API Key
|
||||
{t("settings.ai.openaiKeyLabel")}
|
||||
{data?.hasOpenaiKey && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">
|
||||
(gespeichert {data.openaiKeyPreview})
|
||||
{t("settings.ai.savedPreview", {
|
||||
preview: data.openaiKeyPreview ?? "",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
@@ -611,8 +628,7 @@ function UserAIProviderSubsection() {
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
Eigener Key — keine Verbindung zu deinem ChatGPT-Abo. Für das
|
||||
Abo nutze den Verbinden-Button oben.
|
||||
{t("settings.ai.openaiKeyHelp")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -620,10 +636,12 @@ function UserAIProviderSubsection() {
|
||||
{provider === "ANTHROPIC" && (
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="ai-anthropic-key">
|
||||
Anthropic API Key
|
||||
{t("settings.ai.anthropicKeyLabel")}
|
||||
{data?.hasAnthropicKey && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">
|
||||
(gespeichert {data.anthropicKeyPreview})
|
||||
{t("settings.ai.savedPreview", {
|
||||
preview: data.anthropicKeyPreview ?? "",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
@@ -640,7 +658,9 @@ function UserAIProviderSubsection() {
|
||||
{provider === "LOCAL" && (
|
||||
<>
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="ai-base-url">Base URL</Label>
|
||||
<Label htmlFor="ai-base-url">
|
||||
{t("settings.ai.baseUrlLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="ai-base-url"
|
||||
value={baseUrl}
|
||||
@@ -650,12 +670,16 @@ function UserAIProviderSubsection() {
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="ai-local-key">API Key (optional)</Label>
|
||||
<Label htmlFor="ai-local-key">
|
||||
{t("settings.ai.localKeyLabel")}
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="ai-local-key"
|
||||
value={localKey}
|
||||
onChange={(e) => setLocalKey(e.target.value)}
|
||||
placeholder={data?.hasLocalKey ? "(gespeichert)" : ""}
|
||||
placeholder={
|
||||
data?.hasLocalKey ? t("settings.ai.savedShort") : ""
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
@@ -674,7 +698,7 @@ function UserAIProviderSubsection() {
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Speichern
|
||||
{t("settings.ai.saveCta")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -687,7 +711,7 @@ function UserAIProviderSubsection() {
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Verbindung testen
|
||||
{t("settings.ai.testCta")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user