muthuk1 commited on
Commit
92624a0
·
verified ·
1 Parent(s): 18b47fb

Add Tab 4: Graph Explorer with interactive SVG knowledge graph visualization

Browse files
web/src/components/tabs/GraphExplorer.tsx ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useMemo, useCallback } from "react";
4
+
5
+ interface GraphNode {
6
+ id: string;
7
+ label: string;
8
+ type: string;
9
+ x: number;
10
+ y: number;
11
+ }
12
+
13
+ interface GraphEdge {
14
+ source: string;
15
+ target: string;
16
+ label: string;
17
+ }
18
+
19
+ const TYPE_COLORS: Record<string, string> = {
20
+ PERSON: "#FF6B6B",
21
+ ORGANIZATION: "#4ECDC4",
22
+ LOCATION: "#45B7D1",
23
+ EVENT: "#FFA07A",
24
+ DATE: "#98D8C8",
25
+ CONCEPT: "#AED6F1",
26
+ WORK: "#F9E79F",
27
+ QUERY: "#FF6B00",
28
+ };
29
+
30
+ const DEMO_NODES: GraphNode[] = [
31
+ { id: "q", label: "Query", type: "QUERY", x: 400, y: 60 },
32
+ { id: "sd", label: "Scott Derrickson", type: "PERSON", x: 200, y: 180 },
33
+ { id: "ew", label: "Ed Wood", type: "PERSON", x: 600, y: 180 },
34
+ { id: "us", label: "United States", type: "LOCATION", x: 400, y: 300 },
35
+ { id: "denver", label: "Denver, CO", type: "LOCATION", x: 150, y: 320 },
36
+ { id: "pough", label: "Poughkeepsie, NY", type: "LOCATION", x: 650, y: 320 },
37
+ { id: "sinister", label: "Sinister (2012)", type: "WORK", x: 100, y: 200 },
38
+ { id: "planNine", label: "Plan 9 from Outer Space", type: "WORK", x: 700, y: 200 },
39
+ { id: "horror", label: "Horror Genre", type: "CONCEPT", x: 400, y: 420 },
40
+ ];
41
+
42
+ const DEMO_EDGES: GraphEdge[] = [
43
+ { source: "q", target: "sd", label: "FOUND" },
44
+ { source: "q", target: "ew", label: "FOUND" },
45
+ { source: "sd", target: "denver", label: "BORN_IN" },
46
+ { source: "ew", target: "pough", label: "BORN_IN" },
47
+ { source: "denver", target: "us", label: "LOCATED_IN" },
48
+ { source: "pough", target: "us", label: "LOCATED_IN" },
49
+ { source: "sd", target: "sinister", label: "DIRECTED" },
50
+ { source: "ew", target: "planNine", label: "DIRECTED" },
51
+ { source: "sinister", target: "horror", label: "GENRE" },
52
+ { source: "planNine", target: "horror", label: "GENRE" },
53
+ ];
54
+
55
+ export function GraphExplorer() {
56
+ const [query, setQuery] = useState("Were Scott Derrickson and Ed Wood of the same nationality?");
57
+ const [selectedNode, setSelectedNode] = useState<string | null>(null);
58
+ const [hops, setHops] = useState(2);
59
+ const [nodes] = useState(DEMO_NODES);
60
+ const [edges] = useState(DEMO_EDGES);
61
+
62
+ const nodeMap = useMemo(() => {
63
+ const map: Record<string, GraphNode> = {};
64
+ nodes.forEach((n) => { map[n.id] = n; });
65
+ return map;
66
+ }, [nodes]);
67
+
68
+ const selectedInfo = selectedNode ? nodeMap[selectedNode] : null;
69
+
70
+ return (
71
+ <div>
72
+ {/* Controls */}
73
+ <div className="card mb-6">
74
+ <div className="display-sm mb-4">Knowledge Graph Explorer</div>
75
+ <p className="body-sm mb-4" style={{ color: "#6c6a64" }}>
76
+ Visualize how GraphRAG traverses the knowledge graph to find answers.
77
+ Click on nodes to inspect entity details.
78
+ </p>
79
+ <div className="flex flex-col md:flex-row gap-4">
80
+ <input
81
+ className="input flex-1"
82
+ value={query}
83
+ onChange={(e) => setQuery(e.target.value)}
84
+ placeholder="Enter a question to explore…"
85
+ />
86
+ <div className="flex items-center gap-3">
87
+ <label className="caption whitespace-nowrap">
88
+ Hops: <strong>{hops}</strong>
89
+ <input type="range" min={1} max={4} step={1} value={hops}
90
+ onChange={(e) => setHops(+e.target.value)}
91
+ className="ml-2 w-20 accent-[#FF6B00]" />
92
+ </label>
93
+ <button className="btn btn-primary">🔍 Explore</button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
99
+ {/* Graph Visualization */}
100
+ <div className="card lg:col-span-2" style={{ padding: "16px", minHeight: "500px" }}>
101
+ <svg
102
+ viewBox="0 0 800 480"
103
+ style={{ width: "100%", height: "100%", minHeight: "460px" }}
104
+ >
105
+ {/* Edges */}
106
+ {edges.map((edge, i) => {
107
+ const s = nodeMap[edge.source];
108
+ const t = nodeMap[edge.target];
109
+ if (!s || !t) return null;
110
+ const mx = (s.x + t.x) / 2;
111
+ const my = (s.y + t.y) / 2;
112
+ return (
113
+ <g key={`edge-${i}`}>
114
+ <line
115
+ x1={s.x} y1={s.y} x2={t.x} y2={t.y}
116
+ stroke="#e6dfd8"
117
+ strokeWidth={selectedNode && (edge.source === selectedNode || edge.target === selectedNode) ? 2.5 : 1.5}
118
+ strokeOpacity={selectedNode && edge.source !== selectedNode && edge.target !== selectedNode ? 0.3 : 0.8}
119
+ />
120
+ <text x={mx} y={my - 6} textAnchor="middle" fontSize="9" fill="#8e8b82"
121
+ fontFamily="var(--font-mono)">
122
+ {edge.label}
123
+ </text>
124
+ </g>
125
+ );
126
+ })}
127
+
128
+ {/* Nodes */}
129
+ {nodes.map((node) => {
130
+ const color = TYPE_COLORS[node.type] || "#AED6F1";
131
+ const isSelected = selectedNode === node.id;
132
+ const r = isSelected ? 24 : 18;
133
+ return (
134
+ <g
135
+ key={node.id}
136
+ className="graph-node cursor-pointer"
137
+ onClick={() => setSelectedNode(isSelected ? null : node.id)}
138
+ >
139
+ {/* Glow on select */}
140
+ {isSelected && (
141
+ <circle cx={node.x} cy={node.y} r={r + 8} fill={color} opacity={0.15} />
142
+ )}
143
+ <circle
144
+ cx={node.x} cy={node.y} r={r}
145
+ fill={color}
146
+ stroke="white" strokeWidth="2.5"
147
+ opacity={selectedNode && !isSelected ? 0.5 : 1}
148
+ />
149
+ <text
150
+ x={node.x} y={node.y + r + 14}
151
+ textAnchor="middle"
152
+ fontSize={isSelected ? "12" : "10"}
153
+ fontWeight={isSelected ? "600" : "400"}
154
+ fill="#141413"
155
+ fontFamily="var(--font-sans)"
156
+ >
157
+ {node.label}
158
+ </text>
159
+ </g>
160
+ );
161
+ })}
162
+ </svg>
163
+ </div>
164
+
165
+ {/* Info Panel */}
166
+ <div className="flex flex-col gap-4">
167
+ {/* Node Details */}
168
+ <div className="card-dark">
169
+ <div className="caption-uppercase" style={{ color: "#a09d96" }}>Node Details</div>
170
+ {selectedInfo ? (
171
+ <div className="mt-3">
172
+ <div className="title-md" style={{ color: "#faf9f5" }}>{selectedInfo.label}</div>
173
+ <span className="badge mt-2" style={{
174
+ background: TYPE_COLORS[selectedInfo.type] || "#AED6F1",
175
+ color: "white",
176
+ fontSize: "0.6875rem",
177
+ }}>
178
+ {selectedInfo.type}
179
+ </span>
180
+ <div className="body-sm mt-3" style={{ color: "#a09d96" }}>
181
+ Connected to {edges.filter(
182
+ (e) => e.source === selectedInfo.id || e.target === selectedInfo.id
183
+ ).length} other nodes
184
+ </div>
185
+ <div className="mt-3">
186
+ <div className="caption" style={{ color: "#a09d96" }}>Connections:</div>
187
+ {edges
188
+ .filter((e) => e.source === selectedInfo.id || e.target === selectedInfo.id)
189
+ .map((e, i) => {
190
+ const other = e.source === selectedInfo.id ? e.target : e.source;
191
+ return (
192
+ <div key={i} className="body-sm mt-1" style={{ color: "#faf9f5", fontFamily: "var(--font-mono)", fontSize: "0.75rem" }}>
193
+ 🔗 {e.label} → {nodeMap[other]?.label || other}
194
+ </div>
195
+ );
196
+ })}
197
+ </div>
198
+ </div>
199
+ ) : (
200
+ <p className="body-sm mt-3" style={{ color: "#a09d96" }}>
201
+ Click a node on the graph to see its details.
202
+ </p>
203
+ )}
204
+ </div>
205
+
206
+ {/* Graph Stats */}
207
+ <div className="card-cream">
208
+ <div className="caption-uppercase mb-3">Graph Statistics</div>
209
+ {[
210
+ { label: "Nodes", value: nodes.length },
211
+ { label: "Edges", value: edges.length },
212
+ { label: "Avg Degree", value: (edges.length * 2 / nodes.length).toFixed(1) },
213
+ { label: "Entity Types", value: new Set(nodes.map((n) => n.type)).size },
214
+ { label: "Hops Traversed", value: hops },
215
+ ].map((s, i) => (
216
+ <div key={i} className="flex justify-between items-center py-2" style={{ borderBottom: i < 4 ? "1px solid var(--color-hairline-soft)" : "none" }}>
217
+ <span className="body-sm">{s.label}</span>
218
+ <span className="title-sm" style={{ fontFamily: "var(--font-mono)" }}>{s.value}</span>
219
+ </div>
220
+ ))}
221
+ </div>
222
+
223
+ {/* Legend */}
224
+ <div className="card" style={{ padding: "16px" }}>
225
+ <div className="caption-uppercase mb-2">Entity Types</div>
226
+ <div className="grid grid-cols-2 gap-2">
227
+ {Object.entries(TYPE_COLORS).map(([type, color]) => (
228
+ <div key={type} className="flex items-center gap-2">
229
+ <div className="w-3 h-3 rounded-full" style={{ background: color }} />
230
+ <span className="body-sm">{type}</span>
231
+ </div>
232
+ ))}
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ {/* Reasoning Explanation */}
239
+ <div className="card-cream mt-6">
240
+ <div className="title-md mb-3">🧠 Graph Reasoning Path</div>
241
+ <div className="prose">
242
+ <p>
243
+ <strong>1. Entry Points:</strong> The query identified two key entities —{" "}
244
+ <code>Scott Derrickson</code> and <code>Ed Wood</code>.
245
+ </p>
246
+ <p>
247
+ <strong>2. Traversal:</strong> Following BORN_IN relationships:
248
+ Scott Derrickson → Denver, CO → United States;
249
+ Ed Wood → Poughkeepsie, NY → United States.
250
+ </p>
251
+ <p>
252
+ <strong>3. Evidence:</strong> Both paths converge at <code>United States</code>,
253
+ confirming shared nationality through 2-hop graph traversal.
254
+ </p>
255
+ <p>
256
+ <strong>4. Conclusion:</strong> Yes — both directors are American.
257
+ The graph reasoning path provides explicit, traceable evidence for the answer.
258
+ </p>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ );
263
+ }