Spaces:
Sleeping
Sleeping
File size: 3,714 Bytes
3786a3f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 'use client';
import { useEffect, useState } from 'react';
import { X, MessageSquare, ExternalLink } from 'lucide-react';
import { useGraphStore, GraphNode } from '@/store/graph';
import { entityColor } from '@/lib/constants';
import { Badge } from '@/components/ui/badge';
interface EntityDetail {
description?: string;
sourceDoc?: string;
relationships: { target: string; type: string; confidence?: number }[];
}
export function EntityPanel({
onAsk,
}: {
onAsk?: (name: string) => void;
}) {
const selected = useGraphStore((s) => s.selectedEntity);
const close = () => useGraphStore.getState().selectEntity(null);
const [detail, setDetail] = useState<EntityDetail | null>(null);
useEffect(() => {
if (!selected) {
setDetail(null);
return;
}
// Build relationships by scanning graph links
const { data } = useGraphStore.getState();
const rels = data.links
.filter((l) => l.source === selected.id || l.target === selected.id)
.map((l) => ({
target: l.source === selected.id ? l.target : l.source,
type: l.type,
confidence: l.confidence,
}));
setDetail({
description: selected.description,
sourceDoc: selected.sourceDoc,
relationships: rels,
});
}, [selected]);
if (!selected) return null;
return (
<div className="absolute inset-y-0 right-0 z-30 flex w-full max-w-sm animate-slide-in flex-col border-l border-border bg-bg-surface shadow-2xl">
<div className="flex items-center justify-between border-b border-border p-4">
<div className="flex items-center gap-2">
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: entityColor(selected.type) }}
/>
<span className="font-semibold">{selected.name}</span>
</div>
<button onClick={close} aria-label="Close" className="text-text-muted hover:text-text-primary">
<X className="h-5 w-5" />
</button>
</div>
<div className="flex-1 space-y-4 overflow-y-auto p-4 scrollbar-thin">
<div>
<Badge variant="info">{selected.type}</Badge>
</div>
{detail?.description && (
<p className="text-sm text-text-secondary">{detail.description}</p>
)}
{detail?.sourceDoc && (
<div className="flex items-center gap-1 text-xs text-text-muted">
<ExternalLink className="h-3.5 w-3.5" /> Source: {detail.sourceDoc}
</div>
)}
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted">
Relationships ({detail?.relationships.length || 0})
</p>
<ul className="space-y-1.5">
{detail?.relationships.map((r, i) => (
<li
key={i}
className="flex items-center justify-between rounded-md border border-border bg-bg-base px-3 py-2 text-sm"
>
<span className="font-medium text-accent-cyan">
{r.type.replace(/_/g, ' ').toLowerCase()}
</span>
<span className="text-text-secondary">{r.target}</span>
</li>
))}
</ul>
</div>
</div>
{onAsk && (
<div className="border-t border-border p-4">
<button
onClick={() => onAsk(selected.name)}
className="flex w-full items-center justify-center gap-2 rounded-md bg-accent-violet px-4 py-2 text-sm font-medium text-white hover:bg-accent-violet/90"
>
<MessageSquare className="h-4 w-4" /> Ask about this entity
</button>
</div>
)}
</div>
);
}
|