/** * Entity detail page — DB-first (live data from public.entity_mentions + chunks). * Wiki frontmatter usado só como fallback estático (aliases, narrativa). */ import Link from "next/link"; import { notFound } from "next/navigation"; import Image from "next/image"; import { readEntity, classKeyToFolder, type EntityClass } from "@/lib/wiki"; import { MarkdownBody } from "@/components/markdown-body"; import { ChatBubble } from "@/components/chat-bubble"; import { AuthBar } from "@/components/auth-bar"; import { EntityGraphMini } from "@/components/entity-graph-mini"; import { getEntityCore, getEntityMentionsByDoc, getEntityChunks, } from "@/lib/retrieval/entity-pages"; const CLASS_TO_SINGULAR: Record = { people: "person", organizations: "organization", locations: "location", events: "event", "uap-objects": "uap_object", vehicles: "vehicle", operations: "operation", concepts: "concept", }; export const dynamic = "force-dynamic"; const CLASS_TITLE: Record = { people: "Pessoa", organizations: "Organização", locations: "Local", events: "Evento", "uap-objects": "Objeto UAP", vehicles: "Veículo", operations: "Operação", concepts: "Conceito", }; const CLASS_COLOR: Record = { people: "text-[#ff6ec7] border-[#ff6ec7]", organizations: "text-[#ff8a4d] border-[#ff8a4d]", locations: "text-[#3fde6a] border-[#3fde6a]", events: "text-[#ffa500] border-[#ffa500]", "uap-objects": "text-[#ff3344] border-[#ff3344]", vehicles: "text-[#5b9bd5] border-[#5b9bd5]", operations: "text-[#9b5de5] border-[#9b5de5]", concepts: "text-[#06d6a0] border-[#06d6a0]", }; const CLASS_BG: Record = { people: "from-[rgba(255,110,199,0.10)]", organizations: "from-[rgba(255,138,77,0.10)]", locations: "from-[rgba(63,222,106,0.10)]", events: "from-[rgba(255,165,0,0.10)]", "uap-objects": "from-[rgba(255,51,68,0.10)]", vehicles: "from-[rgba(91,155,213,0.10)]", operations: "from-[rgba(155,93,229,0.10)]", concepts: "from-[rgba(6,214,160,0.10)]", }; function pageOcurrencesText(pages: number[]): string { if (pages.length === 0) return "—"; if (pages.length <= 5) return `p${pages.join(", p")}`; return `p${pages.slice(0, 4).join(", p")} +${pages.length - 4}`; } export default async function EntityPage({ params, }: { params: Promise<{ cls: string; id: string }>; }) { const { cls, id } = await params; const folder = classKeyToFolder(cls); if (!folder) notFound(); const entityClassSingular = CLASS_TO_SINGULAR[folder as string] ?? folder; // YAML-first: every count comes from the entity's frontmatter (kept in sync // by scripts/maintain/42_sync_entity_stats.py). The DB is consulted ONLY for // chunk previews, not for counts. const core = await getEntityCore(entityClassSingular, id).catch(() => null); const wiki = await readEntity(folder as EntityClass, id); if (!core && !wiki) notFound(); const canonical = core?.canonical_name ?? (wiki?.fm.canonical_name as string) ?? id; const aliases = (core?.aliases ?? (wiki?.fm.aliases as string[]) ?? []).filter( (a) => a !== canonical, ); const mentionGroups = core ? await getEntityMentionsByDoc(entityClassSingular, id, 100).catch(() => []) : []; const sampleChunks = core ? await getEntityChunks(core.entity_pk, 12).catch(() => []) : []; const totalMentions = core?.total_mentions ?? 0; const documentsCount = core?.documents_count ?? 0; const strength = core?.signal_strength ?? "unverified"; const sigs = core?.signal_sources ?? { db_chunks: 0, page_refs: 0, cross_refs: 0 }; const classColor = CLASS_COLOR[folder as EntityClass]; const classBg = CLASS_BG[folder as EntityClass]; return (
← home ← todos {folder} 🕸 ver no grafo
{/* Hero header */}
{CLASS_TITLE[folder as EntityClass]} · /e/{folder}/{id}

▍ {canonical}

{aliases.length > 0 && (
{aliases.slice(0, 12).map((a) => ( {a} ))}
)}
menções
{totalMentions}
documentos
{documentsCount}
{core?.enrichment_status && core.enrichment_status !== "none" && (
enrichment
{core.enrichment_status}
)}
força do sinal
{strength === "strong" && "forte"} {strength === "weak" && "fraca"} {strength === "orphan" && "órfã"} {strength === "unverified" && "não verificada"}
{sigs.db_chunks} chunks · {sigs.page_refs} págs · {sigs.cross_refs} backlinks
{strength === "orphan" && (

⚠ entidade não confirmada: nenhuma página, chunk ou outra entidade aponta para ela. Pode ser extração ruidosa do pipeline original.

)}
{/* MAIN — narrative + chunks live */}
{/* Live chunk previews — most impactful section */} {sampleChunks.length > 0 && (

Aparece em {sampleChunks.length}+ trechos · top {sampleChunks.length}

{sampleChunks.map((c) => { const text = (c.content_pt || c.content_en || "").trim(); const docPretty = c.doc_id.replace(/^doc-/, "").replace(/-/g, " ").slice(0, 60); const cropUrl = c.bbox ? `/api/crop?doc=${encodeURIComponent(c.doc_id)}&page=${c.page}&x=${c.bbox.x}&y=${c.bbox.y}&w=${c.bbox.w}&h=${c.bbox.h}&w_px=200` : null; return ( {cropUrl && ( )}
{c.chunk_id} p{c.page} {c.type} {c.classification && ( {c.classification} )} {c.ufo_anomaly && ( 🛸 {c.ufo_anomaly_type ?? "UAP"} )}
{text}
{docPretty}
); })}
)} {/* Narrative body (Haiku stub OK quando rico) */} {wiki?.body && wiki.body.trim().length > 30 && (

Narrativa

{wiki.body}
)} {sampleChunks.length === 0 && (!wiki?.body || wiki.body.trim().length === 0) && (
Entidade ainda sem chunks indexados na DB. Aguarde o indexer terminar.
)}
{/* SIDEBAR — documentos onde aparece (DB live) + grafo mini */}
); }