Download frontend/src/components/GraphView.tsx from AtPeak/cc-rag: direct link, hf CLI and curl.
- Browser
- Download file 7.05 kB
-
https://huggingface.co/spaces/AtPeak/cc-rag/resolve/main/frontend/src/components/GraphView.tsx
- Command line
-
hf download hf://spaces/AtPeak/cc-rag/frontend/src/components/GraphView.tsx
-
curl -L -o GraphView.tsx https://huggingface.co/spaces/AtPeak/cc-rag/resolve/main/frontend/src/components/GraphView.tsx
7.05 kB
| 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<string, string> = { | |
| 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<GraphData | null>(null) | |
| const [loading, setLoading] = useState(true) | |
| const [error, setError] = useState<string | null>(null) | |
| const [selectedEdge, setSelectedEdge] = useState<GraphEdge | null>(null) | |
| const [selectedNode, setSelectedNode] = useState<string | null>(null) | |
| const [themeFilter, setThemeFilter] = useState('') | |
| const [cyRef, setCyRef] = useState<any>(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 <div className="spinner" /> | |
| if (error) return <div className="error-msg">{error}</div> | |
| if (!data || data.nodes.length === 0) return ( | |
| <div className="empty-state"><p>No graph data. Build graph first.</p></div> | |
| ) | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 12 }}> | |
| <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}> | |
| {themes.length > 0 && ( | |
| <div> | |
| <select value={themeFilter} onChange={e => setThemeFilter(e.target.value)} style={{ width: 160 }}> | |
| <option value="">All themes</option> | |
| {themes.map(t => <option key={t} value={t}>{t}</option>)} | |
| </select> | |
| </div> | |
| )} | |
| <button className="btn-secondary" onClick={load} style={{ fontSize: 12 }}>Reload</button> | |
| <button className="btn-secondary" onClick={() => cyRef?.fit()} style={{ fontSize: 12 }}>Fit</button> | |
| <span style={{ fontSize: 12, color: 'var(--muted)' }}> | |
| {data.nodes.length} nodes · {data.edges.length} edges | |
| </span> | |
| </div> | |
| <div style={{ display: 'flex', gap: 12, flex: 1, minHeight: 0 }}> | |
| <div style={{ | |
| flex: 1, background: 'var(--bg2)', border: '1px solid var(--border)', | |
| borderRadius: 'var(--radius)', overflow: 'hidden', | |
| }}> | |
| <CytoscapeComponent | |
| elements={elements} | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| stylesheet={stylesheet as any} | |
| layout={{ name: 'cose', idealEdgeLength: 120, animate: false }} | |
| style={{ width: '100%', height: '100%', minHeight: 400 }} | |
| cy={(cy: any) => { | |
| 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) } | |
| }) | |
| }} | |
| /> | |
| </div> | |
| {(selectedEdge || selectedNode) && ( | |
| <div className="card" style={{ width: 260, flexShrink: 0, fontSize: 13, overflowY: 'auto' }}> | |
| {selectedNode && ( | |
| <> | |
| <div className="label">Node</div> | |
| <div style={{ fontWeight: 600, marginBottom: 8 }}>{selectedNode}</div> | |
| </> | |
| )} | |
| {selectedEdge && ( | |
| <> | |
| <div className="label">Edge</div> | |
| <div style={{ marginBottom: 8 }}> | |
| <span style={{ color: '#f87171' }}>{selectedEdge.source}</span> | |
| <span style={{ color: 'var(--muted)', margin: '0 6px' }}>→</span> | |
| <span style={{ color: '#60a5fa' }}>{selectedEdge.target}</span> | |
| </div> | |
| <div style={{ marginBottom: 4 }}> | |
| <span className={`badge ${selectedEdge.label}`}>{selectedEdge.label}</span> | |
| </div> | |
| <div style={{ color: 'var(--muted)', marginBottom: 8 }}> | |
| Confidence: <strong>{selectedEdge.confidence.toFixed(2)}</strong> | |
| </div> | |
| {selectedEdge.evidence && ( | |
| <> | |
| <div className="label">Evidence</div> | |
| <div style={{ color: 'var(--muted)', fontStyle: 'italic' }}>"{selectedEdge.evidence}"</div> | |
| </> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |