From 8b9421f55e1169d9360d3e505984b092f345381a Mon Sep 17 00:00:00 2001 From: Sulthan Nauval Abdillah Date: Mon, 10 Aug 2026 13:53:16 +0000 Subject: [PATCH] feat(ops): KB activation screen; stop stacking broken sub-panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KbPanel mounted the settings card, the Documents/Graph switch, and KbList unconditionally; KbList fetches on mount, so a disabled or keyless KB rendered the 'add an API key' card with two 503 error panels underneath it — the panel now gates on GET /config/knowledge and renders ONLY the activation screen until the KB can answer - knowledge-settings-card becomes the activation screen with three states (off+no-key: inputs + Activate; off+key: one-click Activate, key kept; on+key: compact row + Deactivate/Edit/Remove key) - Deactivate is not Clear: it sends {enabled:false} and keeps the key; the destructive Remove key stays behind its confirm modal, reworded — 'search will stop working' now describes Deactivate, and the modal points at it for the non-destructive path - the gateway's live key probe 400 surfaces INLINE on the key input (a rejected key is a form error), with the button reading 'Verifying…' - third dead-end KB_INTELLIGENCE_ENABLED instruction (knowledge-graph zero-node EmptyState) replaced with actions the operator can take — the other two were fixed with the no-credential state in #54 - KnowledgeStatus.enabled + setKnowledge enabled are optional: older gateways omit them and the panel treats configured-as-enabled (pre-v0.18.5 behaviour preserved) --- src/components/ops/kb-panel.tsx | 17 +- src/components/ops/knowledge-graph.tsx | 5 +- .../ops/knowledge-settings-card.tsx | 236 +++++++++++++----- src/lib/api.ts | 3 +- src/lib/types.ts | 2 + 5 files changed, 197 insertions(+), 66 deletions(-) diff --git a/src/components/ops/kb-panel.tsx b/src/components/ops/kb-panel.tsx index 40a6ff0..0924630 100644 --- a/src/components/ops/kb-panel.tsx +++ b/src/components/ops/kb-panel.tsx @@ -71,6 +71,14 @@ const errMsg = (e: unknown) => (e instanceof Error ? e.message : String(e)); type LibraryView = "documents" | "graph"; export function KbPanel() { + // Gate on activation BEFORE mounting the library: KbList fetches on mount + // and a 503 from a disabled/keyless KB stacked error panels under the + // activation card (plan 106). Older gateways omit `enabled`; treat + // configured-as-enabled there. + const kbStatus = useAsync(() => api.getKnowledge(), []); + const kbEnabled = kbStatus.data + ? (kbStatus.data.enabled ?? kbStatus.data.embedding_configured) + : false; const groups = useAsync(() => api.kbGroups(), []); const [selected, setSelected] = React.useState(null); const [view, setView] = React.useState("documents"); @@ -85,10 +93,15 @@ export function KbPanel() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [groups.data]); + if (kbStatus.loading) return null; + if (!kbEnabled) { + // Activation screen only — no Documents/Graph chrome, no doomed fetches. + return ; + } + return (
- {/* Self-refreshes on save via its own useAsync — no parent state needed. */} - + - No entities have been extracted yet. Intelligence extraction may be disabled - (KB_INTELLIGENCE_ENABLED) or not yet run for these documents. + No entities have been extracted for these documents yet. Check the Graph tab's + status for the cause (extraction off, or no credential under Knowledge Base + settings), or use a document's Re-extract. } /> diff --git a/src/components/ops/knowledge-settings-card.tsx b/src/components/ops/knowledge-settings-card.tsx index a3264cc..5913b6d 100644 --- a/src/components/ops/knowledge-settings-card.tsx +++ b/src/components/ops/knowledge-settings-card.tsx @@ -12,57 +12,95 @@ import { Badge } from "@/components/ui/badge"; import { ConfirmModal } from "@/components/ui/confirm-modal"; /** - * Knowledge Base credentials card. Doubles as the "not configured" gate: - * prominent with an input when no key resolves; a compact status row when - * configured. Consumes GET/PUT /api/rc/config/knowledge. The key value is - * never returned by the backend — this card only ever shows booleans + source. + * Knowledge Base activation screen + status row (RantAIClaw plan 106). + * + * Three states, driven by `enabled` × `embedding_configured`: + * - off + no key → "Activate Knowledge Base": key inputs + Activate + * - off + key → "Knowledge Base is off": one-click Activate (key kept) + * - on + key → compact status row + Deactivate / Edit / Remove key + * + * Deactivate is NOT Clear: it sends `{enabled:false}` and keeps the key so + * re-activation is one click. The destructive Remove key stays behind its + * confirm modal. A key the provider rejects (the gateway probes it live and + * answers 400) surfaces INLINE on the input — a rejected key is a form + * error, not a toast. */ -export function KnowledgeSettingsCard() { +export function KnowledgeSettingsCard({ onChanged }: { onChanged?: () => void }) { const status = useAsync(() => api.getKnowledge(), []); const [editing, setEditing] = React.useState(false); const [embedding, setEmbedding] = React.useState(""); const [vision, setVision] = React.useState(""); const [busy, setBusy] = React.useState(false); const [confirmClear, setConfirmClear] = React.useState(false); + const [formError, setFormError] = React.useState(null); const configured = status.data?.embedding_configured ?? false; + // Older gateways omit `enabled`; treat configured-as-enabled there so the + // console keeps working against them (pre-v0.18.5 behaviour). + const enabled = status.data?.enabled ?? configured; const source = status.data?.source ?? "none"; + const envManaged = source === "env"; - const save = async () => { - const emb = embedding.trim(); - if (!emb && !configured) return; + const refreshAll = () => { + status.refresh(); + onChanged?.(); + }; + + const put = async ( + body: { enabled?: boolean; embedding_api_key?: string; vision_api_key?: string }, + okMessage: string, + ): Promise => { setBusy(true); + setFormError(null); try { - const body: { embedding_api_key?: string; vision_api_key?: string } = {}; - if (emb) body.embedding_api_key = emb; - if (vision.trim()) body.vision_api_key = vision.trim(); await api.setKnowledge(body); - toast.success("Knowledge Base configured"); - setEmbedding(""); - setVision(""); - setEditing(false); - status.refresh(); + toast.success(okMessage); + return true; } catch (e) { - toast.error(e instanceof Error ? e.message : String(e)); + const msg = e instanceof Error ? e.message : String(e); + // The gateway probes the key before persisting; its 400 belongs on + // the form, not (only) in a toast. + if (body.embedding_api_key) setFormError(msg); + else toast.error(msg); + return false; } finally { setBusy(false); } }; - const clear = async () => { - setBusy(true); - try { - await api.setKnowledge({ embedding_api_key: "", vision_api_key: "" }); - toast.success("Knowledge Base keys cleared"); - setConfirmClear(false); - status.refresh(); - } catch (e) { - toast.error(e instanceof Error ? e.message : String(e)); - } finally { - setBusy(false); + const saveAndActivate = async () => { + const emb = embedding.trim(); + if (!emb && !configured) return; + const body: { enabled?: boolean; embedding_api_key?: string; vision_api_key?: string } = { + enabled: true, + }; + if (emb) body.embedding_api_key = emb; + if (vision.trim()) body.vision_api_key = vision.trim(); + if (await put(body, "Knowledge Base activated")) { + setEmbedding(""); + setVision(""); + setEditing(false); + refreshAll(); } }; + const setEnabled = async (next: boolean) => { + const ok = await put( + { enabled: next }, + next ? "Knowledge Base activated" : "Knowledge Base deactivated — key kept", + ); + if (ok) refreshAll(); + }; + + const clear = async () => { + const ok = await put( + { enabled: false, embedding_api_key: "", vision_api_key: "" }, + "Knowledge Base keys removed", + ); + setConfirmClear(false); + if (ok) refreshAll(); + }; + if (status.loading) return null; // Never silently vanish on error — that hides the only place to enter an @@ -82,51 +120,109 @@ export function KnowledgeSettingsCard() { ); } - if (configured && !editing) { - const envManaged = source === "env"; + // State: configured but off — one-click reactivation, key retained. + if (!enabled && configured && !editing) { return ( <> - -
- - Knowledge Base configured - source: {source} - {status.data?.vision_configured && OCR on} -
- {envManaged ? ( - - Managed by KB_EMBEDDING_API_KEY — unset it to manage the key here. - - ) : ( + +
+ + Knowledge Base is off + key stored +
- - + + {!envManaged && ( + + )}
- )} -
- setConfirmClear(false)} - title="Clear Knowledge Base keys?" - description="Document search will stop working until you re-enter an embedding key." - confirmLabel="Clear keys" - busy={busy} - onConfirm={clear} - /> +
+ setConfirmClear(false)} + title="Remove Knowledge Base keys?" + description="Permanently removes the stored keys — you will have to re-enter one to activate again. To pause the Knowledge Base without losing the key, use Deactivate instead." + confirmLabel="Remove keys" + busy={busy} + onConfirm={clear} + /> + + ); + } + + // State: on + configured — compact status row. + if (enabled && configured && !editing) { + return ( + <> + +
+ + Knowledge Base active + source: {source} + {status.data?.vision_configured && OCR on} +
+ {envManaged ? ( +
+ + Key managed by KB_EMBEDDING_API_KEY — unset it to manage the key here. + + +
+ ) : ( +
+ + + +
+ )} +
+ setConfirmClear(false)} + title="Remove Knowledge Base keys?" + description="Permanently removes the stored keys — you will have to re-enter one to activate again. To pause the Knowledge Base without losing the key, use Deactivate instead." + confirmLabel="Remove keys" + busy={busy} + onConfirm={clear} + /> ); } + // State: activation form — no key yet (or editing an existing one). return (
- Knowledge Base — add an API key + {" "} + {editing ? "Edit Knowledge Base key" : "Activate Knowledge Base"}

