88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* EntityGraphMini — sidebar widget with co-mentioned entities (neighbors)
|
|||
|
|
* and a sample of chunks where they co-occur. Lives next to the entity body.
|
|||
|
|
*
|
|||
|
|
* Gracefully degrades to empty state when entity_mentions is unpopulated
|
|||
|
|
* or the DB is unreachable.
|
|||
|
|
*/
|
|||
|
|
import Link from "next/link";
|
|||
|
|
import { findEntity, getNeighbors } from "@/lib/retrieval/graph";
|
|||
|
|
|
|||
|
|
const CLASS_COLOR: Record<string, string> = {
|
|||
|
|
person: "text-[#ff6ec7] border-[#ff6ec7]",
|
|||
|
|
organization: "text-[#ff8a4d] border-[#ff8a4d]",
|
|||
|
|
location: "text-[#3fde6a] border-[#3fde6a]",
|
|||
|
|
event: "text-[#ffa500] border-[#ffa500]",
|
|||
|
|
uap_object: "text-[#ff3344] border-[#ff3344]",
|
|||
|
|
vehicle: "text-[#5b9bd5] border-[#5b9bd5]",
|
|||
|
|
operation: "text-[#9b5de5] border-[#9b5de5]",
|
|||
|
|
concept: "text-[#06d6a0] border-[#06d6a0]",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const CLASS_FOLDER: Record<string, string> = {
|
|||
|
|
person: "people",
|
|||
|
|
organization: "organizations",
|
|||
|
|
location: "locations",
|
|||
|
|
event: "events",
|
|||
|
|
uap_object: "uap-objects",
|
|||
|
|
vehicle: "vehicles",
|
|||
|
|
operation: "operations",
|
|||
|
|
concept: "concepts",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export async function EntityGraphMini({
|
|||
|
|
entityClassSingular,
|
|||
|
|
entityId,
|
|||
|
|
}: {
|
|||
|
|
entityClassSingular: string;
|
|||
|
|
entityId: string;
|
|||
|
|
}) {
|
|||
|
|
let neighbors: Awaited<ReturnType<typeof getNeighbors>> = [];
|
|||
|
|
let totalMentions = 0;
|
|||
|
|
try {
|
|||
|
|
const ent = await findEntity(entityClassSingular, entityId);
|
|||
|
|
if (ent) {
|
|||
|
|
totalMentions = ent.total_mentions ?? 0;
|
|||
|
|
neighbors = await getNeighbors(ent.entity_pk, { limit: 25 });
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (neighbors.length === 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<section className="mt-6 border-t border-[rgba(0,255,156,0.12)] pt-4">
|
|||
|
|
<h3 className="font-mono text-[10px] text-[#8896aa] uppercase tracking-widest mb-3">
|
|||
|
|
🕸 co-mentioned · top {neighbors.length} via {totalMentions} mentions
|
|||
|
|
</h3>
|
|||
|
|
<ul className="space-y-1.5">
|
|||
|
|
{neighbors.map((n) => {
|
|||
|
|
const folder = CLASS_FOLDER[n.entity_class] ?? n.entity_class;
|
|||
|
|
const color = CLASS_COLOR[n.entity_class] ?? "text-[#7fdbff] border-[#7fdbff]";
|
|||
|
|
return (
|
|||
|
|
<li key={n.entity_pk}>
|
|||
|
|
<Link
|
|||
|
|
href={`/e/${folder}/${n.entity_id}`}
|
|||
|
|
className="group flex items-center gap-2 text-xs hover:bg-[rgba(0,255,156,0.04)] rounded px-2 py-1 -mx-2"
|
|||
|
|
>
|
|||
|
|
<span className={`font-mono text-[9px] px-1.5 py-0.5 border rounded ${color} opacity-70 group-hover:opacity-100`}>
|
|||
|
|
{n.entity_class.slice(0, 3)}
|
|||
|
|
</span>
|
|||
|
|
<span className="flex-1 truncate text-[#c8d4e6] group-hover:text-[#00ff9c]">
|
|||
|
|
{n.canonical_name}
|
|||
|
|
</span>
|
|||
|
|
<span className="font-mono text-[10px] text-[#5a6678]">
|
|||
|
|
×{n.weight}
|
|||
|
|
</span>
|
|||
|
|
</Link>
|
|||
|
|
</li>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</ul>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|