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 {