diff --git a/src/components/ops/doc-intelligence-drawer.tsx b/src/components/ops/doc-intelligence-drawer.tsx index 693cdc9..75814a5 100644 --- a/src/components/ops/doc-intelligence-drawer.tsx +++ b/src/components/ops/doc-intelligence-drawer.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import { Loader2, Network, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { api } from "@/lib/api"; +import { deriveGraphState } from "./graph-lens-helpers"; import { useAsync } from "@/hooks/use-async"; import { formatNumber } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; @@ -103,27 +104,34 @@ export function DocIntelligenceBody({ documentId }: { documentId: string }) { } title="No entities found" - hint={ - intel.data?.capability && !intel.data.capability.intelligence_enabled ? ( - <> - Intelligence extraction is disabled — enable it with{" "} - KB_INTELLIGENCE_ENABLED, or use Re-extract, which works - while disabled. - - ) : intel.data?.capability && - intel.data.capability.credential_configured === false ? ( - <> - Extraction is enabled but no API key resolves for the extraction endpoint, so - extraction fails silently. Add a key under Knowledge Base settings (or set{" "} - OPENROUTER_API_KEY), then Re-extract. - - ) : ( + hint={(() => { + // Same pure function as the graph tab — the two surfaces + // disagreeing about WHY it is empty is how this started + // (plan 111 reuses plan 097's deriveGraphState). + const state = deriveGraphState(intel.data?.capability, 0, false, true); + if (state === "disabled") + return ( + <> + Intelligence extraction is disabled — enable it with{" "} + KB_INTELLIGENCE_ENABLED, or use Re-extract, which works + while disabled. + + ); + if (state === "no-credential") + return ( + <> + Extraction is enabled but no API key resolves for the extraction endpoint, so + extraction fails silently. Add a key under Knowledge Base settings (or set{" "} + OPENROUTER_API_KEY), then Re-extract. + + ); + return ( <> No entities are stored for this document — it may genuinely yield none. Try{" "} Re-extract. - ) - } + ); + })()} /> ) : ( diff --git a/src/components/ops/graph-lens-helpers.test.ts b/src/components/ops/graph-lens-helpers.test.ts index 2f180a5..f29a0e2 100644 --- a/src/components/ops/graph-lens-helpers.test.ts +++ b/src/components/ops/graph-lens-helpers.test.ts @@ -47,3 +47,23 @@ describe("deriveGraphState", () => { ); }); }); + +describe("deriveGraphState as used by the drawer (plan 111)", () => { + // The drawer passes corpusEntities=0, loading=false, hasData=true and + // branches its empty-state on the result — pin the three branches it + // renders so drawer and graph tab cannot drift apart again. + it("drawer branch: disabled", () => { + expect(deriveGraphState(cap({ intelligence_enabled: false }), 0, false, true)).toBe( + "disabled", + ); + }); + it("drawer branch: no-credential", () => { + expect(deriveGraphState(cap({ credential_configured: false }), 0, false, true)).toBe( + "no-credential", + ); + }); + it("drawer branch: genuinely empty (incl. missing capability)", () => { + expect(deriveGraphState(cap(), 0, false, true)).toBe("empty"); + expect(deriveGraphState(undefined, 0, false, true)).toBe("empty"); + }); +}); diff --git a/src/components/ops/kb-panel.tsx b/src/components/ops/kb-panel.tsx index 0924630..f9d56c3 100644 --- a/src/components/ops/kb-panel.tsx +++ b/src/components/ops/kb-panel.tsx @@ -666,7 +666,10 @@ function KbDetail({ } }; - const docCount = docs.data?.length ?? group.document_count ?? 0; + // Server count first (correct since RantAIClaw plan 100 — soft-deleted + // excluded): preferring the locally-fetched list length quietly hid a + // server-side divergence instead of revealing it (plan 111). + const docCount = group.document_count ?? docs.data?.length ?? 0; return (
diff --git a/src/lib/attachments.ts b/src/lib/attachments.ts index 6384827..6655a67 100644 --- a/src/lib/attachments.ts +++ b/src/lib/attachments.ts @@ -4,10 +4,13 @@ // retrieved on each send and injected into the SENT message only. // Accepted upload extensions: documents + common code/text formats. +// Source of truth: RantAIClaw src/kb/file/mod.rs (MARKDOWN/PDF/IMAGE/TEXT +// extension lists) — every entry here MUST appear there or the upload +// transfers fully and then fails server-side. .docx/.xlsx are deliberately +// absent: they need the non-default kb-office build feature (RantAIClaw +// plan 111 option (a) — a stock gateway rejects them after the transfer). export const ACCEPT_EXTS = [ ".pdf", - ".docx", - ".xlsx", ".md", ".txt", ".csv", @@ -31,7 +34,6 @@ export const ACCEPT_EXTS = [ ".yml", ".toml", ".html", - ".css", ".sql", ]; @@ -43,9 +45,11 @@ export function acceptAttr(): string { return ACCEPT_EXTS.join(","); } -// Images are ingested the same way as documents — the KB's vision-LLM extractor -// reads them into searchable text at ingest time (no vision chat model needed). -export const IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".gif"]; +// Images are ingested the same way as documents — the KB's vision-LLM +// extractor posts them to a chat-completions endpoint at ingest time, so a +// vision-capable model AND a credential ARE required (the old comment +// claimed otherwise — plan 111). +export const IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".heic"]; /** Value for an image attribute. */ export function imageAcceptAttr(): string {