File size: 4,579 Bytes
d91cbff | 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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 'use client';
import { useEffect, useRef, useState } from 'react';
import { ForceGraph } from './ForceGraph';
import { GraphFilterSidebar } from './GraphFilterSidebar';
import { SectionDrawer } from './SectionDrawer';
import type { UseGraphExplorerReturn } from '@/hooks/useGraphExplorer';
interface Props extends UseGraphExplorerReturn {
onChatAboutSection: (
sectionId: string,
title: string,
docName: string,
jurisdiction: string,
) => void;
}
export function GraphExplorer({
topology,
isLoading,
error,
selectedNode,
hoveredNode,
sectionContent,
isSectionLoading,
filters,
setSelectedNode,
setHoveredNode,
setFilters,
navigateToNode,
onChatAboutSection,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [dims, setDims] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!containerRef.current) {
return;
}
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
setDims({ width: Math.floor(width), height: Math.floor(height) });
}
});
ro.observe(containerRef.current);
return () => ro.disconnect();
}, []);
const nodes = topology?.nodes ?? [];
const edges = topology?.edges ?? [];
return (
<div className="flex h-full min-h-0 flex-col bg-[#0d0d0d]">
<div className="flex h-12 shrink-0 items-center justify-between border-b border-white/[0.06] px-4">
<div>
<p className="font-mono text-[10px] uppercase tracking-[0.24em] text-white/30">Graph Explorer</p>
<p className="mt-0.5 text-[12px] text-white/40">
{nodes.length} sections / {edges.length} relationships
</p>
</div>
<div className="flex items-center gap-2 font-mono text-[10px] uppercase tracking-[0.2em] text-white/30">
<span className="h-1.5 w-1.5 rounded-full bg-[#4f98a3]" />
Live topology
</div>
</div>
<div ref={containerRef} className="relative min-h-0 flex-1 overflow-hidden bg-[#0d0d0d]">
<div className="pointer-events-none absolute inset-0 opacity-[0.14] [background-image:radial-gradient(circle_at_center,rgba(229,226,225,0.72)_1px,transparent_1.5px)] [background-position:0_0] [background-size:24px_24px]" />
{isLoading ? (
<div className="absolute inset-0 z-10 flex items-center justify-center">
<div className="flex items-center gap-2 font-mono text-[10px] uppercase tracking-[0.24em] text-white/30">
{[0, 1, 2].map(i => (
<span
key={i}
className="h-1.5 w-1.5 rounded-full bg-white/25 animate-pulse"
style={{ animationDelay: `${i * 140}ms` }}
/>
))}
Loading graph
</div>
</div>
) : null}
{error ? (
<div className="absolute inset-0 z-10 flex items-center justify-center">
<div className="max-w-xs border border-red-300/15 bg-red-950/10 p-4 text-center">
<p className="text-sm text-red-200/80">Graph unavailable</p>
<p className="mt-1 text-xs leading-5 text-white/30">{error}</p>
</div>
</div>
) : null}
{!isLoading && !error && nodes.length === 0 ? (
<div className="absolute inset-0 z-10 flex items-center justify-center">
<div className="border border-white/[0.06] bg-[#141414] p-5 text-center">
<p className="text-sm text-white/60">No section relationships found</p>
<p className="mt-1 text-xs text-white/30">Run ingestion to populate the graph.</p>
</div>
</div>
) : null}
{dims.width > 0 && dims.height > 0 && nodes.length > 0 ? (
<ForceGraph
nodes={nodes}
edges={edges}
filters={filters}
selectedNode={selectedNode}
hoveredNode={hoveredNode}
onNodeClick={node => setSelectedNode(node)}
onNodeHover={setHoveredNode}
width={dims.width}
height={dims.height}
/>
) : null}
<GraphFilterSidebar filters={filters} onFiltersChange={setFilters} topology={topology} />
<SectionDrawer
content={sectionContent}
isLoading={isSectionLoading}
onClose={() => setSelectedNode(null)}
onNodeNavigate={navigateToNode}
onChatAboutSection={onChatAboutSection}
/>
</div>
</div>
);
}
|