170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
|
|
/**
|
||
|
|
* IndexerStatus — sidebar widget for /admin/batch showing the next-step gap:
|
||
|
|
* how many docs/chunks need to be indexed into Postgres.
|
||
|
|
*/
|
||
|
|
"use client";
|
||
|
|
import { useEffect, useState } from "react";
|
||
|
|
|
||
|
|
interface IndexerPayload {
|
||
|
|
disk: { docs_on_disk: number; chunks_on_disk: number };
|
||
|
|
db: {
|
||
|
|
documents_count: number;
|
||
|
|
chunks_count: number;
|
||
|
|
chunks_with_embedding: number;
|
||
|
|
entities_count: number;
|
||
|
|
entity_mentions_count: number;
|
||
|
|
} | null;
|
||
|
|
db_error: string | null;
|
||
|
|
gap: {
|
||
|
|
docs_to_index: number;
|
||
|
|
chunks_to_index: number;
|
||
|
|
chunks_without_embedding: number;
|
||
|
|
ready_for_retrieval: boolean;
|
||
|
|
} | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function IndexerStatus() {
|
||
|
|
const [data, setData] = useState<IndexerPayload | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let alive = true;
|
||
|
|
async function tick() {
|
||
|
|
try {
|
||
|
|
const r = await fetch("/api/admin/indexer");
|
||
|
|
if (!r.ok) return;
|
||
|
|
const j = (await r.json()) as IndexerPayload;
|
||
|
|
if (alive) {
|
||
|
|
setData(j);
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
if (alive) setLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
tick();
|
||
|
|
const i = setInterval(tick, 60_000);
|
||
|
|
return () => {
|
||
|
|
alive = false;
|
||
|
|
clearInterval(i);
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (loading && !data) {
|
||
|
|
return <div className="font-mono text-xs text-[#5a6678] animate-pulse">indexer status…</div>;
|
||
|
|
}
|
||
|
|
if (!data) return null;
|
||
|
|
|
||
|
|
if (data.db_error) {
|
||
|
|
return (
|
||
|
|
<div className="border border-[rgba(255,107,107,0.30)] bg-[rgba(255,107,107,0.05)] rounded p-4 text-xs space-y-1">
|
||
|
|
<div className="text-[#ff6b6b] font-mono uppercase tracking-widest">database offline</div>
|
||
|
|
<code className="text-[#8896aa] text-[10px]">{data.db_error}</code>
|
||
|
|
<p className="text-[#8896aa]">
|
||
|
|
start Postgres + embed-service (compose), then apply migrations.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const db = data.db!;
|
||
|
|
const gap = data.gap!;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section className="space-y-3">
|
||
|
|
<h3 className="font-mono text-xs uppercase tracking-widest text-[#7fdbff]">
|
||
|
|
retrieval index — Postgres + pgvector
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-3 gap-2 text-xs font-mono">
|
||
|
|
<Card label="docs" lhs={data.disk.docs_on_disk} rhs={db.documents_count} gap={gap.docs_to_index} />
|
||
|
|
<Card
|
||
|
|
label="chunks"
|
||
|
|
lhs={data.disk.chunks_on_disk}
|
||
|
|
rhs={db.chunks_count}
|
||
|
|
gap={gap.chunks_to_index}
|
||
|
|
/>
|
||
|
|
<Card
|
||
|
|
label="embedded"
|
||
|
|
lhs={db.chunks_count}
|
||
|
|
rhs={db.chunks_with_embedding}
|
||
|
|
gap={gap.chunks_without_embedding}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-2 text-xs font-mono">
|
||
|
|
<div className="border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded p-2">
|
||
|
|
<div className="text-[9px] uppercase text-[#5a6678]">entidades canônicas</div>
|
||
|
|
<div className="text-[#a78bfa] mt-0.5">{db.entities_count.toLocaleString()}</div>
|
||
|
|
</div>
|
||
|
|
<div className="border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded p-2">
|
||
|
|
<div className="text-[9px] uppercase text-[#5a6678]">entity_mentions</div>
|
||
|
|
<div className="text-[#7fdbff] mt-0.5">{db.entity_mentions_count.toLocaleString()}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div
|
||
|
|
className={`text-xs font-mono p-2 rounded ${
|
||
|
|
gap.ready_for_retrieval
|
||
|
|
? "border border-[rgba(0,255,156,0.30)] bg-[rgba(0,255,156,0.05)] text-[#00ff9c]"
|
||
|
|
: "border border-[rgba(255,165,0,0.30)] bg-[rgba(255,165,0,0.05)] text-[#ffa500]"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{gap.ready_for_retrieval
|
||
|
|
? "✓ hybrid_search está OPERACIONAL — agente já pode usar"
|
||
|
|
: "⚠ aguardando embeddings — execute scripts/30-index-chunks-to-db.py para ativar retrieval"}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{(gap.docs_to_index > 0 || gap.chunks_to_index > 0) && (
|
||
|
|
<details className="text-xs">
|
||
|
|
<summary className="cursor-pointer font-mono text-[#7fdbff] hover:text-[#00ff9c]">
|
||
|
|
▸ próximos comandos
|
||
|
|
</summary>
|
||
|
|
<pre className="mt-2 p-2 bg-[#060a13] border border-[rgba(0,255,156,0.12)] rounded text-[10px] text-[#c8d4e6] overflow-x-auto">
|
||
|
|
{`# index chunks → Postgres + BGE-M3 embeddings
|
||
|
|
export DATABASE_URL='postgres://...'
|
||
|
|
export EMBED_SERVICE_URL='http://embed:8000'
|
||
|
|
python3 scripts/30-index-chunks-to-db.py --skip-existing
|
||
|
|
|
||
|
|
# materialize entity_mentions (links chunk ↔ entity)
|
||
|
|
python3 scripts/31-populate-entity-mentions.py`}
|
||
|
|
</pre>
|
||
|
|
</details>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function Card({
|
||
|
|
label,
|
||
|
|
lhs,
|
||
|
|
rhs,
|
||
|
|
gap,
|
||
|
|
}: {
|
||
|
|
label: string;
|
||
|
|
lhs: number;
|
||
|
|
rhs: number;
|
||
|
|
gap: number;
|
||
|
|
}) {
|
||
|
|
const ok = gap === 0;
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={`border rounded p-2 ${
|
||
|
|
ok
|
||
|
|
? "border-[rgba(0,255,156,0.30)] bg-[rgba(0,255,156,0.04)]"
|
||
|
|
: "border-[rgba(255,165,0,0.30)] bg-[rgba(255,165,0,0.04)]"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div className="text-[9px] uppercase text-[#5a6678]">{label}</div>
|
||
|
|
<div className="flex items-baseline gap-1 mt-0.5">
|
||
|
|
<span className="text-[#c8d4e6]">{lhs.toLocaleString()}</span>
|
||
|
|
<span className="text-[#5a6678]">/</span>
|
||
|
|
<span className={ok ? "text-[#00ff9c]" : "text-[#ffa500]"}>{rhs.toLocaleString()}</span>
|
||
|
|
</div>
|
||
|
|
{!ok && (
|
||
|
|
<div className="text-[9px] text-[#ffa500] mt-0.5">gap {gap.toLocaleString()}</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|