diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3bc4094e..adea827bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: language: system pass_filenames: true types: [python] + exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/crewai/tests/|lib/crewai-tools/tests/) - repo: https://github.com/astral-sh/uv-pre-commit rev: 0.9.3 hooks: diff --git a/lib/crewai/src/crewai/flow/__init__.py b/lib/crewai/src/crewai/flow/__init__.py index bda0186c7..bdb28fabd 100644 --- a/lib/crewai/src/crewai/flow/__init__.py +++ b/lib/crewai/src/crewai/flow/__init__.py @@ -1,12 +1,10 @@ +from crewai.flow.flow import Flow, and_, listen, or_, router, start +from crewai.flow.persistence import persist from crewai.flow.visualization import ( FlowStructure, build_flow_structure, - print_structure_summary, - structure_to_dict, visualize_flow_structure, ) -from crewai.flow.flow import Flow, and_, listen, or_, router, start -from crewai.flow.persistence import persist __all__ = [ @@ -17,9 +15,7 @@ __all__ = [ "listen", "or_", "persist", - "print_structure_summary", "router", "start", - "structure_to_dict", "visualize_flow_structure", ] diff --git a/lib/crewai/src/crewai/flow/visualization/__init__.py b/lib/crewai/src/crewai/flow/visualization/__init__.py index 98665f642..4d77c764d 100644 --- a/lib/crewai/src/crewai/flow/visualization/__init__.py +++ b/lib/crewai/src/crewai/flow/visualization/__init__.py @@ -3,8 +3,6 @@ from crewai.flow.visualization.builder import ( build_flow_structure, calculate_execution_paths, - print_structure_summary, - structure_to_dict, ) from crewai.flow.visualization.renderers import render_interactive from crewai.flow.visualization.types import FlowStructure, NodeMetadata, StructureEdge @@ -18,8 +16,6 @@ __all__ = [ "StructureEdge", "build_flow_structure", "calculate_execution_paths", - "print_structure_summary", "render_interactive", - "structure_to_dict", "visualize_flow_structure", ] diff --git a/lib/crewai/src/crewai/flow/visualization/assets/interactive.js b/lib/crewai/src/crewai/flow/visualization/assets/interactive.js index c6998becd..8d4fe9bd9 100644 --- a/lib/crewai/src/crewai/flow/visualization/assets/interactive.js +++ b/lib/crewai/src/crewai/flow/visualization/assets/interactive.js @@ -1,1039 +1,1186 @@ +"use strict"; + +/** + * Flow Visualization Interactive Script + * Handles the interactive network visualization for CrewAI flows + */ + +// ============================================================================ +// Constants +// ============================================================================ + +const CONSTANTS = { + NODE: { + BASE_WIDTH: 200, + BASE_HEIGHT: 60, + BORDER_RADIUS: 20, + TEXT_SIZE: 13, + TEXT_PADDING: 8, + TEXT_BG_RADIUS: 6, + HOVER_SCALE: 1.04, + PRESSED_SCALE: 0.98, + }, + EDGE: { + DEFAULT_WIDTH: 2, + HIGHLIGHTED_WIDTH: 8, + ANIMATION_DURATION: 300, + DEFAULT_SHADOW_SIZE: 4, + HIGHLIGHTED_SHADOW_SIZE: 20, + }, + ANIMATION: { + DURATION: 300, + EASE_OUT_CUBIC: (t) => 1 - Math.pow(1 - t, 3), + }, + NETWORK: { + STABILIZATION_ITERATIONS: 200, + NODE_DISTANCE: 180, + SPRING_LENGTH: 150, + LEVEL_SEPARATION: 180, + NODE_SPACING: 220, + TREE_SPACING: 250, + }, + DRAWER: { + WIDTH: 400, + OFFSET_SCALE: 0.3, + }, +}; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Loads the vis-network library from CDN + */ function loadVisCDN() { return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = 'https://unpkg.com/vis-network@9.1.2/dist/vis-network.min.js'; + const script = document.createElement("script"); + script.src = "https://unpkg.com/vis-network@9.1.2/dist/vis-network.min.js"; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } -(async () => { +/** + * Draws a rounded rectangle on a canvas context + */ +function drawRoundedRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); +} - try { - await loadVisCDN(); - const nodes = new vis.DataSet('{{ nodes_list_json }}'); - const edges = new vis.DataSet('{{ edges_list_json }}'); +/** + * Highlights Python code using Prism + */ +function highlightPython(code) { + return Prism.highlight(code, Prism.languages.python, "python"); +} - const container = document.getElementById('network'); - const data = { - nodes: nodes, - edges: edges - }; +// ============================================================================ +// Node Renderer +// ============================================================================ - const options = { - nodes: { - shape: 'custom', - shadow: false, - chosen: false, - size: 30, - ctxRenderer: function ({ctx, id, x, y, state: {selected, hover}, style, label}) { - const node = nodes.get(id); - if (!node || !node.nodeStyle) return {}; +class NodeRenderer { + constructor(nodes, networkManager) { + this.nodes = nodes; + this.networkManager = networkManager; + } - const nodeStyle = node.nodeStyle; - const baseWidth = 200; - const baseHeight = 60; + render({ ctx, id, x, y, state, style, label }) { + const node = this.nodes.get(id); + if (!node || !node.nodeStyle) return {}; - let scale = 1.0; - if (pressedNodeId === id) { - scale = 0.98; - } else if (hoveredNodeId === id) { - scale = 1.04; - } + const scale = this.getNodeScale(id); + const isActiveDrawer = + this.networkManager.drawerManager?.activeNodeId === id; + const nodeStyle = node.nodeStyle; + const width = CONSTANTS.NODE.BASE_WIDTH * scale; + const height = CONSTANTS.NODE.BASE_HEIGHT * scale; - const isActiveDrawer = activeDrawerNodeId === id; + return { + drawNode: () => { + ctx.save(); + this.applyNodeOpacity(ctx, node); + this.applyShadow(ctx, node, isActiveDrawer); + this.drawNodeShape( + ctx, + x, + y, + width, + height, + scale, + nodeStyle, + isActiveDrawer, + ); + this.drawNodeText(ctx, x, y, scale, nodeStyle); + ctx.restore(); + }, + nodeDimensions: { width, height }, + }; + } - const width = baseWidth * scale; - const height = baseHeight * scale; + getNodeScale(id) { + if (this.networkManager.pressedNodeId === id) { + return CONSTANTS.NODE.PRESSED_SCALE; + } else if (this.networkManager.hoveredNodeId === id) { + return CONSTANTS.NODE.HOVER_SCALE; + } + return 1.0; + } - return { - drawNode() { - ctx.save(); + applyNodeOpacity(ctx, node) { + const nodeOpacity = node.opacity !== undefined ? node.opacity : 1.0; + ctx.globalAlpha = nodeOpacity; + } - const nodeOpacity = node.opacity !== undefined ? node.opacity : 1.0; - ctx.globalAlpha = nodeOpacity; + applyShadow(ctx, node, isActiveDrawer) { + if (node.shadow && node.shadow.enabled) { + ctx.shadowColor = node.shadow.color || "rgba(0,0,0,0.1)"; + ctx.shadowBlur = node.shadow.size || 8; + ctx.shadowOffsetX = node.shadow.x || 0; + ctx.shadowOffsetY = node.shadow.y || 0; + } else if (isActiveDrawer) { + ctx.shadowColor = "{{ CREWAI_ORANGE }}"; + ctx.shadowBlur = 20; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + } + } - if (node.shadow && node.shadow.enabled) { - ctx.shadowColor = node.shadow.color || '{{ CREWAI_ORANGE }}'; - ctx.shadowBlur = node.shadow.size || 20; - ctx.shadowOffsetX = node.shadow.x || 0; - ctx.shadowOffsetY = node.shadow.y || 0; - } else if (isActiveDrawer) { - ctx.shadowColor = '{{ CREWAI_ORANGE }}'; - ctx.shadowBlur = 20; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } else { - ctx.shadowColor = 'rgba(0,0,0,0.1)'; - ctx.shadowBlur = 8; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - } + drawNodeShape(ctx, x, y, width, height, scale, nodeStyle, isActiveDrawer) { + const radius = CONSTANTS.NODE.BORDER_RADIUS * scale; + const rectX = x - width / 2; + const rectY = y - height / 2; - const radius = 20 * scale; - const rectX = x - width / 2; - const rectY = y - height / 2; + drawRoundedRect(ctx, rectX, rectY, width, height, radius); - ctx.beginPath(); - ctx.moveTo(rectX + radius, rectY); - ctx.lineTo(rectX + width - radius, rectY); - ctx.quadraticCurveTo(rectX + width, rectY, rectX + width, rectY + radius); - ctx.lineTo(rectX + width, rectY + height - radius); - ctx.quadraticCurveTo(rectX + width, rectY + height, rectX + width - radius, rectY + height); - ctx.lineTo(rectX + radius, rectY + height); - ctx.quadraticCurveTo(rectX, rectY + height, rectX, rectY + height - radius); - ctx.lineTo(rectX, rectY + radius); - ctx.quadraticCurveTo(rectX, rectY, rectX + radius, rectY); - ctx.closePath(); + ctx.fillStyle = nodeStyle.bgColor; + ctx.fill(); - ctx.fillStyle = nodeStyle.bgColor; - ctx.fill(); + ctx.shadowColor = "transparent"; + ctx.shadowBlur = 0; - ctx.shadowColor = 'transparent'; - ctx.shadowBlur = 0; + ctx.strokeStyle = isActiveDrawer + ? "{{ CREWAI_ORANGE }}" + : nodeStyle.borderColor; + ctx.lineWidth = nodeStyle.borderWidth * scale; + ctx.stroke(); + } - const borderWidth = isActiveDrawer ? nodeStyle.borderWidth * 2 : nodeStyle.borderWidth; - ctx.strokeStyle = isActiveDrawer ? '{{ CREWAI_ORANGE }}' : nodeStyle.borderColor; - ctx.lineWidth = borderWidth * scale; - ctx.stroke(); + drawNodeText(ctx, x, y, scale, nodeStyle) { + ctx.font = `500 ${CONSTANTS.NODE.TEXT_SIZE * scale}px 'JetBrains Mono', 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; - ctx.font = `500 ${13 * scale}px 'JetBrains Mono', 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + const textMetrics = ctx.measureText(nodeStyle.name); + const textWidth = textMetrics.width; + const textHeight = CONSTANTS.NODE.TEXT_SIZE * scale; + const textPadding = CONSTANTS.NODE.TEXT_PADDING * scale; + const textBgRadius = CONSTANTS.NODE.TEXT_BG_RADIUS * scale; - const textMetrics = ctx.measureText(nodeStyle.name); - const textWidth = textMetrics.width; - const textHeight = 13 * scale; - const textPadding = 8 * scale; - const textBgRadius = 6 * scale; + const textBgX = x - textWidth / 2 - textPadding; + const textBgY = y - textHeight / 2 - textPadding / 2; + const textBgWidth = textWidth + textPadding * 2; + const textBgHeight = textHeight + textPadding; - const textBgX = x - textWidth / 2 - textPadding; - const textBgY = y - textHeight / 2 - textPadding / 2; - const textBgWidth = textWidth + textPadding * 2; - const textBgHeight = textHeight + textPadding; + drawRoundedRect( + ctx, + textBgX, + textBgY, + textBgWidth, + textBgHeight, + textBgRadius, + ); - ctx.beginPath(); - ctx.moveTo(textBgX + textBgRadius, textBgY); - ctx.lineTo(textBgX + textBgWidth - textBgRadius, textBgY); - ctx.quadraticCurveTo(textBgX + textBgWidth, textBgY, textBgX + textBgWidth, textBgY + textBgRadius); - ctx.lineTo(textBgX + textBgWidth, textBgY + textBgHeight - textBgRadius); - ctx.quadraticCurveTo(textBgX + textBgWidth, textBgY + textBgHeight, textBgX + textBgWidth - textBgRadius, textBgY + textBgHeight); - ctx.lineTo(textBgX + textBgRadius, textBgY + textBgHeight); - ctx.quadraticCurveTo(textBgX, textBgY + textBgHeight, textBgX, textBgY + textBgHeight - textBgRadius); - ctx.lineTo(textBgX, textBgY + textBgRadius); - ctx.quadraticCurveTo(textBgX, textBgY, textBgX + textBgRadius, textBgY); - ctx.closePath(); + ctx.fillStyle = "rgba(255, 255, 255, 0.2)"; + ctx.fill(); - ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; - ctx.fill(); + ctx.fillStyle = nodeStyle.fontColor; + ctx.fillText(nodeStyle.name, x, y); + } +} - ctx.fillStyle = nodeStyle.fontColor; - ctx.fillText(nodeStyle.name, x, y); +// ============================================================================ +// Animation Manager +// ============================================================================ - ctx.restore(); - }, - nodeDimensions: {width, height} - }; - }, - scaling: { - min: 1, - max: 100 - } - }, - edges: { - width: 2, - hoverWidth: 0, - labelHighlightBold: false, - shadow: { - enabled: true, - color: 'rgba(0,0,0,0.08)', - size: 4, - x: 1, - y: 1 - }, - smooth: { - type: 'cubicBezier', - roundness: 0.5 - }, - font: { - size: 13, - align: 'middle', - color: 'transparent', - face: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - strokeWidth: 0, - background: 'transparent', - vadjust: 0, - bold: { - face: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - mod: 'bold', - vadjust: 0 - } - }, - arrows: { - to: { - enabled: true, - scaleFactor: 0.8, - type: 'triangle' - } - }, - arrowStrikethrough: true, - chosen: { - edge: false, - label: false - } - }, - physics: { - enabled: true, - hierarchicalRepulsion: { - nodeDistance: 180, - centralGravity: 0.0, - springLength: 150, - springConstant: 0.01, - damping: 0.09 - }, - solver: 'hierarchicalRepulsion', - stabilization: { - enabled: true, - iterations: 200, - updateInterval: 25 - } - }, - layout: { - hierarchical: { - enabled: true, - direction: 'UD', - sortMethod: 'directed', - levelSeparation: 180, - nodeSpacing: 220, - treeSpacing: 250 - } - }, - interaction: { - hover: true, - hoverConnectedEdges: false, - navigationButtons: false, - keyboard: true, - selectConnectedEdges: false, - multiselect: false - } - }; +class AnimationManager { + constructor() { + this.animations = new Map(); + } - const network = new vis.Network(container, data, options); + animateEdgeWidth( + edges, + edgeId, + targetWidth, + duration = CONSTANTS.EDGE.ANIMATION_DURATION, + ) { + this.cancel(edgeId); - const ideSelector = document.getElementById('ide-selector'); - const savedIDE = localStorage.getItem('preferred_ide') || 'auto'; - ideSelector.value = savedIDE; - ideSelector.addEventListener('change', function () { - localStorage.setItem('preferred_ide', this.value); + const edge = edges.get(edgeId); + if (!edge) return; + + const startWidth = edge.width || CONSTANTS.EDGE.DEFAULT_WIDTH; + const startTime = performance.now(); + + const animate = () => { + const elapsed = performance.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = CONSTANTS.ANIMATION.EASE_OUT_CUBIC(progress); + const currentWidth = startWidth + (targetWidth - startWidth) * eased; + + edges.update({ id: edgeId, width: currentWidth }); + + if (progress < 1) { + const frameId = requestAnimationFrame(animate); + this.animations.set(edgeId, frameId); + } else { + this.animations.delete(edgeId); + } + }; + + animate(); + } + + cancel(id) { + if (this.animations.has(id)) { + cancelAnimationFrame(this.animations.get(id)); + this.animations.delete(id); + } + } + + cancelAll() { + this.animations.forEach((frameId) => cancelAnimationFrame(frameId)); + this.animations.clear(); + } +} + +// ============================================================================ +// Triggered By Highlighter +// ============================================================================ + +class TriggeredByHighlighter { + constructor(network, nodes, edges, highlightCanvas) { + this.network = network; + this.nodes = nodes; + this.edges = edges; + this.canvas = highlightCanvas; + this.ctx = highlightCanvas.getContext("2d"); + + this.highlightedNodes = []; + this.highlightedEdges = []; + this.activeDrawerNodeId = null; + this.activeDrawerEdges = []; + + this.setupCanvas(); + } + + setupCanvas() { + this.resizeCanvas(); + this.canvas.classList.remove("visible"); + window.addEventListener("resize", () => this.resizeCanvas()); + } + + resizeCanvas() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + } + + setActiveDrawer(nodeId, edges) { + this.activeDrawerNodeId = nodeId; + this.activeDrawerEdges = edges; + } + + highlightTriggeredByGroup(triggerNodeIds) { + this.clear(); + + if (!this.activeDrawerNodeId || !triggerNodeIds || triggerNodeIds.length === 0) { + console.warn("TriggeredByHighlighter: Missing activeDrawerNodeId or triggerNodeIds"); + return; + } + + const allEdges = this.edges.get(); + const pathNodes = new Set([this.activeDrawerNodeId]); + const connectingEdges = []; + const nodeData = '{{ nodeData }}'; + + triggerNodeIds.forEach(triggerNodeId => { + const directEdges = allEdges.filter( + (edge) => edge.from === triggerNodeId && edge.to === this.activeDrawerNodeId + ); + + if (directEdges.length > 0) { + directEdges.forEach(edge => { + connectingEdges.push(edge); + pathNodes.add(edge.from); + pathNodes.add(edge.to); }); + } else { + for (const [nodeName, nodeInfo] of Object.entries(nodeData)) { + if (nodeInfo.router_paths && nodeInfo.router_paths.includes(triggerNodeId)) { + const routerNode = nodeName; - const highlightCanvas = document.getElementById('highlight-canvas'); - const highlightCtx = highlightCanvas.getContext('2d'); - - function resizeHighlightCanvas() { - highlightCanvas.width = window.innerWidth; - highlightCanvas.height = window.innerHeight; - } - - resizeHighlightCanvas(); - window.addEventListener('resize', resizeHighlightCanvas); - - let highlightedNodes = []; - let highlightedEdges = []; - let nodeRestoreAnimationId = null; - let edgeRestoreAnimationId = null; - - function drawHighlightLayer() { - highlightCtx.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height); - - if (highlightedNodes.length === 0) return; - - highlightedNodes.forEach(function (nodeId) { - const nodePosition = network.getPositions([nodeId])[nodeId]; - if (!nodePosition) return; - - const canvasPos = network.canvasToDOM(nodePosition); - const node = nodes.get(nodeId); - if (!node || !node.nodeStyle) return; - - const nodeStyle = node.nodeStyle; - const baseWidth = 200; - const baseHeight = 60; - const scale = 1.0; - const width = baseWidth * scale; - const height = baseHeight * scale; - - highlightCtx.save(); - - highlightCtx.shadowColor = '{{ CREWAI_ORANGE }}'; - highlightCtx.shadowBlur = 20; - highlightCtx.shadowOffsetX = 0; - highlightCtx.shadowOffsetY = 0; - - const radius = 20 * scale; - const rectX = canvasPos.x - width / 2; - const rectY = canvasPos.y - height / 2; - - highlightCtx.beginPath(); - highlightCtx.moveTo(rectX + radius, rectY); - highlightCtx.lineTo(rectX + width - radius, rectY); - highlightCtx.quadraticCurveTo(rectX + width, rectY, rectX + width, rectY + radius); - highlightCtx.lineTo(rectX + width, rectY + height - radius); - highlightCtx.quadraticCurveTo(rectX + width, rectY + height, rectX + width - radius, rectY + height); - highlightCtx.lineTo(rectX + radius, rectY + height); - highlightCtx.quadraticCurveTo(rectX, rectY + height, rectX, rectY + height - radius); - highlightCtx.lineTo(rectX, rectY + radius); - highlightCtx.quadraticCurveTo(rectX, rectY, rectX + radius, rectY); - highlightCtx.closePath(); - - highlightCtx.fillStyle = nodeStyle.bgColor; - highlightCtx.fill(); - - highlightCtx.shadowColor = 'transparent'; - highlightCtx.shadowBlur = 0; - - highlightCtx.strokeStyle = '{{ CREWAI_ORANGE }}'; - highlightCtx.lineWidth = nodeStyle.borderWidth * 2 * scale; - highlightCtx.stroke(); - - highlightCtx.fillStyle = nodeStyle.fontColor; - highlightCtx.font = `500 ${15 * scale}px Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`; - highlightCtx.textAlign = 'center'; - highlightCtx.textBaseline = 'middle'; - highlightCtx.fillText(nodeStyle.name, canvasPos.x, canvasPos.y); - - highlightCtx.restore(); - }); - } - - function highlightTriggeredBy(triggerNodeId) { - clearTriggeredByHighlight(); - - if (activeDrawerEdges && activeDrawerEdges.length > 0) { - activeDrawerEdges.forEach(function (edgeId) { - edges.update({ - id: edgeId, - width: 2, - opacity: 1.0 - }); - }); - activeDrawerEdges = []; - } - - if (!activeDrawerNodeId || !triggerNodeId) return; - - const allEdges = edges.get(); - let connectingEdges = []; - let actualTriggerNodeId = triggerNodeId; - - connectingEdges = allEdges.filter(edge => - edge.from === triggerNodeId && edge.to === activeDrawerNodeId + const routerEdges = allEdges.filter( + (edge) => edge.from === routerNode && edge.dashes ); - if (connectingEdges.length === 0) { - const incomingRouterEdges = allEdges.filter(edge => - edge.to === activeDrawerNodeId && edge.dashes - ); + for (const routerEdge of routerEdges) { + if (routerEdge.to === this.activeDrawerNodeId) { + connectingEdges.push(routerEdge); + pathNodes.add(routerNode); + pathNodes.add(this.activeDrawerNodeId); + break; + } - if (incomingRouterEdges.length > 0) { - incomingRouterEdges.forEach(function (edge) { - connectingEdges.push(edge); - actualTriggerNodeId = edge.from; - }); - } else { - const outgoingRouterEdges = allEdges.filter(edge => - edge.from === activeDrawerNodeId && edge.dashes - ); + const intermediateNode = routerEdge.to; + const pathToActive = allEdges.filter( + (edge) => edge.from === intermediateNode && edge.to === this.activeDrawerNodeId + ); - const nodeData = '{{ nodeData }}'; - for (const [nodeName, nodeInfo] of Object.entries(nodeData)) { - if (nodeInfo.trigger_methods && nodeInfo.trigger_methods.includes(triggerNodeId)) { - const edgeToTarget = outgoingRouterEdges.find(e => e.to === nodeName); - if (edgeToTarget) { - connectingEdges.push(edgeToTarget); - actualTriggerNodeId = nodeName; - break; - } - } - } - } + if (pathToActive.length > 0) { + connectingEdges.push(routerEdge); + connectingEdges.push(...pathToActive); + pathNodes.add(routerNode); + pathNodes.add(intermediateNode); + pathNodes.add(this.activeDrawerNodeId); + break; + } } - if (connectingEdges.length === 0) return; + if (connectingEdges.length > 0) break; + } + } + } + }); - highlightedNodes = [actualTriggerNodeId, activeDrawerNodeId]; - highlightedEdges = connectingEdges.map(e => e.id); + if (connectingEdges.length === 0) { + console.warn("TriggeredByHighlighter: No connecting edges found for group", { triggerNodeIds }); + return; + } - const allNodesList = nodes.get(); - const nodeAnimDuration = 300; - const nodeAnimStart = performance.now(); + this.highlightedNodes = Array.from(pathNodes); + this.highlightedEdges = connectingEdges.map((e) => e.id); - function animateNodeOpacity() { - const elapsed = performance.now() - nodeAnimStart; - const progress = Math.min(elapsed / nodeAnimDuration, 1); - const eased = 1 - Math.pow(1 - progress, 3); + this.animateNodeOpacity(); + this.animateEdgeStyles(); + } - allNodesList.forEach(function (node) { - const currentOpacity = node.opacity !== undefined ? node.opacity : 1.0; - const targetOpacity = highlightedNodes.includes(node.id) ? 1.0 : 0.2; - const newOpacity = currentOpacity + (targetOpacity - currentOpacity) * eased; + highlightAllRouterPaths() { + this.clear(); - nodes.update({ - id: node.id, - opacity: newOpacity - }); - }); + if (!this.activeDrawerNodeId) { + console.warn("TriggeredByHighlighter: Missing activeDrawerNodeId"); + return; + } - if (progress < 1) { - requestAnimationFrame(animateNodeOpacity); - } + const allEdges = this.edges.get(); + const nodeData = '{{ nodeData }}'; + const activeMetadata = nodeData[this.activeDrawerNodeId]; + + const outgoingRouterEdges = allEdges.filter( + (edge) => edge.from === this.activeDrawerNodeId && edge.dashes + ); + + let routerEdges = []; + const pathNodes = new Set(); + + if (outgoingRouterEdges.length > 0) { + routerEdges = outgoingRouterEdges; + pathNodes.add(this.activeDrawerNodeId); + routerEdges.forEach(edge => { + pathNodes.add(edge.to); + }); + } else if (activeMetadata && activeMetadata.router_paths && activeMetadata.router_paths.length > 0) { + activeMetadata.router_paths.forEach(pathName => { + for (const [nodeName, nodeInfo] of Object.entries(nodeData)) { + if (nodeInfo.router_paths && nodeInfo.router_paths.includes(pathName)) { + const edgeFromRouter = allEdges.filter( + (edge) => edge.from === nodeName && edge.to === this.activeDrawerNodeId && edge.dashes + ); + + if (edgeFromRouter.length > 0) { + routerEdges.push(...edgeFromRouter); + pathNodes.add(nodeName); + pathNodes.add(this.activeDrawerNodeId); + } + } + } + }); + } + + if (routerEdges.length === 0) { + console.warn("TriggeredByHighlighter: No router paths found for node", { + activeDrawerNodeId: this.activeDrawerNodeId, + outgoingEdges: outgoingRouterEdges.length, + hasRouterPathsMetadata: !!activeMetadata?.router_paths, + }); + return; + } + + this.highlightedNodes = Array.from(pathNodes); + this.highlightedEdges = routerEdges.map((e) => e.id); + + this.animateNodeOpacity(); + this.animateEdgeStyles(); + } + + highlightTriggeredBy(triggerNodeId) { + this.clear(); + + if (this.activeDrawerEdges && this.activeDrawerEdges.length > 0) { + this.activeDrawerEdges.forEach((edgeId) => { + this.edges.update({ + id: edgeId, + width: CONSTANTS.EDGE.DEFAULT_WIDTH, + opacity: 1.0, + }); + }); + this.activeDrawerEdges = []; + } + + if (!this.activeDrawerNodeId || !triggerNodeId) { + console.warn( + "TriggeredByHighlighter: Missing activeDrawerNodeId or triggerNodeId", + { + activeDrawerNodeId: this.activeDrawerNodeId, + triggerNodeId: triggerNodeId, + }, + ); + return; + } + + const allEdges = this.edges.get(); + let connectingEdges = []; + let actualTriggerNodeId = triggerNodeId; + + connectingEdges = allEdges.filter( + (edge) => + edge.from === triggerNodeId && edge.to === this.activeDrawerNodeId, + ); + + if (connectingEdges.length === 0) { + const incomingRouterEdges = allEdges.filter( + (edge) => edge.to === this.activeDrawerNodeId && edge.dashes, + ); + + if (incomingRouterEdges.length > 0) { + incomingRouterEdges.forEach((edge) => { + connectingEdges.push(edge); + actualTriggerNodeId = edge.from; + }); + } + } + + if (connectingEdges.length === 0) { + const outgoingRouterEdges = allEdges.filter( + (edge) => edge.from === this.activeDrawerNodeId && edge.dashes, + ); + + if (outgoingRouterEdges.length > 0) { + const nodeData = '{{ nodeData }}'; + for (const [nodeName, nodeInfo] of Object.entries(nodeData)) { + if ( + nodeInfo.trigger_methods && + nodeInfo.trigger_methods.includes(triggerNodeId) + ) { + const edgeToTarget = outgoingRouterEdges.find( + (e) => e.to === nodeName, + ); + if (edgeToTarget) { + connectingEdges.push(edgeToTarget); + actualTriggerNodeId = nodeName; + break; + } + } + } + } + } + + if (connectingEdges.length === 0) { + const nodeData = '{{ nodeData }}'; + + const activeMetadata = nodeData[this.activeDrawerNodeId]; + if ( + activeMetadata && + activeMetadata.trigger_methods && + activeMetadata.trigger_methods.includes(triggerNodeId) + ) { + for (const [nodeName, nodeInfo] of Object.entries(nodeData)) { + if ( + nodeInfo.router_paths && + nodeInfo.router_paths.includes(triggerNodeId) + ) { + const routerNode = nodeName; + + const routerEdges = allEdges.filter( + (edge) => edge.from === routerNode && edge.dashes, + ); + + for (const routerEdge of routerEdges) { + const intermediateNode = routerEdge.to; + + if (intermediateNode === this.activeDrawerNodeId) { + connectingEdges = [routerEdge]; + actualTriggerNodeId = routerNode; + break; + } + + const pathToActive = allEdges.filter( + (edge) => + edge.from === intermediateNode && + edge.to === this.activeDrawerNodeId, + ); + + if (pathToActive.length > 0) { + connectingEdges = [routerEdge, ...pathToActive]; + actualTriggerNodeId = routerNode; + break; + } } - animateNodeOpacity(); + if (connectingEdges.length > 0) break; + } + } + } + } - const allEdgesList = edges.get(); - const edgeAnimDuration = 300; - const edgeAnimStart = performance.now(); + if (connectingEdges.length === 0) { + const edgesWithLabel = allEdges.filter( + (edge) => + edge.dashes && + edge.label === triggerNodeId && + edge.to === this.activeDrawerNodeId, + ); - function animateEdgeStyles() { - const elapsed = performance.now() - edgeAnimStart; - const progress = Math.min(elapsed / edgeAnimDuration, 1); - const eased = 1 - Math.pow(1 - progress, 3); + if (edgesWithLabel.length > 0) { + connectingEdges = edgesWithLabel; + const firstEdge = edgesWithLabel[0]; + actualTriggerNodeId = firstEdge.from; + } + } - allEdgesList.forEach(function (edge) { - if (highlightedEdges.includes(edge.id)) { - const currentWidth = edge.width || 2; - const targetWidth = 8; - const newWidth = currentWidth + (targetWidth - currentWidth) * eased; + if (connectingEdges.length === 0) { + console.warn("TriggeredByHighlighter: No connecting edges found", { + triggerNodeId, + activeDrawerNodeId: this.activeDrawerNodeId, + allEdges: allEdges.length, + edgeDetails: allEdges.map((e) => ({ + from: e.from, + to: e.to, + label: e.label, + dashes: e.dashes, + })), + }); + return; + } - const currentShadowSize = edge.shadow?.size || 4; - const targetShadowSize = 20; - const newShadowSize = currentShadowSize + (targetShadowSize - currentShadowSize) * eased; + const pathNodes = new Set([actualTriggerNodeId, this.activeDrawerNodeId]); + connectingEdges.forEach((edge) => { + pathNodes.add(edge.from); + pathNodes.add(edge.to); + }); - const updateData = { - id: edge.id, - hidden: false, - opacity: 1.0, - width: newWidth, - color: { - color: '{{ CREWAI_ORANGE }}', - highlight: '{{ CREWAI_ORANGE }}' - }, - shadow: { - enabled: true, - color: '{{ CREWAI_ORANGE }}', - size: newShadowSize, - x: 0, - y: 0 - } - }; + this.highlightedNodes = Array.from(pathNodes); + this.highlightedEdges = connectingEdges.map((e) => e.id); - if (edge.dashes) { - const scale = Math.sqrt(newWidth / 2); - updateData.dashes = [15 * scale, 10 * scale]; - } + this.animateNodeOpacity(); + this.animateEdgeStyles(); + } - updateData.arrows = { - to: { - enabled: true, - scaleFactor: 0.8, - type: 'triangle' - } - }; + animateNodeOpacity() { + const allNodesList = this.nodes.get(); + const nodeAnimDuration = CONSTANTS.ANIMATION.DURATION; + const nodeAnimStart = performance.now(); - updateData.color = { - color: '{{ CREWAI_ORANGE }}', - highlight: '{{ CREWAI_ORANGE }}', - hover: '{{ CREWAI_ORANGE }}', - inherit: 'to' - }; + const animate = () => { + const elapsed = performance.now() - nodeAnimStart; + const progress = Math.min(elapsed / nodeAnimDuration, 1); + const eased = CONSTANTS.ANIMATION.EASE_OUT_CUBIC(progress); - edges.update(updateData); - } else { - edges.update({ - id: edge.id, - hidden: false, - opacity: 1.0, - width: 1, - color: { - color: 'transparent', - highlight: 'transparent' - }, - shadow: { - enabled: false - }, - font: { - color: 'transparent', - background: 'transparent' - } - }); - } - }); + allNodesList.forEach((node) => { + const currentOpacity = node.opacity !== undefined ? node.opacity : 1.0; + const targetOpacity = this.highlightedNodes.includes(node.id) + ? 1.0 + : 0.2; + const newOpacity = + currentOpacity + (targetOpacity - currentOpacity) * eased; - if (progress < 1) { - requestAnimationFrame(animateEdgeStyles); - } - } + this.nodes.update({ + id: node.id, + opacity: newOpacity, + }); + }); - animateEdgeStyles(); + if (progress < 1) { + requestAnimationFrame(animate); + } + }; - highlightCanvas.classList.add('visible'); + animate(); + } - setTimeout(function () { - drawHighlightLayer(); - }, 50); + animateEdgeStyles() { + const edgeIds = this.edges.getIds(); + const edgeAnimDuration = CONSTANTS.ANIMATION.DURATION; + const edgeAnimStart = performance.now(); + + const animate = () => { + const elapsed = performance.now() - edgeAnimStart; + const progress = Math.min(elapsed / edgeAnimDuration, 1); + const eased = CONSTANTS.ANIMATION.EASE_OUT_CUBIC(progress); + + edgeIds.forEach((edgeId) => { + const edge = this.edges.get(edgeId); + if (!edge) return; + + if (this.highlightedEdges.includes(edge.id)) { + const currentWidth = edge.width || CONSTANTS.EDGE.DEFAULT_WIDTH; + const targetWidth = CONSTANTS.EDGE.HIGHLIGHTED_WIDTH; + const newWidth = currentWidth + (targetWidth - currentWidth) * eased; + + const currentShadowSize = + edge.shadow?.size || CONSTANTS.EDGE.DEFAULT_SHADOW_SIZE; + const targetShadowSize = CONSTANTS.EDGE.HIGHLIGHTED_SHADOW_SIZE; + const newShadowSize = + currentShadowSize + (targetShadowSize - currentShadowSize) * eased; + + const updateData = { + id: edge.id, + hidden: false, + opacity: 1.0, + width: newWidth, + color: { + color: "{{ CREWAI_ORANGE }}", + highlight: "{{ CREWAI_ORANGE }}", + }, + shadow: { + enabled: true, + color: "{{ CREWAI_ORANGE }}", + size: newShadowSize, + x: 0, + y: 0, + }, + }; + + if (edge.dashes) { + const scale = Math.sqrt(newWidth / CONSTANTS.EDGE.DEFAULT_WIDTH); + updateData.dashes = [15 * scale, 10 * scale]; + } + + updateData.arrows = { + to: { + enabled: true, + scaleFactor: 0.8, + type: "triangle", + }, + }; + + updateData.color = { + color: "{{ CREWAI_ORANGE }}", + highlight: "{{ CREWAI_ORANGE }}", + hover: "{{ CREWAI_ORANGE }}", + inherit: "to", + }; + + this.edges.update(updateData); + } else { + const currentOpacity = edge.opacity !== undefined ? edge.opacity : 1.0; + const targetOpacity = 0.25; + const newOpacity = currentOpacity + (targetOpacity - currentOpacity) * eased; + + const currentWidth = edge.width !== undefined ? edge.width : CONSTANTS.EDGE.DEFAULT_WIDTH; + const targetWidth = 1; + const newWidth = currentWidth + (targetWidth - currentWidth) * eased; + + this.edges.update({ + id: edge.id, + hidden: false, + opacity: newOpacity, + width: newWidth, + color: { + color: "rgba(128, 128, 128, 0.3)", + highlight: "rgba(128, 128, 128, 0.3)", + }, + shadow: { + enabled: false, + }, + }); + } + }); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + animate(); + } + + drawHighlightLayer() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + if (this.highlightedNodes.length === 0) return; + + this.highlightedNodes.forEach((nodeId) => { + const nodePosition = this.network.getPositions([nodeId])[nodeId]; + if (!nodePosition) return; + + const canvasPos = this.network.canvasToDOM(nodePosition); + const node = this.nodes.get(nodeId); + if (!node || !node.nodeStyle) return; + + const nodeStyle = node.nodeStyle; + const scale = 1.0; + const width = CONSTANTS.NODE.BASE_WIDTH * scale; + const height = CONSTANTS.NODE.BASE_HEIGHT * scale; + + this.ctx.save(); + + this.ctx.shadowColor = "transparent"; + this.ctx.shadowBlur = 0; + this.ctx.shadowOffsetX = 0; + this.ctx.shadowOffsetY = 0; + + const radius = CONSTANTS.NODE.BORDER_RADIUS * scale; + const rectX = canvasPos.x - width / 2; + const rectY = canvasPos.y - height / 2; + + drawRoundedRect(this.ctx, rectX, rectY, width, height, radius); + + this.ctx.fillStyle = nodeStyle.bgColor; + this.ctx.fill(); + + this.ctx.shadowColor = "transparent"; + this.ctx.shadowBlur = 0; + + this.ctx.strokeStyle = "{{ CREWAI_ORANGE }}"; + this.ctx.lineWidth = nodeStyle.borderWidth * scale; + this.ctx.stroke(); + + this.ctx.fillStyle = nodeStyle.fontColor; + this.ctx.font = `500 ${15 * scale}px Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`; + this.ctx.textAlign = "center"; + this.ctx.textBaseline = "middle"; + this.ctx.fillText(nodeStyle.name, canvasPos.x, canvasPos.y); + + this.ctx.restore(); + }); + } + + clear() { + const allNodesList = this.nodes.get(); + const nodeRestoreAnimStart = performance.now(); + const nodeRestoreAnimDuration = CONSTANTS.ANIMATION.DURATION; + + const animate = () => { + const elapsed = performance.now() - nodeRestoreAnimStart; + const progress = Math.min(elapsed / nodeRestoreAnimDuration, 1); + const eased = CONSTANTS.ANIMATION.EASE_OUT_CUBIC(progress); + + allNodesList.forEach((node) => { + const currentOpacity = node.opacity !== undefined ? node.opacity : 1.0; + const targetOpacity = 1.0; + const newOpacity = + currentOpacity + (targetOpacity - currentOpacity) * eased; + this.nodes.update({ id: node.id, opacity: newOpacity }); + }); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + animate(); + + const edgeIds = this.edges.getIds(); + const edgeRestoreAnimStart = performance.now(); + const edgeRestoreAnimDuration = CONSTANTS.ANIMATION.DURATION; + + const animateEdges = () => { + const elapsed = performance.now() - edgeRestoreAnimStart; + const progress = Math.min(elapsed / edgeRestoreAnimDuration, 1); + const eased = CONSTANTS.ANIMATION.EASE_OUT_CUBIC(progress); + + edgeIds.forEach((edgeId) => { + if (this.activeDrawerEdges.includes(edgeId)) { + return; } - function clearTriggeredByHighlight() { - const allNodesList = nodes.get(); - const nodeRestoreAnimStart = performance.now(); - const nodeRestoreAnimDuration = 300; + const edge = this.edges.get(edgeId); + if (!edge) return; - function animateNodeRestore() { - if (isAnimating) { - nodeRestoreAnimationId = null; - return; - } + const defaultColor = + edge.dashes || edge.label === "AND" + ? "{{ CREWAI_ORANGE }}" + : getComputedStyle(document.documentElement).getPropertyValue('--edge-or-color').trim(); + const currentOpacity = edge.opacity !== undefined ? edge.opacity : 1.0; + const currentWidth = + edge.width !== undefined ? edge.width : CONSTANTS.EDGE.DEFAULT_WIDTH; + const currentShadowSize = + edge.shadow && edge.shadow.size !== undefined + ? edge.shadow.size + : CONSTANTS.EDGE.DEFAULT_SHADOW_SIZE; - const elapsed = performance.now() - nodeRestoreAnimStart; - const progress = Math.min(elapsed / nodeRestoreAnimDuration, 1); - const eased = 1 - Math.pow(1 - progress, 3); + const targetOpacity = 1.0; + const targetWidth = CONSTANTS.EDGE.DEFAULT_WIDTH; + const targetShadowSize = CONSTANTS.EDGE.DEFAULT_SHADOW_SIZE; - allNodesList.forEach(function (node) { - const currentOpacity = node.opacity !== undefined ? node.opacity : 1.0; - const targetOpacity = 1.0; - const newOpacity = currentOpacity + (targetOpacity - currentOpacity) * eased; - nodes.update({id: node.id, opacity: newOpacity}); - }); + const newOpacity = + currentOpacity + (targetOpacity - currentOpacity) * eased; + const newWidth = currentWidth + (targetWidth - currentWidth) * eased; + const newShadowSize = + currentShadowSize + (targetShadowSize - currentShadowSize) * eased; - if (progress < 1) { - nodeRestoreAnimationId = requestAnimationFrame(animateNodeRestore); - } else { - nodeRestoreAnimationId = null; - } - } + const updateData = { + id: edge.id, + hidden: false, + opacity: newOpacity, + width: newWidth, + color: { + color: defaultColor, + highlight: defaultColor, + hover: defaultColor, + inherit: false, + }, + shadow: { + enabled: true, + color: "rgba(0,0,0,0.08)", + size: newShadowSize, + x: 1, + y: 1, + }, + font: { + color: "transparent", + background: "transparent", + }, + arrows: { + to: { + enabled: true, + scaleFactor: 0.8, + type: "triangle", + }, + }, + }; - if (nodeRestoreAnimationId) { - cancelAnimationFrame(nodeRestoreAnimationId); - } - nodeRestoreAnimationId = requestAnimationFrame(animateNodeRestore); - - const allEdgesList = edges.get(); - const edgeRestoreAnimStart = performance.now(); - const edgeRestoreAnimDuration = 300; - - function animateEdgeRestore() { - if (isAnimating) { - edgeRestoreAnimationId = null; - return; - } - - const elapsed = performance.now() - edgeRestoreAnimStart; - const progress = Math.min(elapsed / edgeRestoreAnimDuration, 1); - const eased = 1 - Math.pow(1 - progress, 3); - - allEdgesList.forEach(function (edge) { - if (activeDrawerEdges.includes(edge.id)) { - return; - } - - const defaultColor = edge.dashes || edge.label === 'AND' ? '{{ CREWAI_ORANGE }}' : '{{ GRAY }}'; - const currentOpacity = edge.opacity !== undefined ? edge.opacity : 1.0; - const currentWidth = edge.width !== undefined ? edge.width : 2; - const currentShadowSize = edge.shadow && edge.shadow.size !== undefined ? edge.shadow.size : 4; - - const targetOpacity = 1.0; - const targetWidth = 2; - const targetShadowSize = 4; - - const newOpacity = currentOpacity + (targetOpacity - currentOpacity) * eased; - const newWidth = currentWidth + (targetWidth - currentWidth) * eased; - const newShadowSize = currentShadowSize + (targetShadowSize - currentShadowSize) * eased; - - const updateData = { - id: edge.id, - hidden: false, - opacity: newOpacity, - width: newWidth, - color: { - color: defaultColor, - highlight: defaultColor - }, - shadow: { - enabled: true, - color: 'rgba(0,0,0,0.08)', - size: newShadowSize, - x: 1, - y: 1 - }, - font: { - color: 'transparent', - background: 'transparent' - } - }; - - if (edge.dashes) { - const scale = Math.sqrt(newWidth / 2); - updateData.dashes = [15 * scale, 10 * scale]; - } - - edges.update(updateData); - }); - - if (progress < 1) { - edgeRestoreAnimationId = requestAnimationFrame(animateEdgeRestore); - } else { - edgeRestoreAnimationId = null; - } - } - - if (edgeRestoreAnimationId) { - cancelAnimationFrame(edgeRestoreAnimationId); - } - edgeRestoreAnimationId = requestAnimationFrame(animateEdgeRestore); - - highlightedNodes = []; - highlightedEdges = []; - - highlightCanvas.style.transition = 'opacity 300ms ease-out'; - highlightCanvas.style.opacity = '0'; - setTimeout(function () { - highlightCanvas.classList.remove('visible'); - highlightCanvas.style.opacity = '1'; - highlightCanvas.style.transition = ''; - highlightCtx.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height); - }, 300); + if (edge.dashes) { + const scale = Math.sqrt(newWidth / CONSTANTS.EDGE.DEFAULT_WIDTH); + updateData.dashes = [15 * scale, 10 * scale]; } - network.on('afterDrawing', function () { - if (highlightCanvas.classList.contains('visible')) { - drawHighlightLayer(); - } - }); + this.edges.update(updateData); + }); - let hoveredNodeId = null; - let pressedNodeId = null; - let isClicking = false; - let activeDrawerNodeId = null; - let activeDrawerEdges = []; + if (progress < 1) { + requestAnimationFrame(animateEdges); + } + }; - const edgeAnimations = {}; + animateEdges(); - function animateEdgeWidth(edgeId, targetWidth, duration) { - if (edgeAnimations[edgeId]) { - cancelAnimationFrame(edgeAnimations[edgeId].frameId); - } + this.highlightedNodes = []; + this.highlightedEdges = []; - const edge = edges.get(edgeId); - if (!edge) return; + this.canvas.style.transition = "opacity 300ms ease-out"; + this.canvas.style.opacity = "0"; + setTimeout(() => { + this.canvas.classList.remove("visible"); + this.canvas.style.opacity = "1"; + this.canvas.style.transition = ""; + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.network.redraw(); + }, 300); + } +} - const startWidth = edge.width || 2; - const startTime = performance.now(); +// ============================================================================ +// Drawer Manager +// ============================================================================ - function animate() { - const currentTime = performance.now(); - const elapsed = currentTime - startTime; - const progress = Math.min(elapsed / duration, 1); - const eased = 1 - Math.pow(1 - progress, 3); - const currentWidth = startWidth + (targetWidth - startWidth) * eased; +class DrawerManager { + constructor(network, nodes, edges, animationManager, triggeredByHighlighter) { + this.network = network; + this.nodes = nodes; + this.edges = edges; + this.animationManager = animationManager; + this.triggeredByHighlighter = triggeredByHighlighter; - edges.update({ - id: edgeId, - width: currentWidth - }); + this.elements = { + drawer: document.getElementById("drawer"), + overlay: document.getElementById("drawer-overlay"), + title: document.getElementById("drawer-node-name"), + content: document.getElementById("drawer-content"), + openIdeButton: document.getElementById("drawer-open-ide"), + closeButton: document.getElementById("drawer-close"), + navControls: document.querySelector(".nav-controls"), + }; - if (progress < 1) { - edgeAnimations[edgeId] = { - frameId: requestAnimationFrame(animate) - }; - } else { - delete edgeAnimations[edgeId]; - } - } + this.activeNodeId = null; + this.activeEdges = []; - animate(); - } + this.setupEventListeners(); + } - network.on('hoverNode', function (params) { - const nodeId = params.node; - hoveredNodeId = nodeId; - document.body.style.cursor = 'pointer'; - network.redraw(); - }); + setupEventListeners() { + this.elements.overlay.addEventListener("click", () => this.close()); + this.elements.closeButton.addEventListener("click", () => this.close()); - network.on('blurNode', function (params) { - const nodeId = params.node; - if (hoveredNodeId === nodeId) { - hoveredNodeId = null; - } - document.body.style.cursor = 'default'; - network.redraw(); - }); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + this.close(); + } + }); + } - let pressedEdges = []; + open(nodeName, metadata) { + this.elements.title.textContent = nodeName; + this.setupIdeButton(metadata); + this.renderContent(nodeName, metadata); + this.animateOpen(); + } - network.on('selectNode', function (params) { - if (params.nodes.length > 0) { - const nodeId = params.nodes[0]; - pressedNodeId = nodeId; + setupIdeButton(metadata) { + if (metadata.source_file && metadata.source_start_line) { + this.elements.openIdeButton.style.display = "flex"; + this.elements.openIdeButton.onclick = () => this.openInIDE(metadata); + } else { + this.elements.openIdeButton.style.display = "none"; + } + } - const connectedEdges = network.getConnectedEdges(nodeId); - pressedEdges = connectedEdges; + openInIDE(metadata) { + const filePath = metadata.source_file; + const lineNum = metadata.source_start_line; + const detectedIDE = this.detectIDE(); - network.redraw(); - } - }); + const ideUrls = { + pycharm: `pycharm://open?file=${filePath}&line=${lineNum}`, + vscode: `vscode://file/${filePath}:${lineNum}`, + jetbrains: `jetbrains://open?file=${encodeURIComponent(filePath)}&line=${lineNum}`, + auto: `pycharm://open?file=${filePath}&line=${lineNum}`, + }; - network.on('deselectNode', function (params) { - if (pressedNodeId) { - const nodeId = pressedNodeId; + const ideUrl = ideUrls[detectedIDE] || ideUrls.auto; - setTimeout(function () { - if (isClicking) { - isClicking = false; - pressedNodeId = null; - pressedEdges = []; - return; - } + const link = document.createElement("a"); + link.href = ideUrl; + link.target = "_blank"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); - pressedNodeId = null; + const fallbackText = `${filePath}:${lineNum}`; + navigator.clipboard.writeText(fallbackText).catch((err) => { + console.error("Failed to copy:", err); + }); + } - pressedEdges.forEach(function (edgeId) { - if (!activeDrawerEdges.includes(edgeId)) { - animateEdgeWidth(edgeId, 2, 150); - } - }); - pressedEdges = []; - network.redraw(); - }, 10); - } - }); - let highlightedNodeId = null; - let highlightedSourceNodeId = null; - let highlightedEdgeId = null; - let highlightTimeout = null; - let originalNodeData = null; - let originalSourceNodeData = null; - let originalEdgeData = null; - let isAnimating = false; + detectIDE() { + const savedIDE = localStorage.getItem("preferred_ide"); + if (savedIDE) return savedIDE; + if (navigator.userAgent.includes("JetBrains")) return "jetbrains"; + return "auto"; + } - function clearHighlights() { - isAnimating = false; + renderContent(nodeName, metadata) { + let content = ""; - if (highlightTimeout) { - clearTimeout(highlightTimeout); - highlightTimeout = null; - } + content += this.renderMetadata(metadata); - if (originalNodeData && originalNodeData.originalOpacities) { - originalNodeData.originalOpacities.forEach((opacity, nodeId) => { - nodes.update({ - id: nodeId, - opacity: opacity - }); - }); - } + if (metadata.source_code) { + content += this.renderSourceCode(metadata); + } - if (originalNodeData && originalNodeData.originalEdgesMap) { - originalNodeData.originalEdgesMap.forEach((edgeData, edgeId) => { - edges.update({ - id: edgeId, - opacity: edgeData.opacity, - width: edgeData.width, - color: edgeData.color - }); - }); - } + this.elements.content.innerHTML = content; + this.attachContentEventListeners(nodeName); + } - if (highlightedNodeId) { - if (originalNodeData && originalNodeData.shadow) { - nodes.update({ - id: highlightedNodeId, - shadow: originalNodeData.shadow - }); - } else { - nodes.update({ - id: highlightedNodeId, - shadow: { - enabled: true, - color: 'rgba(0,0,0,0.1)', - size: 8, - x: 2, - y: 2 - } - }); - } - highlightedNodeId = null; - originalNodeData = null; - } + renderTriggerCondition(metadata) { + if (metadata.trigger_condition) { + return this.renderConditionTree(metadata.trigger_condition); + } else if (metadata.trigger_methods) { + return ` +
${highlightedClassSignature}
- ${highlightedClassSignature}