hexdag 0.5.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,31 @@
1
+ {
2
+ "dependencies": {
3
+ "@monaco-editor/react": "^4.6.0",
4
+ "@xyflow/react": "^12.0.0",
5
+ "lucide-react": "^0.400.0",
6
+ "react": "^18.3.1",
7
+ "react-dom": "^18.3.1",
8
+ "yaml": "^2.4.0",
9
+ "zustand": "^4.5.0"
10
+ },
11
+ "devDependencies": {
12
+ "@types/react": "^18.3.0",
13
+ "@types/react-dom": "^18.3.0",
14
+ "@vitejs/plugin-react": "^4.3.0",
15
+ "autoprefixer": "^10.4.19",
16
+ "postcss": "^8.4.38",
17
+ "tailwindcss": "^3.4.4",
18
+ "typescript": "^5.4.0",
19
+ "vite": "^5.3.0"
20
+ },
21
+ "name": "hexdag-studio",
22
+ "private": true,
23
+ "scripts": {
24
+ "build": "tsc && vite build",
25
+ "dev": "vite",
26
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
27
+ "preview": "vite preview"
28
+ },
29
+ "type": "module",
30
+ "version": "0.1.0"
31
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect width="32" height="32" rx="6" fill="#6366f1"/>
3
+ <path d="M8 12L16 8L24 12V20L16 24L8 20V12Z" stroke="white" stroke-width="2" fill="none"/>
4
+ <circle cx="16" cy="16" r="3" fill="white"/>
5
+ </svg>
@@ -0,0 +1,251 @@
1
+ import { useEffect, useState, useCallback } from 'react'
2
+ import { ReactFlowProvider } from '@xyflow/react'
3
+ import Header from './components/Header'
4
+ import FileBrowser from './components/FileBrowser'
5
+ import NodePalette from './components/NodePalette'
6
+ import Canvas from './components/Canvas'
7
+ import YamlEditor from './components/YamlEditor'
8
+ import ValidationPanel from './components/ValidationPanel'
9
+ import NodeInspector from './components/NodeInspector'
10
+ import { useStudioStore } from './lib/store'
11
+ import { saveFile } from './lib/api'
12
+
13
+ type ViewMode = 'split' | 'canvas' | 'yaml'
14
+ type RightPanel = 'validation' | 'inspector'
15
+
16
+ export default function App() {
17
+ const [viewMode, setViewMode] = useState<ViewMode>('split')
18
+ const [rightPanel, setRightPanel] = useState<RightPanel>('validation')
19
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
20
+
21
+ const { currentFile, yamlContent, setIsSaving, setIsDirty, nodes } = useStudioStore()
22
+
23
+ // Handle node selection from canvas
24
+ const handleNodeSelect = useCallback((nodeId: string | null) => {
25
+ setSelectedNodeId(nodeId)
26
+ if (nodeId) {
27
+ setRightPanel('inspector')
28
+ }
29
+ }, [])
30
+
31
+ // Handle keyboard shortcuts
32
+ useEffect(() => {
33
+ const handleKeyDown = (e: KeyboardEvent) => {
34
+ // Cmd/Ctrl + S to save
35
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
36
+ e.preventDefault()
37
+ handleSave()
38
+ }
39
+ // Cmd/Ctrl + 1/2/3 to switch views
40
+ if ((e.metaKey || e.ctrlKey) && e.key === '1') {
41
+ e.preventDefault()
42
+ setViewMode('split')
43
+ }
44
+ if ((e.metaKey || e.ctrlKey) && e.key === '2') {
45
+ e.preventDefault()
46
+ setViewMode('canvas')
47
+ }
48
+ if ((e.metaKey || e.ctrlKey) && e.key === '3') {
49
+ e.preventDefault()
50
+ setViewMode('yaml')
51
+ }
52
+ // Escape to deselect
53
+ if (e.key === 'Escape') {
54
+ setSelectedNodeId(null)
55
+ setRightPanel('validation')
56
+ }
57
+ // Delete selected node
58
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNodeId) {
59
+ // Only if not in an input
60
+ const target = e.target as HTMLElement
61
+ if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') {
62
+ e.preventDefault()
63
+ // Delete will be handled by Canvas component
64
+ }
65
+ }
66
+ }
67
+
68
+ const handleSaveEvent = () => {
69
+ handleSave()
70
+ }
71
+
72
+ // Listen for node selection events from Canvas
73
+ const handleNodeSelectEvent = (e: CustomEvent<{ nodeId: string | null }>) => {
74
+ handleNodeSelect(e.detail.nodeId)
75
+ }
76
+
77
+ window.addEventListener('keydown', handleKeyDown)
78
+ window.addEventListener('hexdag-save', handleSaveEvent)
79
+ window.addEventListener('hexdag-node-select', handleNodeSelectEvent as EventListener)
80
+
81
+ return () => {
82
+ window.removeEventListener('keydown', handleKeyDown)
83
+ window.removeEventListener('hexdag-save', handleSaveEvent)
84
+ window.removeEventListener('hexdag-node-select', handleNodeSelectEvent as EventListener)
85
+ }
86
+ }, [currentFile, yamlContent, selectedNodeId, handleNodeSelect])
87
+
88
+ const handleSave = async () => {
89
+ if (!currentFile || !yamlContent) return
90
+
91
+ setIsSaving(true)
92
+ try {
93
+ await saveFile(currentFile, yamlContent)
94
+ setIsDirty(false)
95
+ } catch (error) {
96
+ console.error('Failed to save:', error)
97
+ } finally {
98
+ setIsSaving(false)
99
+ }
100
+ }
101
+
102
+ return (
103
+ <ReactFlowProvider>
104
+ <div className="h-screen flex flex-col bg-hex-bg">
105
+ {/* Header */}
106
+ <Header />
107
+
108
+ {/* Main content */}
109
+ <div className="flex-1 flex overflow-hidden">
110
+ {/* Left sidebar: Files + Node palette */}
111
+ <div className="w-48 flex-shrink-0 flex flex-col bg-hex-surface border-r border-hex-border">
112
+ <FileBrowser />
113
+ <div className="flex-1 overflow-hidden">
114
+ <NodePalette />
115
+ </div>
116
+ </div>
117
+
118
+ {/* Main editor area */}
119
+ <div className="flex-1 flex flex-col overflow-hidden">
120
+ {/* View mode tabs */}
121
+ <div className="h-8 bg-hex-surface border-b border-hex-border flex items-center px-2 gap-1">
122
+ <button
123
+ onClick={() => setViewMode('split')}
124
+ className={`px-3 py-1 text-xs rounded transition-colors ${
125
+ viewMode === 'split'
126
+ ? 'bg-hex-accent text-white'
127
+ : 'text-hex-text-muted hover:text-hex-text hover:bg-hex-border/50'
128
+ }`}
129
+ >
130
+ Split
131
+ </button>
132
+ <button
133
+ onClick={() => setViewMode('canvas')}
134
+ className={`px-3 py-1 text-xs rounded transition-colors ${
135
+ viewMode === 'canvas'
136
+ ? 'bg-hex-accent text-white'
137
+ : 'text-hex-text-muted hover:text-hex-text hover:bg-hex-border/50'
138
+ }`}
139
+ >
140
+ Canvas
141
+ </button>
142
+ <button
143
+ onClick={() => setViewMode('yaml')}
144
+ className={`px-3 py-1 text-xs rounded transition-colors ${
145
+ viewMode === 'yaml'
146
+ ? 'bg-hex-accent text-white'
147
+ : 'text-hex-text-muted hover:text-hex-text hover:bg-hex-border/50'
148
+ }`}
149
+ >
150
+ YAML
151
+ </button>
152
+
153
+ <div className="ml-auto flex items-center gap-2">
154
+ {selectedNodeId && (
155
+ <span className="text-[10px] text-hex-accent">
156
+ Selected: {selectedNodeId}
157
+ </span>
158
+ )}
159
+ <span className="text-[10px] text-hex-text-muted">
160
+ {nodes.length} node{nodes.length !== 1 ? 's' : ''}
161
+ </span>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Editor panels */}
166
+ <div className="flex-1 flex overflow-hidden">
167
+ {/* Canvas panel */}
168
+ {(viewMode === 'split' || viewMode === 'canvas') && (
169
+ <div className={`${viewMode === 'split' ? 'w-1/2' : 'flex-1'} border-r border-hex-border`}>
170
+ <Canvas onNodeSelect={handleNodeSelect} selectedNodeId={selectedNodeId} />
171
+ </div>
172
+ )}
173
+
174
+ {/* YAML editor panel */}
175
+ {(viewMode === 'split' || viewMode === 'yaml') && (
176
+ <div className={`${viewMode === 'split' ? 'w-1/2' : 'flex-1'} flex flex-col`}>
177
+ <div className="flex-1">
178
+ <YamlEditor />
179
+ </div>
180
+ </div>
181
+ )}
182
+ </div>
183
+ </div>
184
+
185
+ {/* Right sidebar: Validation / Inspector */}
186
+ <div className="w-72 flex-shrink-0 bg-hex-surface border-l border-hex-border flex flex-col">
187
+ {/* Tab switcher */}
188
+ <div className="h-8 border-b border-hex-border flex items-center px-1 gap-1">
189
+ <button
190
+ onClick={() => setRightPanel('validation')}
191
+ className={`flex-1 py-1 text-[10px] font-semibold uppercase tracking-wider rounded transition-colors ${
192
+ rightPanel === 'validation'
193
+ ? 'bg-hex-accent/20 text-hex-accent'
194
+ : 'text-hex-text-muted hover:text-hex-text'
195
+ }`}
196
+ >
197
+ Validation
198
+ </button>
199
+ <button
200
+ onClick={() => setRightPanel('inspector')}
201
+ className={`flex-1 py-1 text-[10px] font-semibold uppercase tracking-wider rounded transition-colors ${
202
+ rightPanel === 'inspector'
203
+ ? 'bg-hex-accent/20 text-hex-accent'
204
+ : 'text-hex-text-muted hover:text-hex-text'
205
+ }`}
206
+ >
207
+ Inspector
208
+ {selectedNodeId && (
209
+ <span className="ml-1 w-2 h-2 bg-hex-accent rounded-full inline-block" />
210
+ )}
211
+ </button>
212
+ </div>
213
+
214
+ {/* Panel content */}
215
+ <div className="flex-1 overflow-hidden">
216
+ {rightPanel === 'validation' ? (
217
+ <ValidationPanel />
218
+ ) : (
219
+ <NodeInspector
220
+ nodeId={selectedNodeId}
221
+ onClose={() => {
222
+ setSelectedNodeId(null)
223
+ setRightPanel('validation')
224
+ }}
225
+ />
226
+ )}
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ {/* Status bar */}
232
+ <div className="h-6 bg-hex-bg border-t border-hex-border flex items-center px-3 text-[10px] text-hex-text-muted">
233
+ <div className="flex items-center gap-1">
234
+ <div className="w-2 h-2 bg-hex-success rounded-full" />
235
+ <span>Connected</span>
236
+ </div>
237
+ <div className="mx-3">|</div>
238
+ <span>hexdag studio v0.1.0</span>
239
+ <div className="ml-auto flex items-center gap-3">
240
+ <span>
241
+ <kbd className="px-1 py-0.5 bg-hex-surface rounded border border-hex-border">Cmd+S</kbd> Save
242
+ </span>
243
+ <span>
244
+ <kbd className="px-1 py-0.5 bg-hex-surface rounded border border-hex-border">Esc</kbd> Deselect
245
+ </span>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </ReactFlowProvider>
250
+ )
251
+ }
@@ -0,0 +1,408 @@
1
+ import { useCallback, useRef, useEffect, useState } from 'react'
2
+ import {
3
+ ReactFlow,
4
+ Background,
5
+ Controls,
6
+ MiniMap,
7
+ useNodesState,
8
+ useEdgesState,
9
+ addEdge,
10
+ type Connection,
11
+ type OnNodesChange,
12
+ type OnEdgesChange,
13
+ type OnConnect,
14
+ type Node,
15
+ BackgroundVariant,
16
+ } from '@xyflow/react'
17
+ import '@xyflow/react/dist/style.css'
18
+
19
+ import HexdagNode from './HexdagNode'
20
+ import ContextMenu, { createNodeContextMenuItems } from './ContextMenu'
21
+ import { useStudioStore } from '../lib/store'
22
+ import { getNodeTemplate, getNodeColor } from '../lib/nodeTemplates'
23
+ import type { HexdagNode as HexdagNodeType } from '../types'
24
+
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ const nodeTypes: Record<string, any> = {
27
+ hexdagNode: HexdagNode,
28
+ }
29
+
30
+ interface CanvasProps {
31
+ onNodeSelect?: (nodeId: string | null) => void
32
+ selectedNodeId?: string | null
33
+ }
34
+
35
+ export default function Canvas({ onNodeSelect, selectedNodeId }: CanvasProps) {
36
+ const reactFlowWrapper = useRef<HTMLDivElement>(null)
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const reactFlowInstance = useRef<any>(null)
39
+
40
+ // Context menu state
41
+ const [contextMenu, setContextMenu] = useState<{
42
+ x: number
43
+ y: number
44
+ nodeId: string
45
+ } | null>(null)
46
+
47
+ const {
48
+ nodes: storeNodes,
49
+ edges: storeEdges,
50
+ setNodes: setStoreNodes,
51
+ setEdges: setStoreEdges,
52
+ syncCanvasToYaml,
53
+ } = useStudioStore()
54
+
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ const [nodes, setNodes, onNodesChange] = useNodesState(storeNodes as any)
57
+ const [edges, setEdges, onEdgesChange] = useEdgesState(storeEdges)
58
+
59
+ // Sync from store when store changes externally (e.g., YAML edit)
60
+ useEffect(() => {
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ setNodes(storeNodes as any)
63
+ setEdges(storeEdges)
64
+ }, [storeNodes, storeEdges, setNodes, setEdges])
65
+
66
+ // Update selected state when selectedNodeId changes
67
+ useEffect(() => {
68
+ setNodes((nds) =>
69
+ nds.map((node) => ({
70
+ ...node,
71
+ selected: node.id === selectedNodeId,
72
+ }))
73
+ )
74
+ }, [selectedNodeId, setNodes])
75
+
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ const handleNodesChange: OnNodesChange<any> = useCallback(
78
+ (changes) => {
79
+ onNodesChange(changes)
80
+
81
+ // Handle selection changes
82
+ const selectionChange = changes.find((c) => c.type === 'select')
83
+ if (selectionChange && 'selected' in selectionChange) {
84
+ if (selectionChange.selected) {
85
+ onNodeSelect?.(selectionChange.id)
86
+ }
87
+ }
88
+
89
+ // Handle deletions
90
+ const deletions = changes.filter((c) => c.type === 'remove')
91
+ if (deletions.length > 0) {
92
+ setTimeout(() => {
93
+ const currentNodes = useStudioStore.getState().nodes
94
+ const deletedIds = deletions.map((d) => d.id)
95
+ const newNodes = currentNodes.filter((n) => !deletedIds.includes(n.id))
96
+ setStoreNodes(newNodes)
97
+ syncCanvasToYaml()
98
+
99
+ // Deselect if deleted node was selected
100
+ if (selectedNodeId && deletedIds.includes(selectedNodeId)) {
101
+ onNodeSelect?.(null)
102
+ }
103
+ }, 100)
104
+ return
105
+ }
106
+
107
+ // Debounce sync for position changes
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ const hasPositionChange = changes.some(
110
+ (c: any) => c.type === 'position' && c.dragging === false
111
+ )
112
+ if (hasPositionChange) {
113
+ setTimeout(() => {
114
+ // Get current nodes from React state via callback
115
+ setNodes((currentNodes) => {
116
+ setStoreNodes(currentNodes as HexdagNodeType[])
117
+ setTimeout(() => useStudioStore.getState().syncCanvasToYaml(), 10)
118
+ return currentNodes
119
+ })
120
+ }, 100)
121
+ }
122
+ },
123
+ [onNodesChange, nodes, setStoreNodes, syncCanvasToYaml, onNodeSelect, selectedNodeId]
124
+ )
125
+
126
+ const handleEdgesChange: OnEdgesChange = useCallback(
127
+ (changes) => {
128
+ onEdgesChange(changes)
129
+ const hasNonSelectChange = changes.some((c) => c.type !== 'select')
130
+ if (hasNonSelectChange) {
131
+ setTimeout(() => {
132
+ setStoreEdges(edges)
133
+ syncCanvasToYaml()
134
+ }, 100)
135
+ }
136
+ },
137
+ [onEdgesChange, edges, setStoreEdges, syncCanvasToYaml]
138
+ )
139
+
140
+ const onConnect: OnConnect = useCallback(
141
+ (connection: Connection) => {
142
+ setEdges((eds) => {
143
+ const newEdges = addEdge(
144
+ { ...connection, type: 'smoothstep', animated: false },
145
+ eds
146
+ )
147
+ setTimeout(() => {
148
+ setStoreEdges(newEdges)
149
+ syncCanvasToYaml()
150
+ }, 100)
151
+ return newEdges
152
+ })
153
+ },
154
+ [setEdges, setStoreEdges, syncCanvasToYaml]
155
+ )
156
+
157
+ const onPaneClick = useCallback(() => {
158
+ onNodeSelect?.(null)
159
+ }, [onNodeSelect])
160
+
161
+ const onNodeClick = useCallback(
162
+ (_event: React.MouseEvent, node: Node) => {
163
+ onNodeSelect?.(node.id)
164
+ },
165
+ [onNodeSelect]
166
+ )
167
+
168
+ const onDragOver = useCallback((event: React.DragEvent) => {
169
+ event.preventDefault()
170
+ event.dataTransfer.dropEffect = 'move'
171
+ }, [])
172
+
173
+ const onDrop = useCallback(
174
+ (event: React.DragEvent) => {
175
+ event.preventDefault()
176
+
177
+ const kind = event.dataTransfer.getData('application/hexdag-node')
178
+ if (!kind || !reactFlowInstance.current || !reactFlowWrapper.current) {
179
+ return
180
+ }
181
+
182
+ const bounds = reactFlowWrapper.current.getBoundingClientRect()
183
+ const position = reactFlowInstance.current.screenToFlowPosition({
184
+ x: event.clientX - bounds.left,
185
+ y: event.clientY - bounds.top,
186
+ })
187
+
188
+ // Get template for builtin nodes, or create default spec for plugin nodes
189
+ const template = getNodeTemplate(kind)
190
+ const defaultSpec = template?.defaultSpec || {}
191
+
192
+ // Generate unique name - handle namespaced kinds (e.g., etl:file_reader_node)
193
+ const existingNames = nodes.map((n) => n.id)
194
+ // Extract base name: "etl:file_reader_node" -> "file_reader", "function_node" -> "function"
195
+ const kindParts = kind.split(':')
196
+ const nodeKind = kindParts[kindParts.length - 1] // Get last part after ':'
197
+ let baseName = nodeKind.replace('_node', '')
198
+ let name = baseName
199
+ let counter = 1
200
+ while (existingNames.includes(name)) {
201
+ name = `${baseName}_${counter}`
202
+ counter++
203
+ }
204
+
205
+ const newNode: HexdagNodeType = {
206
+ id: name,
207
+ type: 'hexdagNode',
208
+ position,
209
+ data: {
210
+ kind,
211
+ label: name,
212
+ spec: { ...defaultSpec },
213
+ isValid: true,
214
+ errors: [],
215
+ },
216
+ }
217
+
218
+ // Update both local React state and store, then sync to YAML
219
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
+ const newNodes = [...nodes, newNode] as any
221
+ setNodes(newNodes)
222
+ setStoreNodes(newNodes)
223
+
224
+ // Use setTimeout to ensure store is updated, then call sync from store
225
+ setTimeout(() => {
226
+ useStudioStore.getState().syncCanvasToYaml()
227
+ }, 50)
228
+
229
+ // Select the new node
230
+ onNodeSelect?.(name)
231
+ },
232
+ [nodes, setNodes, setStoreNodes, syncCanvasToYaml, onNodeSelect]
233
+ )
234
+
235
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ const onInit = useCallback((instance: any) => {
237
+ reactFlowInstance.current = instance
238
+ }, [])
239
+
240
+ // Node context menu handlers
241
+ const onNodeContextMenu = useCallback(
242
+ (event: React.MouseEvent, node: Node) => {
243
+ event.preventDefault()
244
+ setContextMenu({
245
+ x: event.clientX,
246
+ y: event.clientY,
247
+ nodeId: node.id,
248
+ })
249
+ },
250
+ []
251
+ )
252
+
253
+ const closeContextMenu = useCallback(() => {
254
+ setContextMenu(null)
255
+ }, [])
256
+
257
+ const duplicateNode = useCallback(
258
+ (nodeId: string) => {
259
+ const node = nodes.find((n) => n.id === nodeId)
260
+ if (!node) return
261
+
262
+ let newName = `${nodeId}_copy`
263
+ let counter = 1
264
+ while (nodes.some((n) => n.id === newName)) {
265
+ newName = `${nodeId}_copy_${counter}`
266
+ counter++
267
+ }
268
+
269
+ const newNode: HexdagNodeType = {
270
+ ...node,
271
+ id: newName,
272
+ position: {
273
+ x: node.position.x + 50,
274
+ y: node.position.y + 50,
275
+ },
276
+ data: {
277
+ ...node.data,
278
+ label: newName,
279
+ },
280
+ } as HexdagNodeType
281
+
282
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
283
+ const newNodes = [...nodes, newNode] as any
284
+ setNodes(newNodes)
285
+ setStoreNodes(newNodes)
286
+ setTimeout(() => {
287
+ useStudioStore.getState().syncCanvasToYaml()
288
+ }, 50)
289
+ },
290
+ [nodes, setNodes, setStoreNodes]
291
+ )
292
+
293
+ const deleteNode = useCallback(
294
+ (nodeId: string) => {
295
+ const newNodes = nodes.filter((n) => n.id !== nodeId)
296
+ const newEdges = edges.filter((e) => e.source !== nodeId && e.target !== nodeId)
297
+
298
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
299
+ setNodes(newNodes as any)
300
+ setEdges(newEdges)
301
+ setStoreNodes(newNodes as HexdagNodeType[])
302
+ setStoreEdges(newEdges)
303
+
304
+ setTimeout(() => {
305
+ useStudioStore.getState().syncCanvasToYaml()
306
+ }, 50)
307
+
308
+ if (selectedNodeId === nodeId) {
309
+ onNodeSelect?.(null)
310
+ }
311
+ },
312
+ [nodes, edges, setNodes, setEdges, setStoreNodes, setStoreEdges, selectedNodeId, onNodeSelect]
313
+ )
314
+
315
+ const disconnectNode = useCallback(
316
+ (nodeId: string) => {
317
+ const newEdges = edges.filter((e) => e.source !== nodeId && e.target !== nodeId)
318
+ setEdges(newEdges)
319
+ setStoreEdges(newEdges)
320
+ setTimeout(() => {
321
+ useStudioStore.getState().syncCanvasToYaml()
322
+ }, 50)
323
+ },
324
+ [edges, setEdges, setStoreEdges]
325
+ )
326
+
327
+ // Get context menu items for the selected node
328
+ const getContextMenuItems = useCallback(
329
+ (nodeId: string) => {
330
+ return createNodeContextMenuItems(nodeId, {
331
+ onEdit: () => {
332
+ onNodeSelect?.(nodeId)
333
+ },
334
+ onDuplicate: () => {
335
+ duplicateNode(nodeId)
336
+ },
337
+ onDelete: () => {
338
+ deleteNode(nodeId)
339
+ },
340
+ onDisconnect: () => {
341
+ disconnectNode(nodeId)
342
+ },
343
+ })
344
+ },
345
+ [onNodeSelect, duplicateNode, deleteNode, disconnectNode]
346
+ )
347
+
348
+ return (
349
+ <div ref={reactFlowWrapper} className="w-full h-full">
350
+ <ReactFlow
351
+ nodes={nodes}
352
+ edges={edges}
353
+ onNodesChange={handleNodesChange}
354
+ onEdgesChange={handleEdgesChange}
355
+ onConnect={onConnect}
356
+ onInit={onInit}
357
+ onDragOver={onDragOver}
358
+ onDrop={onDrop}
359
+ onPaneClick={onPaneClick}
360
+ onNodeClick={onNodeClick}
361
+ onNodeContextMenu={onNodeContextMenu}
362
+ nodeTypes={nodeTypes}
363
+ fitView
364
+ snapToGrid
365
+ snapGrid={[20, 20]}
366
+ deleteKeyCode={['Backspace', 'Delete']}
367
+ multiSelectionKeyCode={['Shift']}
368
+ defaultEdgeOptions={{
369
+ type: 'smoothstep',
370
+ animated: false,
371
+ style: { stroke: '#6366f1', strokeWidth: 2 },
372
+ }}
373
+ proOptions={{ hideAttribution: true }}
374
+ >
375
+ <Background
376
+ color="#2a2a3e"
377
+ gap={20}
378
+ variant={BackgroundVariant.Dots}
379
+ />
380
+ <Controls
381
+ showZoom={true}
382
+ showFitView={true}
383
+ showInteractive={false}
384
+ />
385
+ <MiniMap
386
+ nodeColor={(node: Node) => {
387
+ const data = node.data as { kind?: string }
388
+ return getNodeColor(data?.kind || '')
389
+ }}
390
+ maskColor="rgba(15, 15, 26, 0.8)"
391
+ style={{
392
+ backgroundColor: '#1a1a2e',
393
+ }}
394
+ />
395
+ </ReactFlow>
396
+
397
+ {/* Context Menu */}
398
+ {contextMenu && (
399
+ <ContextMenu
400
+ x={contextMenu.x}
401
+ y={contextMenu.y}
402
+ items={getContextMenuItems(contextMenu.nodeId)}
403
+ onClose={closeContextMenu}
404
+ />
405
+ )}
406
+ </div>
407
+ )
408
+ }