disclosure-bureau/web/components/entity-mention-chunks.tsx

98 lines
3.4 KiB
TypeScript
Raw Normal View History

/**
* EntityMentionChunks live chunk list from public.entity_mentions for this entity.
*
* Server component. Returns up to `limit` chunks where this entity appears
* (after 31-populate-entity-mentions.py has run). Empty state hides gracefully
* the markdown `mentioned_in[]` panel above is the static fallback.
*/
import Image from "next/image";
import Link from "next/link";
import { findEntity } from "@/lib/retrieval/graph";
import { pgQuery } from "@/lib/retrieval/db";
interface ChunkRow {
chunk_pk: number;
doc_id: string;
chunk_id: string;
page: number;
type: string;
bbox: { x: number; y: number; w: number; h: number } | null;
content_en: string | null;
content_pt: string | null;
classification: string | null;
}
export async function EntityMentionChunks({
entityClassSingular,
entityId,
limit = 30,
}: {
entityClassSingular: string;
entityId: string;
limit?: number;
}) {
let rows: ChunkRow[] = [];
try {
const ent = await findEntity(entityClassSingular, entityId);
if (!ent) return null;
rows = await pgQuery<ChunkRow>(
`SELECT c.chunk_pk, c.doc_id, c.chunk_id, c.page, c.type, c.bbox,
c.content_en, c.content_pt, c.classification
FROM public.entity_mentions em
JOIN public.chunks c ON c.chunk_pk = em.chunk_pk
WHERE em.entity_pk = $1
ORDER BY c.doc_id, c.order_global
LIMIT $2`,
[ent.entity_pk, limit],
);
} catch {
return null;
}
if (rows.length === 0) return null;
return (
<section className="mt-8 pt-6 border-t border-[rgba(0,255,156,0.12)]">
<h2 className="font-mono text-sm text-[#7fdbff] uppercase tracking-widest mb-3 border-l-2 border-[#7fdbff] pl-3">
Live chunk mentions · {rows.length}
</h2>
<div className="space-y-2">
{rows.map((r) => {
const cropUrl = r.bbox
? `/api/crop?doc=${encodeURIComponent(r.doc_id)}&page=${r.page}` +
`&x=${r.bbox.x}&y=${r.bbox.y}&w=${r.bbox.w}&h=${r.bbox.h}&w_px=240`
: null;
const text = r.content_pt || r.content_en || "";
return (
<Link
key={r.chunk_pk}
href={`/d/${r.doc_id}#${r.chunk_id}`}
className="flex items-start gap-3 p-2 bg-[#0a121e] border border-[rgba(127,219,255,0.20)] hover:border-[#00ff9c] rounded text-xs"
>
{cropUrl && (
<Image
src={cropUrl}
alt=""
width={96}
height={60}
className="block w-24 h-15 object-cover bg-black rounded flex-shrink-0"
unoptimized
/>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-[10px] font-mono mb-1">
<span className="text-[#7fdbff]">{r.chunk_id}</span>
<span className="text-[#5a6678]">p{r.page}</span>
<span className="text-[#5a6678]">{r.type}</span>
{r.classification && <span className="text-[#ff6b6b]">{r.classification}</span>}
</div>
<div className="text-[#c8d4e6] line-clamp-3">{text}</div>
<div className="text-[10px] font-mono text-[#5a6678] truncate mt-1">{r.doc_id}</div>
</div>
</Link>
);
})}
</div>
</section>
);
}