Spaces:
Running
Running
| import React, { | |
| useEffect, | |
| useState, | |
| useCallback, | |
| useMemo, | |
| useRef, | |
| } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { ZoomIn, ZoomOut, RotateCcw } from "lucide-react"; | |
| import { KnowledgeGraph, OptimizationRecommendation } from "@/types"; | |
| import { | |
| UniversalGraphData, | |
| UniversalNode, | |
| UniversalLink, | |
| } from "@/types/graph-visualization"; | |
| import { getGraphDataAdapter } from "@/lib/graph-data-adapters"; | |
| import { createKnowledgeGraphConfig } from "@/lib/graph-config-factory"; | |
| import { | |
| SchemaCapabilities, | |
| detectSchemaType, | |
| getSchemaCapabilities, | |
| } from "@/lib/schema-detection"; | |
| import { CytoscapeGraphCore } from "@/lib/cytoscape-graph-core"; | |
| import { GraphSelectionCallbacks } from "@/types/graph-visualization"; | |
| import { ElementInfoSidebar } from "@/components/features/temporal/ElementInfoSidebar"; | |
| import { GraphLegend } from "@/components/shared/GraphLegend"; | |
| import { useAgentGraph } from "@/context/AgentGraphContext"; | |
| import { api } from "@/lib/api"; | |
| interface KnowledgeGraphVisualizerProps { | |
| knowledgeGraph: KnowledgeGraph; | |
| } | |
| export const KnowledgeGraphVisualizer: React.FC< | |
| KnowledgeGraphVisualizerProps | |
| > = ({ knowledgeGraph: kg }) => { | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| const cytoscapeRef = useRef<CytoscapeGraphCore | null>(null); | |
| const [graphData, setGraphData] = useState<UniversalGraphData>({ | |
| nodes: [], | |
| links: [], | |
| }); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [containerDimensions, setContainerDimensions] = useState({ | |
| width: 800, | |
| height: 600, | |
| }); | |
| // Failures extracted from graph data for system-level overview | |
| const [failures, setFailures] = useState< | |
| { | |
| id: string; | |
| risk_type: string; | |
| description: string; | |
| }[] | |
| >([]); | |
| const [optimizations, setOptimizations] = useState< | |
| OptimizationRecommendation[] | |
| >([]); | |
| // Selection state | |
| const [selectedElement, setSelectedElement] = useState< | |
| UniversalNode | UniversalLink | null | |
| >(null); | |
| const [selectedElementType, setSelectedElementType] = useState< | |
| "node" | "link" | "failure" | null | |
| >(null); | |
| // Trace viewer state | |
| const { state: agState } = useAgentGraph(); | |
| const [traceLines, setTraceLines] = useState<string[]>([]); | |
| const [highlightRanges, setHighlightRanges] = useState< | |
| { start: number; end: number }[] | |
| >([]); | |
| // Schema capabilities state | |
| const [schemaCapabilities, setSchemaCapabilities] = | |
| useState<SchemaCapabilities>({ | |
| supportsLineReferences: false, | |
| supportsFailures: false, | |
| supportsImportanceLevels: false, | |
| supportsInteractionPrompts: false, | |
| }); | |
| const dataAdapter = useMemo(() => getGraphDataAdapter(), []); | |
| // Helper function to find affected element by ID with smart matching | |
| const findAffectedElement = useCallback( | |
| (affectedId: string, graphData: UniversalGraphData) => { | |
| // Strategy 1: Try exact ID match first | |
| let affectedElement = | |
| graphData.nodes.find((n) => n.id === affectedId) || | |
| graphData.links.find((l) => l.id === affectedId); | |
| // Strategy 2: If not found, try smart matching based on ID pattern | |
| if (!affectedElement) { | |
| const match = affectedId.match( | |
| /^(task|tool|agent|input|output|human)_(\d+)$/i | |
| ); | |
| if (match) { | |
| const [, type, index] = match; | |
| if (type && index) { | |
| const targetIndex = parseInt(index) - 1; // Convert to 0-based index | |
| // Find nodes of the matching type | |
| const nodesOfType = graphData.nodes.filter( | |
| (n) => n.type && n.type.toLowerCase() === type.toLowerCase() | |
| ); | |
| // Get the node at the specified index | |
| if (nodesOfType[targetIndex]) { | |
| affectedElement = nodesOfType[targetIndex]; | |
| console.log( | |
| `Found element by type matching: ${type}[${targetIndex}] -> ${affectedElement.id}` | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // Strategy 3: If still not found, try partial name matching | |
| if (!affectedElement) { | |
| affectedElement = | |
| graphData.nodes.find( | |
| (n) => | |
| n.name && n.name.toLowerCase().includes(affectedId.toLowerCase()) | |
| ) || | |
| graphData.links.find( | |
| (l) => | |
| l.label && | |
| l.label.toLowerCase().includes(affectedId.toLowerCase()) | |
| ); | |
| if (affectedElement) { | |
| console.log( | |
| `Found element by partial name matching: ${affectedId} -> ${affectedElement.id}` | |
| ); | |
| } | |
| } | |
| return affectedElement; | |
| }, | |
| [] | |
| ); | |
| // Fetch graph data from API | |
| const fetchGraphData = useCallback(async () => { | |
| try { | |
| setLoading(true); | |
| setError(null); | |
| console.log("Fetching graph data for kg_id:", kg.kg_id); | |
| // Use the same endpoint as agent_graph_visualizer.html | |
| const response = await fetch( | |
| `/api/knowledge-graphs/${encodeURIComponent(kg.kg_id)}` | |
| ); | |
| console.log("API response status:", response.status); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| console.log("API error response:", errorText); | |
| throw new Error( | |
| `Failed to fetch graph data: ${response.status} - ${errorText}` | |
| ); | |
| } | |
| const data = await response.json(); | |
| console.log("API response data:", data); | |
| console.log("First entity sample:", data.entities?.[0]); | |
| console.log("First relation sample:", data.relations?.[0]); | |
| // Convert the API response using our data adapter | |
| if (data.entities && data.relations) { | |
| console.log( | |
| "Using real API data:", | |
| data.entities.length, | |
| "entities,", | |
| data.relations.length, | |
| "relations" | |
| ); | |
| const rawData = await api.knowledgeGraphs.getData( | |
| kg.kg_id || kg.filename | |
| ); | |
| // Detect schema type and set capabilities | |
| const schemaType = detectSchemaType(rawData); | |
| const capabilities = getSchemaCapabilities(schemaType); | |
| setSchemaCapabilities(capabilities); | |
| const adapted = dataAdapter.adapt(rawData); | |
| const rawFailures = Array.isArray((rawData as any).failures) | |
| ? (rawData as any).failures | |
| : []; | |
| const rawOptimizations = Array.isArray((rawData as any).optimizations) | |
| ? (rawData as any).optimizations | |
| : []; | |
| // Enrich failures with matched element information | |
| const enrichedFailures = rawFailures.map((failure: any) => { | |
| if (failure.affected_id) { | |
| const matchedElement = findAffectedElement( | |
| failure.affected_id, | |
| adapted | |
| ); | |
| if (matchedElement) { | |
| return { | |
| ...failure, | |
| matchedElement, | |
| displayName: | |
| ("name" in matchedElement && matchedElement.name) || | |
| ("label" in matchedElement && matchedElement.label) || | |
| matchedElement.id, | |
| }; | |
| } | |
| } | |
| return failure; | |
| }); | |
| // Enrich optimizations with matched element information | |
| const enrichedOptimizations = rawOptimizations.map((opt: any) => { | |
| const affectedNodes = (opt.affected_ids || []).map((id: string) => { | |
| const matchedElement = findAffectedElement(id, adapted); | |
| return { | |
| id, | |
| name: matchedElement | |
| ? ("name" in matchedElement && matchedElement.name) || | |
| ("label" in matchedElement && matchedElement.label) || | |
| id | |
| : id, | |
| }; | |
| }); | |
| return { ...opt, affected_nodes: affectedNodes }; | |
| }); | |
| // Debug: Log the data structure to understand the mismatch | |
| console.log("Raw failures data:", rawFailures); | |
| console.log("Enriched failures:", enrichedFailures); | |
| console.log("Raw optimizations data:", rawOptimizations); | |
| console.log("Enriched optimizations:", enrichedOptimizations); | |
| console.log("Adapted graph nodes:", adapted.nodes.slice(0, 3)); | |
| setFailures(enrichedFailures); | |
| setOptimizations(enrichedOptimizations); | |
| setGraphData(adapted); | |
| } else { | |
| throw new Error( | |
| "API data format unexpected - missing entities or relations" | |
| ); | |
| } | |
| } catch (error) { | |
| console.error("Error fetching graph data:", error); | |
| setError( | |
| `Failed to load graph data: ${ | |
| error instanceof Error ? error.message : String(error) | |
| }` | |
| ); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, [kg.kg_id, kg.filename, dataAdapter]); | |
| // Load data on component mount | |
| useEffect(() => { | |
| fetchGraphData(); | |
| }, [fetchGraphData]); | |
| // Fetch and number trace once when component mounts or selectedTrace changes | |
| useEffect(() => { | |
| const fetchTrace = async () => { | |
| if (!agState.selectedTrace) return; | |
| try { | |
| const numbered = await api.traces.getNumberedContent( | |
| agState.selectedTrace.trace_id | |
| ); | |
| setTraceLines(numbered.split("\n")); | |
| } catch (err) { | |
| console.error("Failed to fetch trace content", err); | |
| } | |
| }; | |
| fetchTrace(); | |
| }, [agState.selectedTrace]); | |
| // Track container dimensions for draggable legend bounds | |
| useEffect(() => { | |
| const resizeObserver = new ResizeObserver((entries) => { | |
| for (const entry of entries) { | |
| const { width, height } = entry.contentRect; | |
| setContainerDimensions({ width, height }); | |
| } | |
| }); | |
| if (containerRef.current) { | |
| resizeObserver.observe(containerRef.current); | |
| // Set initial dimensions | |
| const rect = containerRef.current.getBoundingClientRect(); | |
| setContainerDimensions({ width: rect.width, height: rect.height }); | |
| } | |
| return () => { | |
| resizeObserver.disconnect(); | |
| }; | |
| }, []); | |
| // Compute highlight ranges when element selection changes | |
| useEffect(() => { | |
| if (!selectedElement) { | |
| setHighlightRanges([]); | |
| return; | |
| } | |
| const sel = selectedElement as any; | |
| const refs = [ | |
| sel.raw_prompt_ref, | |
| sel.raw_text_ref, | |
| sel.interaction_prompt_ref, | |
| ].find((arr) => Array.isArray(arr) && arr.length > 0); | |
| if (!Array.isArray(refs)) { | |
| setHighlightRanges([]); | |
| return; | |
| } | |
| const isContinuation = (idx: number): boolean => { | |
| if (idx < 0 || idx >= traceLines.length) return false; | |
| const line = traceLines[idx] ?? ""; | |
| // Strip <L#> prefix | |
| const contentPart = line.replace(/^<L\d+>\s*/, ""); | |
| return contentPart.startsWith(" "); | |
| }; | |
| const ranges = refs.map((r: any) => { | |
| const start = r.line_start - 1; | |
| let end = r.line_end - 1; | |
| // Extend downward for continuation lines | |
| while (end + 1 < traceLines.length && isContinuation(end + 1)) { | |
| end += 1; | |
| } | |
| return { start, end }; | |
| }); | |
| setHighlightRanges(ranges); | |
| }, [selectedElement, traceLines]); | |
| // Initialize Cytoscape visualization | |
| useEffect(() => { | |
| console.log("KnowledgeGraphVisualizer: Cytoscape useEffect triggered", { | |
| containerRef: !!containerRef.current, | |
| dataNodes: graphData.nodes.length, | |
| dataLinks: graphData.links.length, | |
| loading, | |
| }); | |
| if (loading || graphData.nodes.length === 0) { | |
| console.log( | |
| "KnowledgeGraphVisualizer: Skipping Cytoscape init - still loading or no data" | |
| ); | |
| return; | |
| } | |
| const initializeVisualization = async () => { | |
| try { | |
| if (!containerRef.current) { | |
| console.log( | |
| "KnowledgeGraphVisualizer: Refs not ready during initialization" | |
| ); | |
| return; | |
| } | |
| // Wait for layout to settle | |
| await new Promise((resolve) => setTimeout(resolve, 200)); | |
| const container = containerRef.current; | |
| let width = container.clientWidth || 800; | |
| let height = container.clientHeight || 600; | |
| // If container has zero dimensions, try to get parent dimensions | |
| if (width === 0 || height === 0) { | |
| const parent = container.parentElement; | |
| if (parent) { | |
| width = parent.clientWidth || 800; | |
| height = parent.clientHeight || 600; | |
| console.log("KnowledgeGraphVisualizer: Using parent dimensions", { | |
| parentWidth: width, | |
| parentHeight: height, | |
| }); | |
| } | |
| } | |
| // Ensure minimum dimensions | |
| width = Math.max(width, 500); | |
| height = Math.max(height, 400); | |
| const config = createKnowledgeGraphConfig({ | |
| width, | |
| height, | |
| showToolbar: false, | |
| showSidebar: false, | |
| showStats: false, | |
| enableSearch: false, | |
| enableZoom: true, | |
| enablePan: true, | |
| enableDrag: true, | |
| enableSelection: true, | |
| }); | |
| console.log( | |
| "KnowledgeGraphVisualizer: Creating CytoscapeGraphCore with config", | |
| config | |
| ); | |
| // Create selection callbacks | |
| const selectionCallbacks: GraphSelectionCallbacks = { | |
| onNodeSelect: (node: UniversalNode) => { | |
| console.log("Node selected:", node); | |
| console.log("Node raw_prompt:", node.raw_prompt); | |
| console.log("Node properties:", node.properties); | |
| setSelectedElement(node); | |
| setSelectedElementType("node"); | |
| }, | |
| onLinkSelect: (link: UniversalLink) => { | |
| console.log("Link selected:", link); | |
| console.log("Link interaction_prompt:", link.interaction_prompt); | |
| console.log("Link properties:", link.properties); | |
| setSelectedElement(link); | |
| setSelectedElementType("link"); | |
| }, | |
| onClearSelection: () => { | |
| console.log("Selection cleared"); | |
| setSelectedElement(null); | |
| setSelectedElementType(null); | |
| }, | |
| }; | |
| cytoscapeRef.current = new CytoscapeGraphCore( | |
| containerRef.current, | |
| config, | |
| selectionCallbacks | |
| ); | |
| // Load data | |
| console.log("KnowledgeGraphVisualizer: Loading data", { | |
| nodes: graphData.nodes.length, | |
| links: graphData.links.length, | |
| }); | |
| cytoscapeRef.current.updateGraph(graphData, true, failures); | |
| console.log( | |
| "KnowledgeGraphVisualizer: Cytoscape initialization complete" | |
| ); | |
| } catch (err) { | |
| console.error( | |
| "KnowledgeGraphVisualizer: Error initializing Cytoscape:", | |
| err | |
| ); | |
| setError( | |
| `Failed to initialize visualization: ${ | |
| err instanceof Error ? err.message : String(err) | |
| }` | |
| ); | |
| } | |
| }; | |
| // Function to check if refs are ready and initialize | |
| const checkAndInitialize = () => { | |
| if (!containerRef.current) { | |
| console.log("KnowledgeGraphVisualizer: Missing refs, retrying..."); | |
| return false; | |
| } | |
| return true; | |
| }; | |
| // If refs not ready immediately, set up a retry mechanism | |
| if (!checkAndInitialize()) { | |
| let retryCount = 0; | |
| const maxRetries = 10; | |
| const retryInterval = setInterval(() => { | |
| retryCount++; | |
| console.log( | |
| `KnowledgeGraphVisualizer: Retry attempt ${retryCount}/${maxRetries}` | |
| ); | |
| if (checkAndInitialize()) { | |
| clearInterval(retryInterval); | |
| initializeVisualization(); | |
| } else if (retryCount >= maxRetries) { | |
| clearInterval(retryInterval); | |
| console.error( | |
| "KnowledgeGraphVisualizer: Max retries reached, giving up" | |
| ); | |
| setError( | |
| "Failed to initialize visualization: DOM elements not ready" | |
| ); | |
| } | |
| }, 200); | |
| return () => { | |
| clearInterval(retryInterval); | |
| if (cytoscapeRef.current) { | |
| cytoscapeRef.current.destroy(); | |
| cytoscapeRef.current = null; | |
| } | |
| }; | |
| } | |
| // If refs are ready, initialize immediately | |
| const timeoutId = setTimeout(initializeVisualization, 100); | |
| // Cleanup | |
| return () => { | |
| clearTimeout(timeoutId); | |
| if (cytoscapeRef.current) { | |
| cytoscapeRef.current.destroy(); | |
| cytoscapeRef.current = null; | |
| } | |
| }; | |
| }, [graphData, loading]); | |
| // Update graph when failures change to apply failure zones | |
| useEffect(() => { | |
| if (cytoscapeRef.current && graphData.nodes.length > 0 && !loading) { | |
| console.log("KnowledgeGraphVisualizer: Updating graph with failures", { | |
| failuresCount: failures.length, | |
| failureIds: failures | |
| .map((f) => (f as any).affected_id || (f as any).matchedElement?.id) | |
| .filter(Boolean), | |
| }); | |
| cytoscapeRef.current.updateGraph(graphData, false, failures); | |
| } | |
| }, [failures, graphData, loading]); | |
| // Zoom control handlers | |
| const handleZoomIn = () => { | |
| if (cytoscapeRef.current) { | |
| cytoscapeRef.current.zoomIn(); | |
| } | |
| }; | |
| const handleZoomOut = () => { | |
| if (cytoscapeRef.current) { | |
| cytoscapeRef.current.zoomOut(); | |
| } | |
| }; | |
| const handleResetZoom = () => { | |
| if (cytoscapeRef.current) { | |
| cytoscapeRef.current.resetZoom(); | |
| } | |
| }; | |
| const handleEntitySelect = (entityId: string) => { | |
| if (cytoscapeRef.current) { | |
| cytoscapeRef.current.selectNodeById(entityId); | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="flex flex-col h-screen bg-background"> | |
| <div className="flex items-center justify-center flex-1"> | |
| <div className="text-center"> | |
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div> | |
| <p className="text-lg font-medium">Loading Knowledge Graph...</p> | |
| <p className="text-muted-foreground">Preparing visualization</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (error) { | |
| return ( | |
| <div className="flex flex-col h-screen bg-background"> | |
| <div className="flex items-center justify-center flex-1"> | |
| <div className="text-center"> | |
| <div className="text-red-500 mb-4 text-4xl">⚠️</div> | |
| <p className="text-lg font-medium text-red-600"> | |
| Failed to Load Graph | |
| </p> | |
| <p className="text-muted-foreground mt-2">{error}</p> | |
| <button | |
| onClick={fetchGraphData} | |
| className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" | |
| > | |
| Retry | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Debug logging | |
| console.log("KnowledgeGraphVisualizer render:", { | |
| loading, | |
| error, | |
| graphDataNodes: graphData.nodes.length, | |
| graphDataLinks: graphData.links.length, | |
| }); | |
| return ( | |
| <div className="flex flex-col h-screen bg-background"> | |
| {/* Visualization with sidebars */} | |
| <div className="flex-1 flex min-h-0 overflow-x-auto"> | |
| {/* Graph container */} | |
| <div | |
| className="flex-1 relative p-4 min-w-0" | |
| style={{ minWidth: "400px" }} | |
| > | |
| <div | |
| ref={containerRef} | |
| className="w-full h-full bg-white border rounded-lg relative overflow-hidden shadow-sm" | |
| style={{ minHeight: "500px" }} | |
| > | |
| {/* Draggable Graph Legend */} | |
| <GraphLegend containerBounds={containerDimensions} /> | |
| {/* Zoom Controls - Top Right */} | |
| <div className="absolute top-4 right-4 flex flex-col gap-2 z-50 pointer-events-auto"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handleZoomIn} | |
| disabled={loading} | |
| > | |
| <ZoomIn className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handleZoomOut} | |
| disabled={loading} | |
| > | |
| <ZoomOut className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handleResetZoom} | |
| disabled={loading} | |
| > | |
| <RotateCcw className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Right sidebar with all tabs - always show */} | |
| <ElementInfoSidebar | |
| selectedElement={selectedElement} | |
| selectedElementType={selectedElementType} | |
| currentData={ | |
| cytoscapeRef.current?.getCurrentData() || { nodes: [], links: [] } | |
| } | |
| failures={failures} | |
| optimizations={optimizations} | |
| onFailureSelect={(failure) => { | |
| console.log("Failure selected:", failure); | |
| // Find the affected element using our smart matching | |
| let matchedElement = null; | |
| if ((failure as any).affected_id) { | |
| matchedElement = findAffectedElement( | |
| (failure as any).affected_id, | |
| graphData | |
| ); | |
| if (matchedElement) { | |
| console.log("Matched element for failure:", matchedElement); | |
| // Set the matched element as selected instead of the failure | |
| setSelectedElement(matchedElement); | |
| setSelectedElementType("node"); | |
| } else { | |
| // Fallback to failure if no element found | |
| setSelectedElement(failure); | |
| setSelectedElementType("failure"); | |
| } | |
| } else { | |
| // No affected_id, select the failure | |
| setSelectedElement(failure); | |
| setSelectedElementType("failure"); | |
| } | |
| // Auto-select the affected element in the graph if it exists | |
| if ((failure as any).affected_id && cytoscapeRef.current) { | |
| const affectedId = (failure as any).affected_id; | |
| console.log("Looking for affected element with ID:", affectedId); | |
| const cy = cytoscapeRef.current.getCytoscape(); | |
| if (cy) { | |
| // Strategy 1: Try exact ID match first | |
| let affectedElement = cy.getElementById(affectedId); | |
| // Strategy 2: If not found, try smart matching based on ID pattern | |
| if (affectedElement.length === 0) { | |
| const match = affectedId.match( | |
| /^(task|tool|agent|input|output|human)_(\d+)$/i | |
| ); | |
| if (match) { | |
| const [, type, index] = match; | |
| const targetIndex = parseInt(index) - 1; // Convert to 0-based index | |
| // Find elements of the matching type | |
| const elementsOfType = cy.nodes().filter((node: any) => { | |
| const nodeType = node.data("type"); | |
| return ( | |
| nodeType && | |
| nodeType.toLowerCase() === type.toLowerCase() | |
| ); | |
| }); | |
| // Get the element at the specified index | |
| if ( | |
| elementsOfType.length > targetIndex && | |
| targetIndex >= 0 | |
| ) { | |
| const targetElement = elementsOfType[targetIndex]; | |
| if (targetElement) { | |
| affectedElement = cy.getElementById(targetElement.id()); | |
| console.log( | |
| `Found element by type matching: ${type}[${targetIndex}] -> ${targetElement.id()}` | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // Strategy 3: If still not found, try partial name matching | |
| if (affectedElement.length === 0) { | |
| const matchingElements = cy.nodes().filter((node: any) => { | |
| const nodeName = node.data("name") || node.data("label"); | |
| return ( | |
| nodeName && | |
| nodeName.toLowerCase().includes(affectedId.toLowerCase()) | |
| ); | |
| }); | |
| if (matchingElements.length > 0) { | |
| const firstMatch = matchingElements.first(); | |
| affectedElement = cy.getElementById(firstMatch.id()); | |
| console.log( | |
| `Found element by partial name matching: ${affectedId} -> ${firstMatch.id()}` | |
| ); | |
| } | |
| } | |
| if (affectedElement.length > 0) { | |
| console.log("Found affected element in graph, selecting it"); | |
| // Clear current selection | |
| cy.elements().unselect(); | |
| // Select the affected element | |
| affectedElement.select(); | |
| // Center the view on the affected element | |
| cy.center(affectedElement); | |
| // Optional: Zoom to the element for better visibility | |
| cy.fit(affectedElement, 100); // 100px padding | |
| } else { | |
| console.log( | |
| "Affected element not found in graph:", | |
| affectedId | |
| ); | |
| console.log( | |
| "Available graph element IDs:", | |
| cy | |
| .elements() | |
| .map((el: any) => el.id()) | |
| .join(", ") | |
| ); | |
| } | |
| } | |
| } | |
| }} | |
| knowledgeGraph={kg} | |
| numberedLines={traceLines} | |
| highlightRanges={highlightRanges} | |
| showTraceTab={schemaCapabilities.supportsLineReferences} | |
| onShowInTrace={(ranges) => { | |
| console.log("Show in trace requested with ranges:", ranges); | |
| // Set the highlight ranges with a small delay to ensure tab transition is complete | |
| setTimeout(() => { | |
| setHighlightRanges(ranges); | |
| }, 100); | |
| }} | |
| onEntitySelect={handleEntitySelect} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| }; | |