AK-21/Graphite-Industrial-Intelligence
0
1import React, { useState, useCallback } from 'react';2import { ReactFlow, Controls, Background, useNodesState, useEdgesState, addEdge, Handle, Position, BaseEdge, getBezierPath, EdgeLabelRenderer, useReactFlow } from '@xyflow/react';3import type { Node, Edge } from '@xyflow/react';4import { motion, AnimatePresence } from 'framer-motion';5import { Database, AlertTriangle, FileText, Activity, Server, Zap, Search, X, Loader2, Link as LinkIcon } from 'lucide-react';6import { trpc } from '../trpc';7import ReactMarkdown from 'react-markdown';8import '@xyflow/react/dist/style.css';9 10// 1. Define Custom Node Component11const CustomNode = ({ data, selected }: any) => {12 const isAsset = data.type === 'asset';13 const isAnomaly = data.type === 'anomaly';14 const isDoc = data.type === 'document';15 16 const baseClasses = "px-4 py-3 rounded-xl border-2 shadow-lg transition-all duration-300 min-w-[220px]";17 const selectedClasses = selected ? "ring-2 ring-white scale-105" : "";18 19 let colorClasses = "";20 let Icon = Database;21 22 if (isAsset) {23 colorClasses = "bg-green-900/40 border-green-500/50 text-green-300 shadow-[0_0_15px_rgba(16,185,129,0.2)]";24 Icon = Server;25 } else if (isAnomaly) {26 colorClasses = "bg-red-900/40 border-red-500/50 text-red-300 shadow-[0_0_20px_rgba(239,68,68,0.3)]";27 Icon = AlertTriangle;28 } else if (isDoc) {29 colorClasses = "bg-blue-900/40 border-blue-500/50 text-blue-300 shadow-[0_0_15px_rgba(59,130,246,0.2)]";30 Icon = FileText;31 }32 33 return (34 <div className={`${baseClasses} ${colorClasses} ${selectedClasses} backdrop-blur-md`}>35 <Handle type="target" position={Position.Top} className="!bg-gray-500 !w-3 !h-3" />36 <div className="flex items-center space-x-3">37 <div className={`p-2 rounded-lg ${isAsset ? 'bg-green-500/20' : isAnomaly ? 'bg-red-500/20' : 'bg-blue-500/20'}`}>38 <Icon className="w-5 h-5" />39 </div>40 <div>41 <div className="text-xs font-bold uppercase tracking-wider opacity-70 mb-0.5">{data.type}</div>42 <div className="font-mono font-bold text-sm text-white">{data.label}</div>43 </div>44 </div>45 <Handle type="source" position={Position.Bottom} className="!bg-gray-500 !w-3 !h-3" />46 </div>47 );48};49 50const nodeTypes = { custom: CustomNode };51 52const CustomEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, markerEnd }: any) => {53 const { setEdges } = useReactFlow();54 const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });55 return (56 <>57 <BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />58 <EdgeLabelRenderer>59 <div style={{ position: 'absolute', transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, pointerEvents: 'all' }} className="nodrag nopan">60 <button className="w-5 h-5 bg-gray-900 text-gray-400 hover:text-white hover:bg-red-500/80 border border-gray-700 rounded-full flex items-center justify-center text-xs transition-all shadow-lg" onClick={(event) => { event.stopPropagation(); setEdges((es) => es.filter((e) => e.id !== id)); }} title="Remove Connection">61 <X className="w-3 h-3" />62 </button>63 </div>64 </EdgeLabelRenderer>65 </>66 );67};68const edgeTypes = { custom: CustomEdge };69 70// 2. Initial Data71const initialNodes: Node[] = [72 { id: '1', type: 'custom', position: { x: 400, y: 100 }, data: { label: 'Turbine Generator A', type: 'asset', details: 'Main power turbine in Sector 4. Installed 2019.', status: 'Warning' } },73 { id: '2', type: 'custom', position: { x: 150, y: 300 }, data: { label: 'High Vibration Alert', type: 'anomaly', details: 'Vibration exceeded 15mm/s threshold at bearing housing.', severity: 'Critical' } },74 { id: '3', type: 'custom', position: { x: 650, y: 300 }, data: { label: 'Turbine_Manual_v2.pdf', type: 'document', details: 'OEM Maintenance manual containing emergency procedures.', relevance: '98%' } },75 { id: '4', type: 'custom', position: { x: 300, y: 500 }, data: { label: 'Bearing Assembly', type: 'asset', details: 'Sub-component of Turbine Generator A', status: 'Degraded' } },76 { id: '5', type: 'custom', position: { x: 650, y: 500 }, data: { label: 'Past_Incident_Report.txt', type: 'document', details: 'Report from 2023 detailing similar vibration issues.', relevance: '85%' } },77];78 79const initialEdges: Edge[] = [80 { id: 'e1-2', type: 'custom', source: '1', target: '2', label: 'HAS_ANOMALY', animated: true, style: { stroke: '#ef4444', strokeWidth: 2 } },81 { id: 'e1-3', type: 'custom', source: '1', target: '3', label: 'REFERENCED_BY', style: { stroke: '#60a5fa', strokeWidth: 2, opacity: 0.6 } },82 { id: 'e1-4', type: 'custom', source: '1', target: '4', label: 'CONTAINS', style: { stroke: '#34d399', strokeWidth: 2, opacity: 0.6 } },83 { id: 'e2-4', type: 'custom', source: '2', target: '4', label: 'LOCATED_AT', animated: true, style: { stroke: '#ef4444', strokeWidth: 2 } },84 { id: 'e3-5', type: 'custom', source: '3', target: '5', label: 'SIMILAR_TO', style: { stroke: '#60a5fa', strokeWidth: 2, strokeDasharray: '5 5' } },85];86 87export default function KnowledgeGraph() {88 const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);89 const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);90 const [selectedNode, setSelectedNode] = useState<any>(null);91 const [analysisResult, setAnalysisResult] = useState<string | null>(null);92 const [connectingNodeId, setConnectingNodeId] = useState<string | null>(null);93 94 const analyzeMutation = trpc.qa.analyzeNode.useMutation({95 onSuccess: (data) => {96 setAnalysisResult(data.result);97 }98 });99 100 const onConnect = useCallback((params: any) => setEdges((eds) => addEdge({ ...params, type: 'custom' }, eds)), [setEdges]);101 102 const onNodeClick = (event: React.MouseEvent, node: Node) => {103 if (connectingNodeId && connectingNodeId !== node.id) {104 const newEdge: Edge = {105 id: `e${connectingNodeId}-${node.id}`,106 type: 'custom',107 source: connectingNodeId,108 target: node.id,109 label: 'USER_LINKED',110 style: { stroke: '#a855f7', strokeWidth: 2, strokeDasharray: '5 5' }111 };112 setEdges((eds) => addEdge(newEdge, eds));113 setConnectingNodeId(null);114 return;115 }116 117 setSelectedNode(node);118 setAnalysisResult(null); // clear previous analysis when clicking a new node119 };120 121 const handlePaneClick = () => {122 setSelectedNode(null);123 setAnalysisResult(null);124 setConnectingNodeId(null);125 };126 127 const runAnalysis = (action: 'root_cause' | 'summarize') => {128 if (!selectedNode) return;129 setAnalysisResult(null);130 analyzeMutation.mutate({131 nodeId: selectedNode.id,132 nodeLabel: selectedNode.data.label,133 nodeType: selectedNode.data.type,134 action135 });136 };137 138 const onEdgeClick = (event: React.MouseEvent, edge: Edge) => {139 event.stopPropagation();140 setEdges((eds) => eds.filter((e) => e.id !== edge.id));141 };142 143 return (144 <div className="flex flex-col h-[calc(100vh-120px)] relative">145 <div className="flex justify-between items-center bg-gray-900/40 p-5 rounded-t-2xl border-x border-t border-gray-800 backdrop-blur-sm z-10 relative">146 <div className="flex items-center space-x-3">147 <div className="p-2.5 bg-accent/20 rounded-lg border border-accent/30 shadow-[0_0_15px_rgba(6,182,212,0.2)]">148 <Search className="w-6 h-6 text-accent" />149 </div>150 <div>151 <h1 className="text-2xl font-mono font-bold text-white">Interactive Knowledge Graph</h1>152 <p className="text-gray-400 text-xs">Explore relationships between Assets, Documents, and Anomalies.</p>153 </div>154 </div>155 <div className="text-sm text-gray-500 font-mono">156 Nodes: {nodes.length} | Edges: {edges.length}157 </div>158 </div>159 160 <div className="flex-1 bg-gray-950/80 rounded-b-2xl border-x border-b border-gray-800 relative overflow-hidden">161 162 {connectingNodeId && (163 <div className="absolute top-4 left-1/2 -translate-x-1/2 bg-purple-600 text-white px-6 py-2 rounded-full shadow-[0_0_20px_rgba(147,51,234,0.5)] z-20 flex items-center font-bold text-sm animate-pulse">164 <LinkIcon className="w-4 h-4 mr-2" /> Select target node to connect...165 <button onClick={() => setConnectingNodeId(null)} className="ml-4 hover:text-gray-300">166 <X className="w-4 h-4" />167 </button>168 </div>169 )}170 <ReactFlow171 nodes={nodes}172 edges={edges}173 onNodesChange={onNodesChange}174 onEdgesChange={onEdgesChange}175 onConnect={onConnect}176 onNodeClick={onNodeClick}177 onEdgeClick={onEdgeClick}178 onPaneClick={handlePaneClick}179 nodeTypes={nodeTypes}180 edgeTypes={edgeTypes}181 fitView182 className="bg-grid-pattern"183 proOptions={{ hideAttribution: true }}184 >185 <Background color="#1f2937" gap={20} size={1.5} />186 <Controls className="bg-gray-900 border-gray-800 fill-white" />187 </ReactFlow>188 189 {/* Slide-out Details Panel */}190 <AnimatePresence>191 {selectedNode && (192 <motion.div193 initial={{ x: '100%', opacity: 0 }}194 animate={{ x: 0, opacity: 1 }}195 exit={{ x: '100%', opacity: 0 }}196 transition={{ type: 'spring', damping: 25, stiffness: 200 }}197 className="absolute top-4 right-4 bottom-4 w-96 bg-gray-900/95 backdrop-blur-xl border border-gray-700 rounded-2xl shadow-2xl p-6 flex flex-col z-20"198 >199 <div className="flex justify-between items-start mb-6">200 <div>201 <span className="px-2.5 py-1 rounded-md text-xs font-bold uppercase tracking-wider bg-gray-800 text-gray-400 border border-gray-700">202 {selectedNode.data.type} Node203 </span>204 <h2 className="text-xl font-bold text-white mt-3 font-mono">{selectedNode.data.label}</h2>205 </div>206 <button onClick={handlePaneClick} className="p-1 hover:bg-gray-800 rounded-lg text-gray-400 hover:text-white transition-colors">207 <X className="w-5 h-5" />208 </button>209 </div>210 211 <div className="space-y-6 flex-1 overflow-y-auto pr-2 custom-scrollbar pb-6">212 <div>213 <h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Description</h3>214 <p className="text-gray-300 text-sm leading-relaxed">{selectedNode.data.details}</p>215 </div>216 217 <div className="bg-gray-800/50 p-4 rounded-xl border border-gray-700/50">218 <h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">Properties</h3>219 220 {selectedNode.data.status && (221 <div className="flex justify-between items-center mb-2">222 <span className="text-sm text-gray-400">Health Status</span>223 <span className={`text-sm font-bold ${selectedNode.data.status === 'Warning' ? 'text-amber-400' : 'text-red-400'}`}>224 {selectedNode.data.status}225 </span>226 </div>227 )}228 {selectedNode.data.severity && (229 <div className="flex justify-between items-center mb-2">230 <span className="text-sm text-gray-400">Severity</span>231 <span className="text-sm font-bold text-red-400">{selectedNode.data.severity}</span>232 </div>233 )}234 {selectedNode.data.relevance && (235 <div className="flex justify-between items-center mb-2">236 <span className="text-sm text-gray-400">AI Relevance</span>237 <span className="text-sm font-bold text-blue-400">{selectedNode.data.relevance}</span>238 </div>239 )}240 <div className="flex justify-between items-center">241 <span className="text-sm text-gray-400">Node ID</span>242 <span className="text-sm font-mono text-gray-500">{selectedNode.id}</span>243 </div>244 </div>245 246 <div>247 <h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">AI Analysis Actions</h3>248 <div className="space-y-2">249 <button 250 onClick={() => runAnalysis('root_cause')}251 disabled={analyzeMutation.isPending}252 className="w-full py-2.5 px-4 bg-accent/10 hover:bg-accent/20 border border-accent/30 text-accent rounded-lg text-sm font-medium transition-colors flex items-center justify-center disabled:opacity-50"253 >254 <Zap className="w-4 h-4 mr-2" /> Find Root Cause255 </button>256 <button 257 onClick={() => runAnalysis('summarize')}258 disabled={analyzeMutation.isPending}259 className="w-full py-2.5 px-4 bg-gray-800 hover:bg-gray-700 border border-gray-700 text-gray-300 rounded-lg text-sm font-medium transition-colors flex items-center justify-center disabled:opacity-50"260 >261 <FileText className="w-4 h-4 mr-2" /> Summarize Context262 </button>263 <button 264 onClick={() => {265 setConnectingNodeId(selectedNode.id);266 setSelectedNode(null);267 }}268 className="w-full py-2.5 px-4 bg-purple-900/30 hover:bg-purple-900/50 border border-purple-500/30 text-purple-400 rounded-lg text-sm font-medium transition-colors flex items-center justify-center mt-2"269 >270 <LinkIcon className="w-4 h-4 mr-2" /> Link to another node271 </button>272 </div>273 </div>274 275 {/* AI Analysis Result Section */}276 <AnimatePresence>277 {(analyzeMutation.isPending || analysisResult) && (278 <motion.div 279 initial={{ opacity: 0, height: 0 }}280 animate={{ opacity: 1, height: 'auto' }}281 exit={{ opacity: 0, height: 0 }}282 className="mt-6 border-t border-gray-800 pt-6"283 >284 <h3 className="text-xs font-bold text-accent uppercase tracking-wider mb-4 flex items-center">285 <Activity className="w-4 h-4 mr-2" />286 AI Analysis Output287 </h3>288 289 {analyzeMutation.isPending ? (290 <div className="flex flex-col items-center justify-center py-6 space-y-4">291 <Loader2 className="w-8 h-8 text-accent animate-spin" />292 <span className="text-sm text-gray-400 animate-pulse">Running Deep Analysis...</span>293 </div>294 ) : (295 <div className="prose prose-invert prose-sm prose-p:text-gray-300 prose-strong:text-white bg-gray-950 p-4 rounded-xl border border-gray-800 shadow-inner">296 <ReactMarkdown>{analysisResult || ''}</ReactMarkdown>297 </div>298 )}299 </motion.div>300 )}301 </AnimatePresence>302 303 </div>304 </motion.div>305 )}306 </AnimatePresence>307 </div>308 </div>309 );310}311 