├── .env.example ├── screenshot-1.png ├── screenshot-2.png ├── src ├── contexts │ ├── ThemeContextDef.js │ └── ThemeContext.jsx ├── hooks │ └── useTheme.js ├── config │ └── nodeTypes.js ├── main.jsx ├── components │ ├── ThemeToggle.jsx │ ├── SystemDiagram.jsx │ ├── CustomNodes.jsx │ ├── UploadZone.jsx │ ├── InfoPanel.jsx │ └── MermaidDisplay.jsx ├── index.css ├── assets │ └── react.svg ├── themes │ └── index.js ├── services │ └── analysisService.js └── App.jsx ├── postcss.config.js ├── vite.config.js ├── tailwind.config.js ├── index.html ├── .gitignore ├── eslint.config.js ├── package.json ├── public └── vite.svg └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | VITE_OPENAI_API_KEY=your_sk_key_here -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mallahyari/system-design-visualizer/HEAD/screenshot-1.png -------------------------------------------------------------------------------- /screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mallahyari/system-design-visualizer/HEAD/screenshot-2.png -------------------------------------------------------------------------------- /src/contexts/ThemeContextDef.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const ThemeContext = createContext(undefined); 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useTheme.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ThemeContext } from "../contexts/ThemeContextDef"; 3 | 4 | export const useTheme = () => { 5 | const context = useContext(ThemeContext); 6 | if (context === undefined) { 7 | throw new Error("useTheme must be used within a ThemeProvider"); 8 | } 9 | return context; 10 | }; 11 | -------------------------------------------------------------------------------- /src/config/nodeTypes.js: -------------------------------------------------------------------------------- 1 | import { 2 | CacheNode, 3 | ClientNode, 4 | DatabaseNode, 5 | LoadBalancerNode, 6 | ServerNode, 7 | } from "../components/CustomNodes"; 8 | 9 | export const nodeTypes = { 10 | databaseNode: DatabaseNode, 11 | serverNode: ServerNode, 12 | clientNode: ClientNode, 13 | loadBalancerNode: LoadBalancerNode, 14 | cacheNode: CacheNode, 15 | }; 16 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import { ThemeProvider } from "./contexts/ThemeContext.jsx"; 5 | import "./index.css"; 6 | 7 | createRoot(document.getElementById("root")).render( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interactive-system-design 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Environment variables 27 | .env 28 | .env.* 29 | !.env.example 30 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs.flat.recommended, 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interactive-system-design", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "clsx": "^2.1.1", 14 | "lucide-react": "^0.555.0", 15 | "mermaid": "^11.12.1", 16 | "react": "^19.2.0", 17 | "react-dom": "^19.2.0", 18 | "reactflow": "^11.11.4", 19 | "tailwind-merge": "^3.4.0" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.39.1", 23 | "@tailwindcss/postcss": "^4.1.17", 24 | "@types/react": "^19.2.5", 25 | "@types/react-dom": "^19.2.3", 26 | "@vitejs/plugin-react": "^5.1.1", 27 | "autoprefixer": "^10.4.22", 28 | "eslint": "^9.39.1", 29 | "eslint-plugin-react-hooks": "^7.0.1", 30 | "eslint-plugin-react-refresh": "^0.4.24", 31 | "globals": "^16.5.0", 32 | "postcss": "^8.5.6", 33 | "tailwindcss": "^4.1.17", 34 | "vite": "^7.2.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.jsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { useTheme } from "../hooks/useTheme"; 3 | 4 | const ThemeToggle = () => { 5 | const { themeName, toggleTheme } = useTheme(); 6 | const isDark = themeName === "dark"; 7 | 8 | return ( 9 | 36 | ); 37 | }; 38 | 39 | export default ThemeToggle; 40 | -------------------------------------------------------------------------------- /src/components/SystemDiagram.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import ReactFlow, { 3 | addEdge, 4 | Background, 5 | Controls, 6 | useEdgesState, 7 | useNodesState, 8 | } from "reactflow"; 9 | import "reactflow/dist/style.css"; 10 | import { nodeTypes } from "../config/nodeTypes"; 11 | import { useTheme } from "../hooks/useTheme"; 12 | 13 | const SystemDiagram = ({ initialNodes, initialEdges, onNodeClick }) => { 14 | const [nodes, , onNodesChange] = useNodesState(initialNodes); 15 | const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); 16 | const { themeName } = useTheme(); 17 | 18 | const nodeTypesMemo = React.useMemo(() => nodeTypes, []); 19 | 20 | const onConnect = useCallback( 21 | (params) => setEdges((eds) => addEdge(params, eds)), 22 | [setEdges] 23 | ); 24 | 25 | // Dynamic colors based on theme 26 | const bgColor = themeName === "dark" ? "#333" : "#ccc"; 27 | 28 | return ( 29 |
36 | onNodeClick(node)} 44 | fitView 45 | style={{ backgroundColor: "var(--bg-primary)" }} 46 | proOptions={{ hideAttribution: true }} 47 | > 48 | 49 | 50 | 51 |
52 | ); 53 | }; 54 | 55 | export default SystemDiagram; 56 | -------------------------------------------------------------------------------- /src/contexts/ThemeContext.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { applyTheme, getTheme, themes } from "../themes"; 3 | import { ThemeContext } from "./ThemeContextDef"; 4 | 5 | const THEME_STORAGE_KEY = "sdv-theme"; 6 | 7 | export const ThemeProvider = ({ children }) => { 8 | const [themeName, setThemeName] = useState(() => { 9 | // Check localStorage first 10 | const stored = localStorage.getItem(THEME_STORAGE_KEY); 11 | if (stored && themes[stored]) { 12 | return stored; 13 | } 14 | // Check system preference 15 | if (window.matchMedia("(prefers-color-scheme: light)").matches) { 16 | return "light"; 17 | } 18 | return "dark"; 19 | }); 20 | 21 | const theme = getTheme(themeName); 22 | 23 | useEffect(() => { 24 | applyTheme(theme); 25 | localStorage.setItem(THEME_STORAGE_KEY, themeName); 26 | }, [themeName, theme]); 27 | 28 | // Listen for system preference changes 29 | useEffect(() => { 30 | const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); 31 | const handleChange = (e) => { 32 | const stored = localStorage.getItem(THEME_STORAGE_KEY); 33 | // Only auto-switch if user hasn't explicitly set a preference 34 | if (!stored) { 35 | setThemeName(e.matches ? "dark" : "light"); 36 | } 37 | }; 38 | 39 | mediaQuery.addEventListener("change", handleChange); 40 | return () => mediaQuery.removeEventListener("change", handleChange); 41 | }, []); 42 | 43 | const toggleTheme = () => { 44 | setThemeName((prev) => (prev === "dark" ? "light" : "dark")); 45 | }; 46 | 47 | const setTheme = (name) => { 48 | if (themes[name]) { 49 | setThemeName(name); 50 | } 51 | }; 52 | 53 | return ( 54 | 57 | {children} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 5 | line-height: 1.5; 6 | font-weight: 400; 7 | 8 | /* Default to dark theme - these will be overridden by ThemeContext */ 9 | --bg-primary: #020617; 10 | --bg-secondary: #0f172a; 11 | --bg-tertiary: #1e293b; 12 | --bg-elevated: rgba(15, 23, 42, 0.7); 13 | --bg-overlay: rgba(0, 0, 0, 0.2); 14 | 15 | --text-primary: #f8fafc; 16 | --text-secondary: #94a3b8; 17 | --text-tertiary: #64748b; 18 | --text-muted: #475569; 19 | 20 | --border-primary: rgba(255, 255, 255, 0.1); 21 | --border-secondary: rgba(255, 255, 255, 0.05); 22 | --border-hover: rgba(255, 255, 255, 0.2); 23 | 24 | --interactive-bg: #1e293b; 25 | --interactive-hover: #334155; 26 | --interactive-active: #475569; 27 | 28 | --accent-blue: #3b82f6; 29 | --accent-blue-hover: #2563eb; 30 | --accent-blue-glow: rgba(59, 130, 246, 0.2); 31 | --accent-emerald: #10b981; 32 | --accent-purple: #a855f7; 33 | --accent-orange: #f97316; 34 | --accent-yellow: #eab308; 35 | 36 | --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); 37 | --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3); 38 | --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.3); 39 | --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4); 40 | 41 | --node-bg: #111827; 42 | --node-border: #374151; 43 | --node-border-hover: #4b5563; 44 | --panel-bg: rgba(17, 24, 39, 0.95); 45 | 46 | --mermaid-theme: dark; 47 | } 48 | 49 | body { 50 | margin: 0; 51 | min-width: 320px; 52 | min-height: 100vh; 53 | background-color: var(--bg-primary); 54 | color: var(--text-primary); 55 | transition: background-color 0.3s ease, color 0.3s ease; 56 | } 57 | 58 | /* Smooth theme transitions */ 59 | *, 60 | *::before, 61 | *::after { 62 | transition: background-color 0.2s ease, border-color 0.2s ease, 63 | color 0.2s ease, box-shadow 0.2s ease; 64 | } 65 | 66 | /* ReactFlow overrides for theming */ 67 | .react-flow__controls { 68 | background-color: var(--bg-tertiary) !important; 69 | border-color: var(--border-primary) !important; 70 | box-shadow: var(--shadow-lg) !important; 71 | } 72 | 73 | .react-flow__controls-button { 74 | background-color: var(--bg-tertiary) !important; 75 | border-color: var(--border-primary) !important; 76 | fill: var(--text-secondary) !important; 77 | } 78 | 79 | .react-flow__controls-button:hover { 80 | background-color: var(--interactive-hover) !important; 81 | } 82 | 83 | .react-flow__background { 84 | background-color: var(--bg-primary) !important; 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # System Design Visualizer 2 | 3 | An interactive tool that transforms static system design diagrams into explorable, interactive visualizations using AI. 4 | 5 | ## 📸 Screenshots 6 | 7 | ![Dashboard View](screenshot-1.png) 8 | *Original Image and Mermaid Diagram side-by-side* 9 | 10 | ![Interactive Graph](screenshot-2.png) 11 | *Interactive React Flow Graph* 12 | 13 | ## 🚀 Features 14 | 15 | - **AI-Powered Analysis**: Upload any system design image (architecture diagrams, flowcharts, etc.). 16 | - **Mermaid Generation**: Automatically converts images into editable Mermaid.js diagrams. 17 | - **Interactive Visualization**: Converts Mermaid diagrams into interactive React Flow graphs. 18 | - **Deep Dive**: Click on any component (Load Balancer, Database, etc.) to see inferred details like technology stack and role. 19 | - **Premium UI**: A modern, dark-themed dashboard with zoom, pan, and copy controls. 20 | 21 | ## 🛠️ Tech Stack 22 | 23 | - **Frontend**: React, Vite, Tailwind CSS 24 | - **Visualization**: React Flow, Mermaid.js 25 | - **AI**: OpenAI GPT-4o (Vision & Code Generation) 26 | - **Icons**: Lucide React 27 | 28 | ## 🏃‍♂️ Running Locally 29 | 30 | ### Prerequisites 31 | 32 | - Node.js (v18 or higher) 33 | - An OpenAI API Key (for AI analysis features) 34 | 35 | ### Installation 36 | 37 | 1. **Clone the repository** 38 | ```bash 39 | git clone https://github.com/mallahyari/system-design-visualizer.git 40 | cd system-design-visualizer 41 | ``` 42 | 43 | 2. **Install dependencies** 44 | ```bash 45 | npm install 46 | ``` 47 | 48 | 3. **Configure Environment** 49 | Create a `.env` file in the root directory: 50 | ```bash 51 | touch .env 52 | ``` 53 | Add your OpenAI API key: 54 | ```env 55 | VITE_OPENAI_API_KEY=your_sk_key_here 56 | ``` 57 | > **Note**: If no API key is provided, the app will run in **Mock Mode**, generating sample data for testing. 58 | 59 | 4. **Start the Development Server** 60 | ```bash 61 | npm run dev 62 | ``` 63 | 64 | 5. **Open in Browser** 65 | Navigate to `http://localhost:5173` to see the app in action. 66 | 67 | ## 📸 Workflow 68 | 69 | 1. **Upload**: Drag & drop your system design image. 70 | 2. **Review**: See the generated Mermaid diagram code and preview. Use the toolbar to zoom or copy the code. 71 | 3. **Convert**: Click "Convert to Interactive" to generate the node-based graph. 72 | 4. **Explore**: Interact with the graph nodes to learn more about your system's architecture. 73 | 74 | ## 🤝 Contributing 75 | 76 | Contributions are welcome! Please feel free to submit a Pull Request. 77 | 78 | ## 📄 License 79 | 80 | This project is licensed under the MIT License. 81 | -------------------------------------------------------------------------------- /src/components/CustomNodes.jsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Database, Globe, Layers, Server, Smartphone } from "lucide-react"; 3 | import { memo } from "react"; 4 | import { Handle, Position } from "reactflow"; 5 | 6 | const BaseNode = ({ data, icon: Icon, colorClass, isSelected }) => { 7 | return ( 8 |
19 | 25 | 26 |
27 |
33 | 34 |
35 |
36 |
40 | {data.label} 41 |
42 |
43 | {data.tech} 44 |
45 |
46 |
47 | 48 | 54 |
55 | ); 56 | }; 57 | 58 | export const DatabaseNode = memo(({ data, selected }) => ( 59 | 65 | )); 66 | 67 | export const ServerNode = memo(({ data, selected }) => ( 68 | 74 | )); 75 | 76 | export const ClientNode = memo(({ data, selected }) => ( 77 | 83 | )); 84 | 85 | export const LoadBalancerNode = memo(({ data, selected }) => ( 86 | 92 | )); 93 | 94 | export const CacheNode = memo(({ data, selected }) => ( 95 | 101 | )); 102 | -------------------------------------------------------------------------------- /src/components/UploadZone.jsx: -------------------------------------------------------------------------------- 1 | import { FileUp, Loader2 } from "lucide-react"; 2 | import { useCallback } from "react"; 3 | 4 | const UploadZone = ({ onUpload, isAnalyzing }) => { 5 | const handleDrop = useCallback( 6 | (e) => { 7 | e.preventDefault(); 8 | e.stopPropagation(); 9 | 10 | if (e.dataTransfer.files && e.dataTransfer.files[0]) { 11 | console.log("UploadZone: File dropped", e.dataTransfer.files[0]); 12 | onUpload(e.dataTransfer.files[0]); 13 | } 14 | }, 15 | [onUpload] 16 | ); 17 | 18 | const handleDragOver = (e) => { 19 | e.preventDefault(); 20 | e.stopPropagation(); 21 | }; 22 | 23 | const handleChange = (e) => { 24 | console.log("UploadZone: File input changed", e.target.files); 25 | if (e.target.files && e.target.files[0]) { 26 | onUpload(e.target.files[0]); 27 | e.target.value = null; 28 | } 29 | }; 30 | 31 | return ( 32 |
document.getElementById("file-upload").click()} 41 | > 42 | 50 | 51 | {isAnalyzing ? ( 52 | <> 53 | 57 |

61 | Analyzing System Architecture... 62 |

63 |

64 | Identifying components and relationships 65 |

66 | 67 | ) : ( 68 | <> 69 |
73 | 77 |
78 |
79 |

83 | Drop your system design here 84 |

85 |

86 | or click to browse (JPG, PNG) 87 |

88 |
89 | 90 | )} 91 |
92 | ); 93 | }; 94 | 95 | export default UploadZone; 96 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/InfoPanel.jsx: -------------------------------------------------------------------------------- 1 | import { Activity, Code2, Info, X } from "lucide-react"; 2 | 3 | const InfoPanel = ({ node, onClose }) => { 4 | if (!node) return null; 5 | 6 | return ( 7 |
15 |
22 |

26 | 27 | Component Details 28 |

29 | 44 |
45 | 46 |
47 |
48 | 54 |

58 | {node.data.label} 59 |

60 | 68 | {node.type.replace("Node", "")} 69 | 70 |
71 | 72 |
73 | 79 |

83 | {node.data.description} 84 |

85 |
86 | 87 |
88 | 94 |
95 | {node.data.tech.split(",").map((tech, i) => ( 96 | 105 | {tech.trim()} 106 | 107 | ))} 108 |
109 |
110 |
111 |
112 | ); 113 | }; 114 | 115 | export default InfoPanel; 116 | -------------------------------------------------------------------------------- /src/components/MermaidDisplay.jsx: -------------------------------------------------------------------------------- 1 | import { Check, Copy, RotateCcw, ZoomIn, ZoomOut } from "lucide-react"; 2 | import mermaid from "mermaid"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { useTheme } from "../hooks/useTheme"; 5 | 6 | const MermaidDisplay = ({ chart }) => { 7 | const containerRef = useRef(null); 8 | const [scale, setScale] = useState(1); 9 | const [copied, setCopied] = useState(false); 10 | const { themeName } = useTheme(); 11 | 12 | // Re-initialize mermaid when theme changes 13 | useEffect(() => { 14 | mermaid.initialize({ 15 | startOnLoad: true, 16 | theme: themeName === "dark" ? "dark" : "default", 17 | securityLevel: "loose", 18 | flowchart: { 19 | useMaxWidth: false, 20 | htmlLabels: true, 21 | }, 22 | }); 23 | }, [themeName]); 24 | 25 | useEffect(() => { 26 | if (containerRef.current && chart) { 27 | mermaid.render(`mermaid-${Date.now()}`, chart).then(({ svg }) => { 28 | containerRef.current.innerHTML = svg; 29 | }); 30 | } 31 | }, [chart, themeName]); 32 | 33 | const handleZoomIn = () => setScale((s) => Math.min(s + 0.2, 3)); 34 | const handleZoomOut = () => setScale((s) => Math.max(s - 0.2, 0.5)); 35 | const handleReset = () => setScale(1); 36 | 37 | const handleCopy = async () => { 38 | try { 39 | await navigator.clipboard.writeText(chart); 40 | setCopied(true); 41 | setTimeout(() => setCopied(false), 2000); 42 | } catch (err) { 43 | console.error("Failed to copy:", err); 44 | } 45 | }; 46 | 47 | return ( 48 |
52 | {/* Toolbar */} 53 |
61 | 69 | 73 | {Math.round(scale * 100)}% 74 | 75 | 83 |
87 | 95 |
99 | 113 |
114 | 115 | {/* Diagram Container */} 116 |
117 |
121 |
122 |
123 |
124 |
125 | ); 126 | }; 127 | 128 | export default MermaidDisplay; 129 | -------------------------------------------------------------------------------- /src/themes/index.js: -------------------------------------------------------------------------------- 1 | // Theme definitions using CSS custom properties 2 | // Each theme defines color tokens that map to Tailwind-style naming 3 | 4 | export const themes = { 5 | dark: { 6 | name: "dark", 7 | label: "Dark", 8 | colors: { 9 | // Background colors 10 | "--bg-primary": "#020617", // slate-950 11 | "--bg-secondary": "#0f172a", // slate-900 12 | "--bg-tertiary": "#1e293b", // slate-800 13 | "--bg-elevated": "rgba(15, 23, 42, 0.7)", // slate-900/70 14 | "--bg-overlay": "rgba(0, 0, 0, 0.2)", 15 | 16 | // Text colors 17 | "--text-primary": "#f8fafc", // slate-50 18 | "--text-secondary": "#94a3b8", // slate-400 19 | "--text-tertiary": "#64748b", // slate-500 20 | "--text-muted": "#475569", // slate-600 21 | 22 | // Border colors 23 | "--border-primary": "rgba(255, 255, 255, 0.1)", 24 | "--border-secondary": "rgba(255, 255, 255, 0.05)", 25 | "--border-hover": "rgba(255, 255, 255, 0.2)", 26 | 27 | // Interactive elements 28 | "--interactive-bg": "#1e293b", // slate-800 29 | "--interactive-hover": "#334155", // slate-700 30 | "--interactive-active": "#475569", // slate-600 31 | 32 | // Accent colors (these stay consistent across themes) 33 | "--accent-blue": "#3b82f6", 34 | "--accent-blue-hover": "#2563eb", 35 | "--accent-blue-glow": "rgba(59, 130, 246, 0.2)", 36 | "--accent-emerald": "#10b981", 37 | "--accent-purple": "#a855f7", 38 | "--accent-orange": "#f97316", 39 | "--accent-yellow": "#eab308", 40 | 41 | // Shadows 42 | "--shadow-sm": "0 1px 2px rgba(0, 0, 0, 0.3)", 43 | "--shadow-md": "0 4px 6px rgba(0, 0, 0, 0.3)", 44 | "--shadow-lg": "0 10px 15px rgba(0, 0, 0, 0.3)", 45 | "--shadow-xl": "0 20px 25px rgba(0, 0, 0, 0.4)", 46 | 47 | // Component-specific 48 | "--node-bg": "#111827", // gray-900 49 | "--node-border": "#374151", // gray-700 50 | "--node-border-hover": "#4b5563", // gray-600 51 | "--panel-bg": "rgba(17, 24, 39, 0.95)", 52 | 53 | // Mermaid theme 54 | "--mermaid-theme": "dark", 55 | }, 56 | }, 57 | 58 | light: { 59 | name: "light", 60 | label: "Light", 61 | colors: { 62 | // Background colors 63 | "--bg-primary": "#ffffff", 64 | "--bg-secondary": "#f8fafc", // slate-50 65 | "--bg-tertiary": "#f1f5f9", // slate-100 66 | "--bg-elevated": "rgba(248, 250, 252, 0.9)", 67 | "--bg-overlay": "rgba(255, 255, 255, 0.5)", 68 | 69 | // Text colors 70 | "--text-primary": "#0f172a", // slate-900 71 | "--text-secondary": "#475569", // slate-600 72 | "--text-tertiary": "#64748b", // slate-500 73 | "--text-muted": "#94a3b8", // slate-400 74 | 75 | // Border colors 76 | "--border-primary": "rgba(0, 0, 0, 0.1)", 77 | "--border-secondary": "rgba(0, 0, 0, 0.05)", 78 | "--border-hover": "rgba(0, 0, 0, 0.15)", 79 | 80 | // Interactive elements 81 | "--interactive-bg": "#f1f5f9", // slate-100 82 | "--interactive-hover": "#e2e8f0", // slate-200 83 | "--interactive-active": "#cbd5e1", // slate-300 84 | 85 | // Accent colors 86 | "--accent-blue": "#2563eb", 87 | "--accent-blue-hover": "#1d4ed8", 88 | "--accent-blue-glow": "rgba(37, 99, 235, 0.15)", 89 | "--accent-emerald": "#059669", 90 | "--accent-purple": "#9333ea", 91 | "--accent-orange": "#ea580c", 92 | "--accent-yellow": "#ca8a04", 93 | 94 | // Shadows 95 | "--shadow-sm": "0 1px 2px rgba(0, 0, 0, 0.05)", 96 | "--shadow-md": "0 4px 6px rgba(0, 0, 0, 0.07)", 97 | "--shadow-lg": "0 10px 15px rgba(0, 0, 0, 0.1)", 98 | "--shadow-xl": "0 20px 25px rgba(0, 0, 0, 0.15)", 99 | 100 | // Component-specific 101 | "--node-bg": "#ffffff", 102 | "--node-border": "#e2e8f0", // slate-200 103 | "--node-border-hover": "#cbd5e1", // slate-300 104 | "--panel-bg": "rgba(255, 255, 255, 0.95)", 105 | 106 | // Mermaid theme 107 | "--mermaid-theme": "default", 108 | }, 109 | }, 110 | }; 111 | 112 | export const getTheme = (themeName) => themes[themeName] || themes.dark; 113 | 114 | export const applyTheme = (theme) => { 115 | const root = document.documentElement; 116 | Object.entries(theme.colors).forEach(([property, value]) => { 117 | root.style.setProperty(property, value); 118 | }); 119 | 120 | // Set data-theme attribute for potential CSS selectors 121 | root.setAttribute("data-theme", theme.name); 122 | }; 123 | -------------------------------------------------------------------------------- /src/services/analysisService.js: -------------------------------------------------------------------------------- 1 | import { MarkerType } from 'reactflow'; 2 | 3 | /** 4 | * Analyzes an image using OpenAI's GPT-4o to generate a Mermaid diagram. 5 | * @param {File} imageFile 6 | * @returns {Promise} Mermaid diagram string 7 | */ 8 | export const generateMermaidFromImage = async (imageFile) => { 9 | const apiKey = import.meta.env.VITE_OPENAI_API_KEY; 10 | 11 | if (apiKey) { 12 | console.log("analysisService: Using OpenAI API for Mermaid generation"); 13 | return generateMermaidWithOpenAI(imageFile, apiKey); 14 | } else { 15 | console.log("analysisService: No API key found, using mock data"); 16 | return new Promise((resolve) => { 17 | setTimeout(() => { 18 | resolve(getMockMermaid()); 19 | }, 1500); 20 | }); 21 | } 22 | }; 23 | 24 | /** 25 | * Converts a Mermaid diagram string to React Flow nodes and edges. 26 | * @param {string} mermaidCode 27 | * @returns {Promise<{nodes: Array, edges: Array}>} 28 | */ 29 | export const convertMermaidToFlow = async (mermaidCode) => { 30 | const apiKey = import.meta.env.VITE_OPENAI_API_KEY; 31 | 32 | if (apiKey) { 33 | console.log("analysisService: Using OpenAI API for Flow conversion"); 34 | return convertToFlowWithOpenAI(mermaidCode, apiKey); 35 | } else { 36 | console.log("analysisService: No API key found, using mock data"); 37 | return new Promise((resolve) => { 38 | setTimeout(() => { 39 | resolve(getMockGraph()); 40 | }, 1500); 41 | }); 42 | } 43 | }; 44 | 45 | const generateMermaidWithOpenAI = async (file, apiKey) => { 46 | try { 47 | const base64Image = await toBase64(file); 48 | 49 | const response = await fetch('https://api.openai.com/v1/chat/completions', { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | 'Authorization': `Bearer ${apiKey}` 54 | }, 55 | body: JSON.stringify({ 56 | model: "gpt-4o", 57 | messages: [ 58 | { 59 | role: "system", 60 | content: `You are a system architecture expert. Analyze the provided system design diagram image and convert it into a Mermaid JS diagram. 61 | 62 | Return ONLY the Mermaid code string. Do not include markdown code blocks (like \`\`\`mermaid). 63 | 64 | Rules: 65 | 1. Use 'graph TD' or 'graph LR' based on the layout. 66 | 2. Use appropriate shapes for components (cylinder for databases, rect for servers, etc). 67 | 3. Ensure directionality of arrows matches the image.` 68 | }, 69 | { 70 | role: "user", 71 | content: [ 72 | { type: "text", text: "Convert this system design to Mermaid." }, 73 | { type: "image_url", image_url: { url: base64Image } } 74 | ] 75 | } 76 | ], 77 | max_tokens: 4000 78 | }) 79 | }); 80 | 81 | const data = await response.json(); 82 | 83 | if (data.error) { 84 | throw new Error(data.error.message); 85 | } 86 | 87 | let content = data.choices[0].message.content; 88 | // Clean up markdown code blocks if present 89 | content = content.replace(/^```mermaid\n/, '').replace(/^```\n/, '').replace(/```$/, ''); 90 | return content.trim(); 91 | 92 | } catch (error) { 93 | console.error("OpenAI API Error:", error); 94 | alert("Failed to analyze image with AI. Falling back to mock data."); 95 | return getMockMermaid(); 96 | } 97 | }; 98 | 99 | const convertToFlowWithOpenAI = async (mermaidCode, apiKey) => { 100 | try { 101 | const response = await fetch('https://api.openai.com/v1/chat/completions', { 102 | method: 'POST', 103 | headers: { 104 | 'Content-Type': 'application/json', 105 | 'Authorization': `Bearer ${apiKey}` 106 | }, 107 | body: JSON.stringify({ 108 | model: "gpt-4o", 109 | messages: [ 110 | { 111 | role: "system", 112 | content: `You are a system architecture expert. Convert the provided Mermaid diagram code into a structured graph format for React Flow. 113 | 114 | Return ONLY a valid JSON object (no markdown formatting) with this structure: 115 | { 116 | "nodes": [ 117 | { 118 | "id": "string", 119 | "type": "one of: clientNode, serverNode, databaseNode, loadBalancerNode, cacheNode", 120 | "position": { "x": number, "y": number }, 121 | "data": { 122 | "label": "string", 123 | "description": "brief description of role inferred from context", 124 | "tech": "inferred technologies" 125 | } 126 | } 127 | ], 128 | "edges": [ 129 | { "id": "string", "source": "nodeId", "target": "nodeId", "animated": true, "label": "optional connection label" } 130 | ] 131 | } 132 | 133 | Rules: 134 | 1. Map Mermaid shapes/names to the most appropriate node type. 135 | 2. Space out the position coordinates (x, y) so the graph is readable. 136 | 3. Ensure all source and target IDs in edges exist in the nodes array.` 137 | }, 138 | { 139 | role: "user", 140 | content: `Convert this Mermaid code to React Flow JSON:\n\n${mermaidCode}` 141 | } 142 | ], 143 | max_tokens: 4000, 144 | response_format: { type: "json_object" } 145 | }) 146 | }); 147 | 148 | const data = await response.json(); 149 | 150 | if (data.error) { 151 | throw new Error(data.error.message); 152 | } 153 | 154 | const result = JSON.parse(data.choices[0].message.content); 155 | return result; 156 | 157 | } catch (error) { 158 | console.error("OpenAI API Error:", error); 159 | alert("Failed to convert Mermaid to Flow. Falling back to mock data."); 160 | return getMockGraph(); 161 | } 162 | }; 163 | 164 | const toBase64 = (file) => new Promise((resolve, reject) => { 165 | const reader = new FileReader(); 166 | reader.readAsDataURL(file); 167 | reader.onload = () => resolve(reader.result); 168 | reader.onerror = error => reject(error); 169 | }); 170 | 171 | const getMockMermaid = () => { 172 | return `graph TD 173 | Client[Client / Browser] -->|HTTPS| LB[Load Balancer] 174 | LB --> Web1[Web Server 1] 175 | LB --> Web2[Web Server 2] 176 | Web1 --> DB[(Primary Database)] 177 | Web2 --> DB 178 | Web1 -.-> Cache[(Redis Cache)]`; 179 | }; 180 | 181 | const getMockGraph = () => { 182 | // This is a hardcoded "standard" 3-tier architecture for demo purposes 183 | const nodes = [ 184 | { 185 | id: 'client', 186 | type: 'clientNode', 187 | position: { x: 250, y: 0 }, 188 | data: { 189 | label: 'Client / Browser', 190 | description: 'The user interface running in the browser. Sends requests to the Load Balancer.', 191 | tech: 'React, Mobile App' 192 | }, 193 | }, 194 | { 195 | id: 'lb', 196 | type: 'loadBalancerNode', 197 | position: { x: 250, y: 150 }, 198 | data: { 199 | label: 'Load Balancer', 200 | description: 'Distributes incoming network traffic across multiple servers to ensure reliability and performance.', 201 | tech: 'NGINX, AWS ALB' 202 | }, 203 | }, 204 | { 205 | id: 'web-server-1', 206 | type: 'serverNode', 207 | position: { x: 100, y: 300 }, 208 | data: { 209 | label: 'Web Server 1', 210 | description: 'Handles application logic and processes user requests.', 211 | tech: 'Node.js, Express' 212 | }, 213 | }, 214 | { 215 | id: 'web-server-2', 216 | type: 'serverNode', 217 | position: { x: 400, y: 300 }, 218 | data: { 219 | label: 'Web Server 2', 220 | description: 'Secondary server for horizontal scaling and high availability.', 221 | tech: 'Node.js, Express' 222 | }, 223 | }, 224 | { 225 | id: 'db-primary', 226 | type: 'databaseNode', 227 | position: { x: 250, y: 450 }, 228 | data: { 229 | label: 'Primary Database', 230 | description: 'Stores persistent data. Handles write operations.', 231 | tech: 'PostgreSQL, MongoDB' 232 | }, 233 | }, 234 | { 235 | id: 'cache', 236 | type: 'cacheNode', 237 | position: { x: 50, y: 450 }, 238 | data: { 239 | label: 'Redis Cache', 240 | description: 'Stores frequently accessed data to reduce database load.', 241 | tech: 'Redis' 242 | }, 243 | }, 244 | ]; 245 | 246 | const edges = [ 247 | { id: 'e1', source: 'client', target: 'lb', animated: true, label: 'HTTPS' }, 248 | { id: 'e2', source: 'lb', target: 'web-server-1', animated: true }, 249 | { id: 'e3', source: 'lb', target: 'web-server-2', animated: true }, 250 | { id: 'e4', source: 'web-server-1', target: 'db-primary', animated: true }, 251 | { id: 'e5', source: 'web-server-2', target: 'db-primary', animated: true }, 252 | { id: 'e6', source: 'web-server-1', target: 'cache', type: 'smoothstep', style: { strokeDasharray: '5,5' } }, 253 | ]; 254 | 255 | return { nodes, edges }; 256 | }; 257 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Activity, 3 | ArrowDown, 4 | ArrowLeft, 5 | Code, 6 | Image as ImageIcon, 7 | Layout, 8 | } from "lucide-react"; 9 | import { useEffect, useRef, useState } from "react"; 10 | import { ReactFlowProvider } from "reactflow"; 11 | import InfoPanel from "./components/InfoPanel"; 12 | import MermaidDisplay from "./components/MermaidDisplay"; 13 | import SystemDiagram from "./components/SystemDiagram"; 14 | import ThemeToggle from "./components/ThemeToggle"; 15 | import UploadZone from "./components/UploadZone"; 16 | import { 17 | convertMermaidToFlow, 18 | generateMermaidFromImage, 19 | } from "./services/analysisService"; 20 | 21 | function App() { 22 | const [graphData, setGraphData] = useState(null); 23 | const [mermaidCode, setMermaidCode] = useState(null); 24 | const [uploadedImageUrl, setUploadedImageUrl] = useState(null); 25 | const [isAnalyzing, setIsAnalyzing] = useState(false); 26 | const [isConverting, setIsConverting] = useState(false); 27 | const [selectedNode, setSelectedNode] = useState(null); 28 | 29 | const interactiveSectionRef = useRef(null); 30 | 31 | const handleUpload = async (file) => { 32 | console.log("App: handleUpload called with file:", file); 33 | 34 | // Create a local URL for the uploaded image to display it 35 | const objectUrl = URL.createObjectURL(file); 36 | setUploadedImageUrl(objectUrl); 37 | 38 | setIsAnalyzing(true); 39 | try { 40 | console.log("App: calling generateMermaidFromImage..."); 41 | const code = await generateMermaidFromImage(file); 42 | console.log("App: generateMermaidFromImage returned:", code); 43 | setMermaidCode(code); 44 | } catch (error) { 45 | console.error("Analysis failed:", error); 46 | } finally { 47 | setIsAnalyzing(false); 48 | } 49 | }; 50 | 51 | const handleConvertToInteractive = async () => { 52 | if (!mermaidCode) return; 53 | setIsConverting(true); 54 | try { 55 | console.log("App: calling convertMermaidToFlow..."); 56 | const data = await convertMermaidToFlow(mermaidCode); 57 | console.log("App: convertMermaidToFlow returned:", data); 58 | setGraphData(data); 59 | 60 | // Scroll to interactive section after a short delay to allow render 61 | setTimeout(() => { 62 | interactiveSectionRef.current?.scrollIntoView({ behavior: "smooth" }); 63 | }, 100); 64 | } catch (error) { 65 | console.error("Conversion failed:", error); 66 | } finally { 67 | setIsConverting(false); 68 | } 69 | }; 70 | 71 | const handleReset = () => { 72 | setGraphData(null); 73 | setMermaidCode(null); 74 | setUploadedImageUrl(null); 75 | setSelectedNode(null); 76 | }; 77 | 78 | // Clean up object URL when component unmounts or image changes 79 | useEffect(() => { 80 | return () => { 81 | if (uploadedImageUrl) { 82 | URL.revokeObjectURL(uploadedImageUrl); 83 | } 84 | }; 85 | }, [uploadedImageUrl]); 86 | 87 | const showDashboard = uploadedImageUrl || mermaidCode || graphData; 88 | 89 | return ( 90 |
97 | {/* Header */} 98 |
106 |
107 |
108 |
112 | 113 |
114 |

