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 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-20 05:12:22 +03:00
parent c8af637654
commit d4ccb4f892
2 changed files with 60 additions and 27 deletions

View File

@@ -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",

View File

@@ -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<typeof formSchema>;
// 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]
: 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 (
<Tooltip label={label} withArrow>
<Box
w={9}
h={9}
style={{ borderRadius: "50%", background: color, flex: "none" }}
/>
</Tooltip>
);
}
@@ -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(