From d4ccb4f8920edac05f6f756719806e1209d2515c Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sat, 20 Jun 2026 05:12:22 +0300 Subject: [PATCH] feat(ai-settings): status dot shows configured x enabled, not test result The endpoint header dot previously reflected only the last manual 'Test endpoint' result (green=ok / red=fail / gray=never tested), so a configured, enabled endpoint looked gray until probed. Rebind it to a synchronous derivation from the form + feature toggle (no network): ready (green) configured AND enabled configured (yellow) configured but feature off misconfigured(orange) feature on but not configured (real misconfig) off (gray) not configured and not enabled 'Configured' = model filled AND a Base URL present (own or inherited from Chat); API key not required (local servers). Adds a Tooltip on the dot so color isn't the only signal. The Test button result stays as text. Implements docs/backlog/ai-endpoint-status-dot-config-enabled.md. Co-Authored-By: Claude Opus 4.8 --- .../public/locales/en-US/translation.json | 4 + .../components/ai-provider-settings.tsx | 83 +++++++++++++------ 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 21f7c5f7..38b6e9b0 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -722,6 +722,10 @@ "Connection successful": "Connection successful", "Connection failed": "Connection failed", "Only workspace admins can manage AI provider settings.": "Only workspace admins can manage AI provider settings.", + "Configured and enabled": "Configured and enabled", + "Configured but disabled": "Configured but disabled", + "Enabled but not configured": "Enabled but not configured", + "Not configured": "Not configured", "Sources": "Sources", "AI Answers not available for attachments": "AI Answers not available for attachments", "No answer available": "No answer available", diff --git a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx index 78727bda..4e1e6831 100644 --- a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx +++ b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx @@ -15,6 +15,7 @@ import { Text, Textarea, TextInput, + Tooltip, useMantineTheme, } from "@mantine/core"; import { useForm } from "@mantine/form"; @@ -60,8 +61,19 @@ const formSchema = z.object({ type FormValues = z.infer; -// Status of an endpoint card, drives the little status dot color. -type CardStatus = "ok" | "error" | "idle"; +// Endpoint health shown by the header dot, derived synchronously from the +// form + feature toggle (never from a network probe — the "Test endpoint" +// button still shows the live probe result as text). +// "ready" (green) — configured AND enabled +// "configured" (yellow) — configured but the feature is OFF +// "misconfigured" (orange/red warning) — enabled but NOT configured (real misconfig) +// "off" (gray) — not configured and not enabled (nothing to enable) +type CardStatus = "ready" | "configured" | "misconfigured" | "off"; + +function resolveCardStatus(configured: boolean, enabled: boolean): CardStatus { + if (configured) return enabled ? "ready" : "configured"; + return enabled ? "misconfigured" : "off"; +} // Resolve a "Base URL + path" hint defensively: trim a single trailing slash // off the base, then append the path. Empty base falls back to `fallback` @@ -71,21 +83,35 @@ function resolveUrl(base: string, path: string, fallback = ""): string { return `${trimmed}${path}`; } -// Small colored dot used in each card header. +// Small colored dot used in each card header. Wrapped in a Tooltip so the +// state is conveyed by text too — color must not be the only signal (a11y). function StatusDot({ status }: { status: CardStatus }) { const theme = useMantineTheme(); + const { t } = useTranslation(); const color = - status === "ok" + status === "ready" ? theme.colors.green[6] - : status === "error" - ? theme.colors.red[6] - : theme.colors.gray[5]; + : status === "configured" + ? theme.colors.yellow[6] + : status === "misconfigured" + ? theme.colors.orange[6] + : theme.colors.gray[5]; + const label = + status === "ready" + ? t("Configured and enabled") + : status === "configured" + ? t("Configured but disabled") + : status === "misconfigured" + ? t("Enabled but not configured") + : t("Not configured"); return ( - + + + ); } @@ -353,21 +379,24 @@ export default function AiProviderSettings() { ); } - const chatStatus: CardStatus = chatTest.data - ? chatTest.data.ok - ? "ok" - : "error" - : "idle"; - const embedStatus: CardStatus = embedTest.data - ? embedTest.data.ok - ? "ok" - : "error" - : "idle"; - const sttStatus: CardStatus = sttTest.data - ? sttTest.data.ok - ? "ok" - : "error" - : "idle"; + // "Configured" = model filled AND a Base URL available (own or inherited from + // Chat). API key is intentionally NOT required — local servers (Ollama, + // speaches / faster-whisper-server) need none. Derived live from form.values + // so the dot reacts while typing. Compare after .trim() (values may have + // surrounding whitespace). + const v = form.values; + const chatBase = v.baseUrl.trim(); + const chatConfigured = v.chatModel.trim() !== "" && chatBase !== ""; + const embedConfigured = + v.embeddingModel.trim() !== "" && + (v.embeddingBaseUrl.trim() !== "" || chatBase !== ""); + const sttConfigured = + v.sttModel.trim() !== "" && + (v.sttBaseUrl.trim() !== "" || chatBase !== ""); + + const chatStatus = resolveCardStatus(chatConfigured, chatEnabled); + const embedStatus = resolveCardStatus(embedConfigured, searchEnabled); + const sttStatus = resolveCardStatus(sttConfigured, dictationEnabled); const chatResolved = resolveUrl(form.values.baseUrl, "/chat/completions"); const embedResolved = resolveUrl(