118 | System Design Visualizer 119 |

120 |
121 | 122 |
123 | {showDashboard && ( 124 | 146 | )} 147 | 148 |
149 |
150 |
151 | 152 | {/* Main Content */} 153 |
154 | {!showDashboard ? ( 155 |
156 |
157 |
164 | 168 |
169 |

173 | Bring your architecture to life 174 |

175 |

179 | Upload a system design image and let our AI convert it into an 180 | interactive, explorable diagram with detailed component 181 | information. 182 |

183 |
184 | 185 |
186 | ) : ( 187 |
188 | {/* Row 1: Source Materials */} 189 |
190 | {/* Top Left: Original Image (35%) */} 191 |
198 |
205 | 209 |

213 | Original Design 214 |

215 |
216 |
220 | {uploadedImageUrl ? ( 221 | Original System Design 230 | ) : ( 231 |
235 | No image loaded 236 |
237 | )} 238 |
239 |
240 | 241 | {/* Top Right: Mermaid Diagram (65%) */} 242 |
249 |
256 |
257 | 261 |

265 | Mermaid Definition 266 |

267 |
268 | {mermaidCode && !graphData && ( 269 | 283 | )} 284 |
285 |
289 | {mermaidCode ? ( 290 | 291 | ) : isAnalyzing ? ( 292 |
296 |
303 | 304 | Generating Mermaid diagram... 305 | 306 |
307 | ) : ( 308 |
312 | Waiting for analysis... 313 |
314 | )} 315 |
316 |
317 |
318 | 319 | {/* Row 2: Interactive Diagram (Full Width) */} 320 |
328 |
335 |
336 | 340 |

344 | Interactive Visualization 345 |

346 |
347 |
348 | 349 |
353 | {graphData ? ( 354 | <> 355 | 356 | 361 | 362 | setSelectedNode(null)} 365 | /> 366 | 367 | ) : ( 368 |
372 | {mermaidCode ? ( 373 |
374 |
382 | 386 |
387 |

391 | Ready to Visualize 392 |

393 |

394 | Review the Mermaid diagram above. When you're ready, 395 | click "Convert to Interactive" to generate the 396 | explorable graph. 397 |

398 | 412 |
413 | ) : ( 414 |
415 |
422 | 423 |
424 |

Upload an image to start the analysis.

425 |
426 | )} 427 |
428 | )} 429 |
430 |
431 |
432 | )} 433 |
434 |
435 | ); 436 | } 437 | 438 | export default App; 439 | --------------------------------------------------------------------------------