/** * EntityRelations — typed relations panel for an entity page. * * Renders semantically-typed edges (Person witnessed Event, Event documented_in * Document, etc.) grouped by relation_type and direction, instead of the * noisy co-mention list. */ "use client"; import { useEffect, useState } from "react"; import Link from "next/link"; interface Relation { source_class: string; source_id: string; relation_type: string; target_class: string; target_id: string; evidence_ref: string | null; confidence: string; } interface ApiResponse { outgoing: Relation[]; incoming: Relation[]; error?: string; } const TYPE_LABEL_PT: Record = { witnessed: { out: "testemunhou", in: "foi testemunhado por" }, occurred_at: { out: "ocorreu em", in: "foi local de" }, involves_uap: { out: "envolve UAP", in: "observado em" }, documented_in: { out: "documentado em", in: "documenta" }, authored: { out: "autoria de", in: "autor:" }, signed: { out: "assinou", in: "assinado por" }, mentioned_by: { out: "mencionado em", in: "menciona" }, employed_by: { out: "trabalhou em", in: "empregou" }, operated_by: { out: "operada por", in: "operou" }, investigated: { out: "investigou", in: "investigado por" }, commanded: { out: "comandou", in: "comandado por" }, related_to: { out: "relacionado a", in: "relacionado por" }, similar_to: { out: "similar a", in: "similar de" }, precedes: { out: "precede", in: "precedido por" }, follows: { out: "segue", in: "seguido por" }, }; const ENTITY_FOLDER: Record = { person: "people", organization: "organizations", location: "locations", event: "events", uap_object: "uap-objects", vehicle: "vehicles", operation: "operations", concept: "concepts", }; function entityHref(cls: string, id: string): string { if (cls === "document") return `/d/${id}`; const folder = ENTITY_FOLDER[cls] ?? cls; return `/e/${folder}/${id}`; } export function EntityRelations({ entityClass, entityId, }: { entityClass: string; entityId: string; }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let aborted = false; setLoading(true); fetch(`/api/relations?class=${entityClass}&id=${encodeURIComponent(entityId)}`) .then((r) => r.json()) .then((j) => { if (!aborted) { setData(j); setLoading(false); } }) .catch(() => { if (!aborted) { setData({ outgoing: [], incoming: [] }); setLoading(false); } }); return () => { aborted = true; }; }, [entityClass, entityId]); if (loading) { return
carregando relações…
; } if (!data || (data.outgoing.length === 0 && data.incoming.length === 0)) { return (
sem relações tipadas extraídas para esta entidade.
); } // Group by relation_type for outgoing and incoming separately const groupOut: Record = {}; for (const r of data.outgoing) (groupOut[r.relation_type] ||= []).push(r); const groupIn: Record = {}; for (const r of data.incoming) (groupIn[r.relation_type] ||= []).push(r); const renderGroup = (type: string, list: Relation[], dir: "out" | "in") => { const label = TYPE_LABEL_PT[type]?.[dir] ?? type; return (
{label} · {list.length}
    {list.slice(0, 12).map((r, i) => { const otherClass = dir === "out" ? r.target_class : r.source_class; const otherId = dir === "out" ? r.target_id : r.source_id; return (
  • [{otherClass[0]}] {otherId}
  • ); })} {list.length > 12 && (
  • … +{list.length - 12}
  • )}
); }; return (
{Object.keys(groupOut).length > 0 && (

Relações desta entidade →

{Object.entries(groupOut).map(([t, list]) => renderGroup(t, list, "out"))}
)} {Object.keys(groupIn).length > 0 && (

← Entidades que apontam para esta

{Object.entries(groupIn).map(([t, list]) => renderGroup(t, list, "in"))}
)}
); }