From 881b18c5ea8e22d41d54a9d9e2e79c5b004b397b Mon Sep 17 00:00:00 2001 From: Sulthan Nauval Abdillah Date: Mon, 10 Aug 2026 14:32:36 +0000 Subject: [PATCH] fix(ops): accept list matches the backend; drawer reuses deriveGraphState; server count first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the file picker offered .docx/.xlsx (need the non-default kb-office build feature — a stock gateway rejects them AFTER the full transfer) and .css (never supported, 400 unsupported_file_type); dropped (plan 111 option (a)), with a comment naming src/kb/file/mod.rs as the source of truth; verified programmatically: every remaining entry appears in the backend lists - .heic added to IMAGE_EXTS (backend supports it); fixed the comment claiming images need no vision model — process_image posts to a chat-completions endpoint and requires a credential - the drawer's empty state now branches through deriveGraphState — the same pure function as the graph tab — instead of its own inline copy of the logic (the two surfaces disagreeing is how this started); three drawer branches pinned in the helper's test file - detail-view document count prefers group.document_count (correct since RantAIClaw plan 100) over the locally-fetched list length, which quietly hid a server-side divergence - re-extract toast wording was already fixed in #53 (plan 095 companion) --- .../ops/doc-intelligence-drawer.tsx | 42 +++++++++++-------- src/components/ops/graph-lens-helpers.test.ts | 20 +++++++++ src/components/ops/kb-panel.tsx | 5 ++- src/lib/attachments.ts | 16 ++++--- 4 files changed, 59 insertions(+), 24 deletions(-) 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 {