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,512 @@
1
+ import { useState } from 'react'
2
+ import { X, Trash2, Copy, Code, ChevronDown, ChevronRight, Plus, FileCode, ExternalLink, Plug } from 'lucide-react'
3
+ import { useStudioStore } from '../lib/store'
4
+ import { getNodeTemplate, nodeTemplates } from '../lib/nodeTemplates'
5
+ import PythonEditor from './PythonEditor'
6
+ import PortsEditor from './PortsEditor'
7
+ import NodePortsSection from './NodePortsSection'
8
+ import type { HexdagNode } from '../types'
9
+
10
+ interface NodeInspectorProps {
11
+ nodeId: string | null
12
+ onClose: () => void
13
+ }
14
+
15
+ export default function NodeInspector({ nodeId, onClose }: NodeInspectorProps) {
16
+ const { nodes, setNodes, edges, setEdges, syncCanvasToYaml } = useStudioStore()
17
+ const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['general', 'spec']))
18
+
19
+ const node = nodes.find((n) => n.id === nodeId)
20
+ const template = node ? getNodeTemplate(node.data.kind) : null
21
+
22
+ // Show PortsEditor when no node is selected
23
+ if (!node || !nodeId) {
24
+ return <PortsEditor />
25
+ }
26
+
27
+ const toggleSection = (section: string) => {
28
+ const newExpanded = new Set(expandedSections)
29
+ if (newExpanded.has(section)) {
30
+ newExpanded.delete(section)
31
+ } else {
32
+ newExpanded.add(section)
33
+ }
34
+ setExpandedSections(newExpanded)
35
+ }
36
+
37
+ const updateNode = (updates: Partial<HexdagNode['data']>) => {
38
+ setNodes(
39
+ nodes.map((n) =>
40
+ n.id === nodeId
41
+ ? { ...n, data: { ...n.data, ...updates } }
42
+ : n
43
+ )
44
+ )
45
+ syncCanvasToYaml()
46
+ }
47
+
48
+ const updateSpec = (key: string, value: unknown) => {
49
+ const newSpec = { ...node.data.spec, [key]: value }
50
+ updateNode({ spec: newSpec })
51
+ }
52
+
53
+ const deleteSpec = (key: string) => {
54
+ const newSpec = { ...node.data.spec }
55
+ delete newSpec[key]
56
+ updateNode({ spec: newSpec })
57
+ }
58
+
59
+ const renameNode = (newName: string) => {
60
+ if (!newName || newName === nodeId) return
61
+
62
+ // Check for duplicates
63
+ if (nodes.some((n) => n.id === newName)) {
64
+ alert('A node with this name already exists')
65
+ return
66
+ }
67
+
68
+ // Update node id
69
+ setNodes(
70
+ nodes.map((n) =>
71
+ n.id === nodeId
72
+ ? { ...n, id: newName, data: { ...n.data, label: newName } }
73
+ : n
74
+ )
75
+ )
76
+
77
+ // Update edges
78
+ setEdges(
79
+ edges.map((e) => ({
80
+ ...e,
81
+ id: e.id.replace(nodeId, newName),
82
+ source: e.source === nodeId ? newName : e.source,
83
+ target: e.target === nodeId ? newName : e.target,
84
+ }))
85
+ )
86
+
87
+ syncCanvasToYaml()
88
+ }
89
+
90
+ const changeNodeKind = (newKind: string) => {
91
+ const newTemplate = getNodeTemplate(newKind)
92
+ if (!newTemplate) return
93
+
94
+ updateNode({
95
+ kind: newKind,
96
+ spec: { ...newTemplate.defaultSpec },
97
+ })
98
+ }
99
+
100
+ const deleteNode = () => {
101
+ if (!confirm(`Delete node "${nodeId}"?`)) return
102
+
103
+ setNodes(nodes.filter((n) => n.id !== nodeId))
104
+ setEdges(edges.filter((e) => e.source !== nodeId && e.target !== nodeId))
105
+ syncCanvasToYaml()
106
+ onClose()
107
+ }
108
+
109
+ const duplicateNode = () => {
110
+ let newName = `${nodeId}_copy`
111
+ let counter = 1
112
+ while (nodes.some((n) => n.id === newName)) {
113
+ newName = `${nodeId}_copy_${counter}`
114
+ counter++
115
+ }
116
+
117
+ const newNode: HexdagNode = {
118
+ ...node,
119
+ id: newName,
120
+ position: {
121
+ x: node.position.x + 50,
122
+ y: node.position.y + 50,
123
+ },
124
+ data: {
125
+ ...node.data,
126
+ label: newName,
127
+ },
128
+ }
129
+
130
+ setNodes([...nodes, newNode])
131
+ syncCanvasToYaml()
132
+ }
133
+
134
+ // Get dependencies (incoming edges)
135
+ const dependencies = edges.filter((e) => e.target === nodeId).map((e) => e.source)
136
+
137
+ return (
138
+ <div className="h-full flex flex-col bg-hex-surface">
139
+ {/* Header */}
140
+ <div className="p-3 border-b border-hex-border flex items-center gap-2">
141
+ <div
142
+ className="w-8 h-8 rounded flex items-center justify-center flex-shrink-0"
143
+ style={{ backgroundColor: `${template?.color || '#6b7280'}20` }}
144
+ >
145
+ <Code size={14} style={{ color: template?.color || '#6b7280' }} />
146
+ </div>
147
+ <div className="flex-1 min-w-0">
148
+ <div className="text-sm font-medium text-hex-text truncate">{nodeId}</div>
149
+ <div className="text-[10px] text-hex-text-muted">{node.data.kind}</div>
150
+ </div>
151
+ <button
152
+ onClick={onClose}
153
+ className="p-1 hover:bg-hex-border/50 rounded transition-colors"
154
+ >
155
+ <X size={14} className="text-hex-text-muted" />
156
+ </button>
157
+ </div>
158
+
159
+ {/* Actions */}
160
+ <div className="p-2 border-b border-hex-border flex gap-1">
161
+ <button
162
+ onClick={duplicateNode}
163
+ className="flex-1 flex items-center justify-center gap-1 py-1.5 text-xs rounded bg-hex-bg hover:bg-hex-border/50 transition-colors"
164
+ >
165
+ <Copy size={12} />
166
+ Duplicate
167
+ </button>
168
+ <button
169
+ onClick={deleteNode}
170
+ className="flex-1 flex items-center justify-center gap-1 py-1.5 text-xs rounded bg-hex-error/10 text-hex-error hover:bg-hex-error/20 transition-colors"
171
+ >
172
+ <Trash2 size={12} />
173
+ Delete
174
+ </button>
175
+ </div>
176
+
177
+ {/* Sections */}
178
+ <div className="flex-1 overflow-y-auto">
179
+ {/* General Section */}
180
+ <Section
181
+ title="General"
182
+ expanded={expandedSections.has('general')}
183
+ onToggle={() => toggleSection('general')}
184
+ >
185
+ <Field label="Name">
186
+ <input
187
+ type="text"
188
+ defaultValue={nodeId}
189
+ onBlur={(e) => renameNode(e.target.value)}
190
+ onKeyDown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()}
191
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1.5 text-xs text-hex-text focus:border-hex-accent focus:outline-none"
192
+ />
193
+ </Field>
194
+
195
+ <Field label="Type">
196
+ <select
197
+ value={node.data.kind}
198
+ onChange={(e) => changeNodeKind(e.target.value)}
199
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1.5 text-xs text-hex-text focus:border-hex-accent focus:outline-none"
200
+ >
201
+ {nodeTemplates.map((t) => (
202
+ <option key={t.kind} value={t.kind}>
203
+ {t.label}
204
+ </option>
205
+ ))}
206
+ </select>
207
+ </Field>
208
+
209
+ <Field label="Dependencies">
210
+ <div className="space-y-1">
211
+ {dependencies.length === 0 ? (
212
+ <div className="text-[10px] text-hex-text-muted italic">No dependencies</div>
213
+ ) : (
214
+ dependencies.map((dep) => (
215
+ <div
216
+ key={dep}
217
+ className="flex items-center gap-2 text-xs bg-hex-bg rounded px-2 py-1"
218
+ >
219
+ <div className="w-2 h-2 bg-hex-accent rounded-full" />
220
+ {dep}
221
+ </div>
222
+ ))
223
+ )}
224
+ </div>
225
+ </Field>
226
+ </Section>
227
+
228
+ {/* Spec Section */}
229
+ <Section
230
+ title="Configuration"
231
+ expanded={expandedSections.has('spec')}
232
+ onToggle={() => toggleSection('spec')}
233
+ >
234
+ {Object.entries(node.data.spec)
235
+ .filter(([key]) => key !== 'code' && key !== 'inline_code' && key !== 'ports')
236
+ .map(([key, value]) => (
237
+ <SpecField
238
+ key={key}
239
+ name={key}
240
+ value={value}
241
+ onChange={(v) => updateSpec(key, v)}
242
+ onDelete={() => deleteSpec(key)}
243
+ />
244
+ ))}
245
+ <AddSpecButton
246
+ onAdd={(key) => updateSpec(key, '')}
247
+ existingKeys={Object.keys(node.data.spec)}
248
+ />
249
+ </Section>
250
+
251
+ {/* Ports Section - only show for nodes that require ports */}
252
+ {template?.requiredPorts && template.requiredPorts.length > 0 && (
253
+ <Section
254
+ title="Ports"
255
+ expanded={expandedSections.has('ports')}
256
+ onToggle={() => toggleSection('ports')}
257
+ icon={<Plug size={12} />}
258
+ >
259
+ <NodePortsSection
260
+ nodeId={nodeId}
261
+ requiredPorts={template.requiredPorts}
262
+ nodePorts={(node.data.spec.ports as Record<string, { adapter: string; config: Record<string, unknown> } | undefined>) || {}}
263
+ onPortsChange={(newPorts) => {
264
+ // Clean up empty ports
265
+ const cleanedPorts: Record<string, unknown> = {}
266
+ for (const [key, value] of Object.entries(newPorts)) {
267
+ if (value && value.adapter) {
268
+ cleanedPorts[key] = value
269
+ }
270
+ }
271
+ if (Object.keys(cleanedPorts).length > 0) {
272
+ updateSpec('ports', cleanedPorts)
273
+ } else {
274
+ deleteSpec('ports')
275
+ }
276
+ }}
277
+ />
278
+ </Section>
279
+ )}
280
+
281
+ {/* Python Code Editor Section - only for function_node */}
282
+ {node.data.kind === 'function_node' && (
283
+ <Section
284
+ title="Python Code"
285
+ expanded={expandedSections.has('code')}
286
+ onToggle={() => toggleSection('code')}
287
+ icon={<FileCode size={12} />}
288
+ >
289
+ <div className="space-y-2">
290
+ <div className="flex items-center justify-between">
291
+ <label className="text-[10px] font-medium text-hex-text-muted uppercase tracking-wider">
292
+ Inline Code
293
+ </label>
294
+ <span className="text-[10px] text-hex-text-muted">
295
+ Python 3.12+
296
+ </span>
297
+ </div>
298
+ <PythonEditor
299
+ value={String(node.data.spec.code || node.data.spec.inline_code || getDefaultPythonCode())}
300
+ onChange={(code) => updateSpec('code', code)}
301
+ height="200px"
302
+ />
303
+ <div className="text-[10px] text-hex-text-muted space-y-1">
304
+ <p>Define a function that processes the input data.</p>
305
+ <p className="text-hex-accent">
306
+ Access inputs via <code className="bg-hex-bg px-1 rounded">ctx</code> parameter.
307
+ </p>
308
+ </div>
309
+ {typeof node.data.spec.fn === 'string' && node.data.spec.fn && (
310
+ <div className="flex items-center gap-2 p-2 bg-hex-bg rounded">
311
+ <ExternalLink size={12} className="text-hex-text-muted" />
312
+ <span className="text-[10px] text-hex-text-muted">
313
+ Module path: <code className="text-hex-accent">{node.data.spec.fn}</code>
314
+ </span>
315
+ </div>
316
+ )}
317
+ </div>
318
+ </Section>
319
+ )}
320
+
321
+ {/* Template Info */}
322
+ {template && (
323
+ <Section
324
+ title="Info"
325
+ expanded={expandedSections.has('info')}
326
+ onToggle={() => toggleSection('info')}
327
+ >
328
+ <div className="text-xs text-hex-text-muted">
329
+ {template.description}
330
+ </div>
331
+ </Section>
332
+ )}
333
+ </div>
334
+ </div>
335
+ )
336
+ }
337
+
338
+ // Helper to get default Python code for new function nodes
339
+ function getDefaultPythonCode(): string {
340
+ return `def process(ctx):
341
+ """Process the input data.
342
+
343
+ Args:
344
+ ctx: Execution context with input data
345
+
346
+ Returns:
347
+ Processed result
348
+ """
349
+ # Access inputs from previous nodes
350
+ # input_data = ctx.get("previous_node")
351
+
352
+ return {"result": "processed"}
353
+ `
354
+ }
355
+
356
+ // Helper components
357
+ function Section({
358
+ title,
359
+ expanded,
360
+ onToggle,
361
+ children,
362
+ icon,
363
+ }: {
364
+ title: string
365
+ expanded: boolean
366
+ onToggle: () => void
367
+ children: React.ReactNode
368
+ icon?: React.ReactNode
369
+ }) {
370
+ return (
371
+ <div className="border-b border-hex-border">
372
+ <button
373
+ onClick={onToggle}
374
+ className="w-full px-3 py-2 flex items-center gap-2 text-xs font-medium text-hex-text-muted hover:bg-hex-border/30 transition-colors"
375
+ >
376
+ {expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
377
+ {icon}
378
+ {title}
379
+ </button>
380
+ {expanded && <div className="px-3 pb-3 space-y-3">{children}</div>}
381
+ </div>
382
+ )
383
+ }
384
+
385
+ function Field({ label, children }: { label: string; children: React.ReactNode }) {
386
+ return (
387
+ <div>
388
+ <label className="block text-[10px] font-medium text-hex-text-muted mb-1 uppercase tracking-wider">
389
+ {label}
390
+ </label>
391
+ {children}
392
+ </div>
393
+ )
394
+ }
395
+
396
+ function SpecField({
397
+ name,
398
+ value,
399
+ onChange,
400
+ onDelete,
401
+ }: {
402
+ name: string
403
+ value: unknown
404
+ onChange: (value: unknown) => void
405
+ onDelete: () => void
406
+ }) {
407
+ const isMultiline = typeof value === 'string' && (value.includes('\n') || value.length > 50)
408
+
409
+ return (
410
+ <div className="group">
411
+ <div className="flex items-center justify-between mb-1">
412
+ <label className="text-[10px] font-medium text-hex-text-muted uppercase tracking-wider">
413
+ {name}
414
+ </label>
415
+ <button
416
+ onClick={onDelete}
417
+ className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-hex-error/20 rounded transition-all"
418
+ >
419
+ <X size={10} className="text-hex-error" />
420
+ </button>
421
+ </div>
422
+ {isMultiline ? (
423
+ <textarea
424
+ value={String(value)}
425
+ onChange={(e) => onChange(e.target.value)}
426
+ rows={4}
427
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1.5 text-xs text-hex-text focus:border-hex-accent focus:outline-none font-mono resize-y"
428
+ />
429
+ ) : typeof value === 'boolean' ? (
430
+ <label className="flex items-center gap-2 cursor-pointer">
431
+ <input
432
+ type="checkbox"
433
+ checked={value}
434
+ onChange={(e) => onChange(e.target.checked)}
435
+ className="w-4 h-4 rounded border-hex-border bg-hex-bg text-hex-accent focus:ring-hex-accent"
436
+ />
437
+ <span className="text-xs text-hex-text">{value ? 'true' : 'false'}</span>
438
+ </label>
439
+ ) : typeof value === 'number' ? (
440
+ <input
441
+ type="number"
442
+ value={value}
443
+ onChange={(e) => onChange(Number(e.target.value))}
444
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1.5 text-xs text-hex-text focus:border-hex-accent focus:outline-none"
445
+ />
446
+ ) : (
447
+ <input
448
+ type="text"
449
+ value={String(value)}
450
+ onChange={(e) => onChange(e.target.value)}
451
+ className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1.5 text-xs text-hex-text focus:border-hex-accent focus:outline-none font-mono"
452
+ />
453
+ )}
454
+ </div>
455
+ )
456
+ }
457
+
458
+ function AddSpecButton({
459
+ onAdd,
460
+ existingKeys,
461
+ }: {
462
+ onAdd: (key: string) => void
463
+ existingKeys: string[]
464
+ }) {
465
+ const [isAdding, setIsAdding] = useState(false)
466
+ const [newKey, setNewKey] = useState('')
467
+
468
+ const handleAdd = () => {
469
+ if (!newKey || existingKeys.includes(newKey)) return
470
+ onAdd(newKey)
471
+ setNewKey('')
472
+ setIsAdding(false)
473
+ }
474
+
475
+ if (isAdding) {
476
+ return (
477
+ <div className="flex gap-1">
478
+ <input
479
+ type="text"
480
+ value={newKey}
481
+ onChange={(e) => setNewKey(e.target.value)}
482
+ onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
483
+ placeholder="Key name"
484
+ autoFocus
485
+ className="flex-1 bg-hex-bg border border-hex-border rounded px-2 py-1 text-xs text-hex-text focus:border-hex-accent focus:outline-none"
486
+ />
487
+ <button
488
+ onClick={handleAdd}
489
+ className="px-2 py-1 text-xs bg-hex-accent text-white rounded hover:bg-hex-accent-hover"
490
+ >
491
+ Add
492
+ </button>
493
+ <button
494
+ onClick={() => setIsAdding(false)}
495
+ className="px-2 py-1 text-xs bg-hex-border text-hex-text rounded hover:bg-hex-border/70"
496
+ >
497
+ Cancel
498
+ </button>
499
+ </div>
500
+ )
501
+ }
502
+
503
+ return (
504
+ <button
505
+ onClick={() => setIsAdding(true)}
506
+ className="w-full flex items-center justify-center gap-1 py-1.5 text-xs text-hex-text-muted hover:text-hex-text border border-dashed border-hex-border rounded hover:border-hex-accent transition-colors"
507
+ >
508
+ <Plus size={12} />
509
+ Add property
510
+ </button>
511
+ )
512
+ }