98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
|
|
/**
|
||
|
|
* 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>
|
||
|
|
);
|
||
|
|
}
|