CoolFace
Apppublic

AK-21/Graphite-Industrial-Intelligence

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
KnowledgeGraph.tsx311 linesDownload Raw Back to pages
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