Document search needs an embedding API key. Optionally add a separate OCR/vision key (defaults to the embedding key). Stored encrypted; env KB_EMBEDDING_API_KEY{" "} - still overrides. + still overrides. The key is verified with the provider before it is saved.

setEmbedding(e.target.value)} + aria-invalid={formError ? true : undefined} + onChange={(e) => { + setEmbedding(e.target.value); + setFormError(null); + }} /> + {formError &&

{formError}

} setVision(e.target.value)} />
- {editing && ( - )} diff --git a/src/lib/api.ts b/src/lib/api.ts index 0d44391..16c99a7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -395,10 +395,11 @@ export const api = { // ---- Knowledge Base credentials ([knowledge] config) ---- getKnowledge: () => rc("config/knowledge"), setKnowledge: (body: { + enabled?: boolean; embedding_api_key?: string; vision_api_key?: string; }) => - rc<{ embedding_configured: boolean; vision_configured: boolean }>( + rc<{ enabled?: boolean; embedding_configured: boolean; vision_configured: boolean }>( "config/knowledge", { method: "PUT", diff --git a/src/lib/types.ts b/src/lib/types.ts index 8bf2544..cc1a098 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -177,6 +177,8 @@ export interface KbGroup { } export interface KnowledgeStatus { + /** Whether the KB is active. Optional: older gateways omit it. */ + enabled?: boolean; embedding_configured: boolean; vision_configured: boolean; /** Effective source of the embedding key, reported without revealing it. */