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:
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
: 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 (
|
||||
<Box
|
||||
w={9}
|
||||
h={9}
|
||||
style={{ borderRadius: "50%", background: color, flex: "none" }}
|
||||
/>
|
||||
<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(
|
||||
|
||||
Reference in New Issue
Block a user