import { useState, useEffect, useCallback } from 'react' import CytoscapeComponent from 'react-cytoscapejs' import type { ElementDefinition } from 'cytoscape' import { api } from '../api' import type { GraphData, GraphEdge } from '../types' interface Props { documentId: string } const THEME_COLORS: Record = { supply_chain: '#6366f1', finance: '#10b981', operations: '#f59e0b', general: '#8892a4', } function nodeColor(theme: string) { return THEME_COLORS[theme] || '#6366f1' } export function GraphView({ documentId }: Props) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedEdge, setSelectedEdge] = useState(null) const [selectedNode, setSelectedNode] = useState(null) const [themeFilter, setThemeFilter] = useState('') const [cyRef, setCyRef] = useState(null) const load = useCallback(() => { setLoading(true) api.getGraph(documentId) .then(setData) .catch(e => setError(String(e.message))) .finally(() => setLoading(false)) }, [documentId]) useEffect(() => { load() }, [load]) const themes = data ? [...new Set(data.edges.map(e => e.theme))].filter(Boolean) : [] const elements: ElementDefinition[] = data ? [ ...data.nodes .filter(n => !themeFilter || data.edges.some(e => (e.source === n.id || e.target === n.id) && e.theme === themeFilter)) .map(n => ({ data: { id: n.id, label: n.label, theme: n.theme }, classes: 'event-node', })), ...data.edges .filter(e => !themeFilter || e.theme === themeFilter) .map(e => ({ data: { id: e.id, source: e.source, target: e.target, label: `${e.label} (${e.confidence.toFixed(2)})`, confidence: e.confidence, evidence: e.evidence, theme: e.theme, edgeData: e, }, classes: 'causal-edge', })), ] : [] // eslint-disable-next-line @typescript-eslint/no-explicit-any const stylesheet: any[] = [ { selector: 'node', style: { label: 'data(label)', 'background-color': (ele: any) => nodeColor(ele.data('theme') || 'general'), color: '#fff', 'font-size': 11, 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'wrap', 'text-max-width': 100, width: 'label', height: 'label', padding: 10, shape: 'round-rectangle', 'border-width': 2, 'border-color': 'transparent', }, }, { selector: 'node:selected', style: { 'border-color': '#fff', 'border-width': 2 }, }, { selector: 'edge', style: { 'line-color': '#4f5880', 'target-arrow-color': '#4f5880', 'target-arrow-shape': 'triangle', 'curve-style': 'bezier', label: 'data(label)', 'font-size': 9, color: '#8892a4', 'text-rotation': 'autorotate', width: (ele: any) => Math.max(1, ele.data('confidence') * 3), }, }, { selector: 'edge:selected', style: { 'line-color': '#6366f1', 'target-arrow-color': '#6366f1' }, }, ] if (loading) return
if (error) return
{error}
if (!data || data.nodes.length === 0) return (

No graph data. Build graph first.

) return (
{themes.length > 0 && (
)} {data.nodes.length} nodes · {data.edges.length} edges
{ setCyRef(cy) cy.on('tap', 'edge', (evt: any) => { setSelectedEdge(evt.target.data('edgeData')) setSelectedNode(null) }) cy.on('tap', 'node', (evt: any) => { setSelectedNode(evt.target.data('label')) setSelectedEdge(null) }) cy.on('tap', (evt: any) => { if (evt.target === cy) { setSelectedEdge(null); setSelectedNode(null) } }) }} />
{(selectedEdge || selectedNode) && (
{selectedNode && ( <>
Node
{selectedNode}
)} {selectedEdge && ( <>
Edge
{selectedEdge.source} {selectedEdge.target}
{selectedEdge.label}
Confidence: {selectedEdge.confidence.toFixed(2)}
{selectedEdge.evidence && ( <>
Evidence
"{selectedEdge.evidence}"
)} )}
)}
) }