diff --git a/src/repello_agent_wiz/visualizers/agent_ui/package-lock.json b/src/repello_agent_wiz/visualizers/agent_ui/package-lock.json index 2aaf9f5..c6c276d 100644 --- a/src/repello_agent_wiz/visualizers/agent_ui/package-lock.json +++ b/src/repello_agent_wiz/visualizers/agent_ui/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^7.3.4", "axios": "^1.12.2", "dagre": "^0.8.5", + "html-to-image": "^1.11.13", "react": "^18.3.1", "react-dom": "^18.3.1", "react-flow-renderer": "^10.3.17", @@ -65,6 +66,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -381,6 +383,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -424,6 +427,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1207,6 +1211,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.4", @@ -2363,6 +2368,7 @@ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -2384,6 +2390,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2459,6 +2466,7 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -2711,6 +2719,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2861,6 +2870,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3098,6 +3108,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -3344,6 +3355,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3872,6 +3884,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4468,6 +4486,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4480,6 +4499,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4814,6 +4834,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4866,6 +4887,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4961,6 +4983,7 @@ "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5054,6 +5077,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5094,21 +5118,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/repello_agent_wiz/visualizers/agent_ui/package.json b/src/repello_agent_wiz/visualizers/agent_ui/package.json index f35a3cd..bea12bc 100644 --- a/src/repello_agent_wiz/visualizers/agent_ui/package.json +++ b/src/repello_agent_wiz/visualizers/agent_ui/package.json @@ -16,6 +16,7 @@ "@mui/material": "^7.3.4", "axios": "^1.12.2", "dagre": "^0.8.5", + "html-to-image": "^1.11.13", "react": "^18.3.1", "react-dom": "^18.3.1", "react-flow-renderer": "^10.3.17", diff --git a/src/repello_agent_wiz/visualizers/agent_ui/src/App.tsx b/src/repello_agent_wiz/visualizers/agent_ui/src/App.tsx index ba1a61f..1f9a871 100644 --- a/src/repello_agent_wiz/visualizers/agent_ui/src/App.tsx +++ b/src/repello_agent_wiz/visualizers/agent_ui/src/App.tsx @@ -1,7 +1,8 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef, useEffect } from 'react'; import { Box, AppBar, Toolbar, Typography, Switch, Drawer, Card, List, ListItem, ButtonGroup, Button, Snackbar, TextField, InputAdornment } from '@mui/material'; import ReactFlow, { MiniMap, Controls, Background, ReactFlowProvider, useReactFlow, useNodesState, useEdgesState, addEdge, ConnectionLineType} from 'reactflow'; import type {Node, Edge, Connection} from 'reactflow'; +import { toPng } from 'html-to-image'; import { loadGraph } from './graphLoader'; import SearchIcon from './assets/search.svg'; import AgentIcon from './assets/agent.svg'; @@ -22,15 +23,23 @@ const { nodes: initialNodes, edges: initialEdges, framework } = loadGraph(); let nodeIdCounter = initialNodes.length + 1; const getId = () => `new-node-${nodeIdCounter++}`; -function FlowCanvas({ nodes, edges, setNodes, setEdges, highlighted, onNodeClick, onEdgeClick, onNodesChange, onEdgesChange }: { +function FlowCanvas({ nodes, edges, setNodes, setEdges, highlighted, onNodeClick, onEdgeClick, onNodesChange, onEdgesChange, exportRef, themeDark }: { nodes: Node[]; edges: Edge[]; setNodes: any; setEdges: any; highlighted: string | null; onNodeClick: (e: any, node: any) => void; onEdgeClick: (e: any, edge: any) => void; onNodesChange: any; onEdgesChange: any; + exportRef?: React.MutableRefObject; + themeDark: boolean; }) { const [search, setSearch] = useState(''); const { setCenter, fitView, screenToFlowPosition } = useReactFlow(); + useEffect(() => { + if (exportRef) { + exportRef.current = { fitView, nodes }; + } + }, [exportRef, fitView, nodes]); + const onConnect = useCallback( (params: Connection) => setEdges((eds: Edge[]) => addEdge(params, eds)), [setEdges] @@ -112,6 +121,25 @@ function FlowCanvas({ nodes, edges, setNodes, setEdges, highlighted, onNodeClick padding: 6, }); + const getEdgeStyle = (edge: Edge) => { + const baseStyle = edge.style || {}; + if (themeDark) { + return { + ...baseStyle, + stroke: baseStyle.stroke || '#5568d3', + strokeWidth: baseStyle.strokeWidth || 3, + opacity: baseStyle.opacity || 0.85, + }; + } else { + return { + ...baseStyle, + stroke: baseStyle.stroke || '#4A90E2', + strokeWidth: baseStyle.strokeWidth || 2.5, + opacity: baseStyle.opacity || 0.75, + }; + } + }; + return ( <> @@ -157,7 +185,7 @@ function FlowCanvas({ nodes, edges, setNodes, setEdges, highlighted, onNodeClick ({ ...n, style: getNodeStyle(n.id, n.data?.color) }))} - edges={edges} + edges={edges.map(e => ({ ...e, style: getEdgeStyle(e) }))} nodeTypes={nodeTypes} edgeTypes={edgeTypes} connectionLineComponent={FloatingConnectionLine} @@ -200,6 +228,7 @@ function App() { const [selectedDetails, setSelectedDetails] = useState(null); const [snackbarOpen, setSnackbarOpen] = useState(false); const [highlighted, setHighlighted] = useState(null); + const exportRef = useRef(null); const handleNodeClick = useCallback((_e: any, node: any) => { setSelectedDetails({ type: 'node', ...node.data, id: node.id, position: node.position }); @@ -215,20 +244,84 @@ function App() { const handleCloseDrawer = () => { setDrawerOpen(false); setSelectedDetails(null); setHighlighted(null); }; const handleThemeToggle = () => setThemeDark((p) => !p); const handleSnackbarClose = () => setSnackbarOpen(false); + const handleResetLayout = useCallback(() => { + setNodes(initialNodes); + setEdges(initialEdges); + setHighlighted(null); + setSnackbarOpen(true); + }, [setNodes, setEdges]); const onDragStart = (event: React.DragEvent, nodeType: string) => { event.dataTransfer.setData('application/reactflow', nodeType); event.dataTransfer.effectAllowed = 'move'; }; + const handleExport = useCallback(() => { + if (!exportRef.current || !exportRef.current.fitView) return; + + const minMap = document.querySelector('.react-flow__minimap'); + const controls = document.querySelector('.react-flow__controls'); + + if (minMap) (minMap as HTMLElement).style.display = 'none'; + if (controls) (controls as HTMLElement).style.display = 'none'; + + exportRef.current.fitView({ padding: 0.2, duration: 200 }); + + setTimeout(() => { + const container = document.querySelector('.react-flow') as HTMLElement; + if (!container) { + if (minMap) (minMap as HTMLElement).style.display = ''; + if (controls) (controls as HTMLElement).style.display = ''; + return; + } + + toPng(container, { + backgroundColor: themeDark ? '#0f1720' : '#fbfcff', + pixelRatio: 2, + cacheBust: true, + }).then((dataUrl: string) => { + const link = document.createElement('a'); + link.download = 'agent-workflow.png'; + link.href = dataUrl; + link.click(); + + if (minMap) (minMap as HTMLElement).style.display = ''; + if (controls) (controls as HTMLElement).style.display = ''; + }).catch((err: any) => { + console.error('Export failed:', err); + if (minMap) (minMap as HTMLElement).style.display = ''; + if (controls) (controls as HTMLElement).style.display = ''; + }); + }, 300); + }, [themeDark]); + return ( - + - Agent-Workflow ({framework}) + Agent-Workflow ({framework}) - {themeDark ? 'Dark' : 'Light'} - + {themeDark ? 'Dark' : 'Light'} + @@ -288,8 +381,8 @@ function App() { - - + + @@ -305,6 +398,8 @@ function App() { onEdgeClick={handleEdgeClick} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} + exportRef={exportRef} + themeDark={themeDark} /> @@ -353,7 +448,8 @@ function App() { - + +