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 successful": "Connection successful",
|
||||||
"Connection failed": "Connection failed",
|
"Connection failed": "Connection failed",
|
||||||
"Only workspace admins can manage AI provider settings.": "Only workspace admins can manage AI provider settings.",
|
"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",
|
"Sources": "Sources",
|
||||||
"AI Answers not available for attachments": "AI Answers not available for attachments",
|
"AI Answers not available for attachments": "AI Answers not available for attachments",
|
||||||
"No answer available": "No answer available",
|
"No answer available": "No answer available",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
Tooltip,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
@@ -60,8 +61,19 @@ const formSchema = z.object({
|
|||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
// Status of an endpoint card, drives the little status dot color.
|
// Endpoint health shown by the header dot, derived synchronously from the
|
||||||
type CardStatus = "ok" | "error" | "idle";
|
// 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
|
// 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`
|
// 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}`;
|
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 }) {
|
function StatusDot({ status }: { status: CardStatus }) {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
const color =
|
const color =
|
||||||
status === "ok"
|
status === "ready"
|
||||||
? theme.colors.green[6]
|
? theme.colors.green[6]
|
||||||
: status === "error"
|
: status === "configured"
|
||||||
? theme.colors.red[6]
|
? theme.colors.yellow[6]
|
||||||
: theme.colors.gray[5];
|
: 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 (
|
return (
|
||||||
<Box
|
<Tooltip label={label} withArrow>
|
||||||
w={9}
|
<Box
|
||||||
h={9}
|
w={9}
|
||||||
style={{ borderRadius: "50%", background: color, flex: "none" }}
|
h={9}
|
||||||
/>
|
style={{ borderRadius: "50%", background: color, flex: "none" }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,21 +379,24 @@ export default function AiProviderSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatStatus: CardStatus = chatTest.data
|
// "Configured" = model filled AND a Base URL available (own or inherited from
|
||||||
? chatTest.data.ok
|
// Chat). API key is intentionally NOT required — local servers (Ollama,
|
||||||
? "ok"
|
// speaches / faster-whisper-server) need none. Derived live from form.values
|
||||||
: "error"
|
// so the dot reacts while typing. Compare after .trim() (values may have
|
||||||
: "idle";
|
// surrounding whitespace).
|
||||||
const embedStatus: CardStatus = embedTest.data
|
const v = form.values;
|
||||||
? embedTest.data.ok
|
const chatBase = v.baseUrl.trim();
|
||||||
? "ok"
|
const chatConfigured = v.chatModel.trim() !== "" && chatBase !== "";
|
||||||
: "error"
|
const embedConfigured =
|
||||||
: "idle";
|
v.embeddingModel.trim() !== "" &&
|
||||||
const sttStatus: CardStatus = sttTest.data
|
(v.embeddingBaseUrl.trim() !== "" || chatBase !== "");
|
||||||
? sttTest.data.ok
|
const sttConfigured =
|
||||||
? "ok"
|
v.sttModel.trim() !== "" &&
|
||||||
: "error"
|
(v.sttBaseUrl.trim() !== "" || chatBase !== "");
|
||||||
: "idle";
|
|
||||||
|
const chatStatus = resolveCardStatus(chatConfigured, chatEnabled);
|
||||||
|
const embedStatus = resolveCardStatus(embedConfigured, searchEnabled);
|
||||||
|
const sttStatus = resolveCardStatus(sttConfigured, dictationEnabled);
|
||||||
|
|
||||||
const chatResolved = resolveUrl(form.values.baseUrl, "/chat/completions");
|
const chatResolved = resolveUrl(form.values.baseUrl, "/chat/completions");
|
||||||
const embedResolved = resolveUrl(
|
const embedResolved = resolveUrl(
|
||||||
|
|||||||
Reference in New Issue
Block a user