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,187 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { Copy, Trash2, Edit3, Unlink, ExternalLink, ChevronRight } from 'lucide-react'
3
+
4
+ export interface ContextMenuItem {
5
+ id: string
6
+ label: string
7
+ icon?: React.ReactNode
8
+ shortcut?: string
9
+ disabled?: boolean
10
+ danger?: boolean
11
+ divider?: boolean
12
+ submenu?: ContextMenuItem[]
13
+ onClick?: () => void
14
+ }
15
+
16
+ interface ContextMenuProps {
17
+ x: number
18
+ y: number
19
+ items: ContextMenuItem[]
20
+ onClose: () => void
21
+ }
22
+
23
+ export default function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
24
+ const menuRef = useRef<HTMLDivElement>(null)
25
+
26
+ // Close on outside click or escape
27
+ useEffect(() => {
28
+ const handleClick = (e: MouseEvent) => {
29
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
30
+ onClose()
31
+ }
32
+ }
33
+
34
+ const handleKeyDown = (e: KeyboardEvent) => {
35
+ if (e.key === 'Escape') {
36
+ onClose()
37
+ }
38
+ }
39
+
40
+ document.addEventListener('mousedown', handleClick)
41
+ document.addEventListener('keydown', handleKeyDown)
42
+
43
+ return () => {
44
+ document.removeEventListener('mousedown', handleClick)
45
+ document.removeEventListener('keydown', handleKeyDown)
46
+ }
47
+ }, [onClose])
48
+
49
+ // Adjust position to stay within viewport
50
+ useEffect(() => {
51
+ if (menuRef.current) {
52
+ const rect = menuRef.current.getBoundingClientRect()
53
+ const viewportWidth = window.innerWidth
54
+ const viewportHeight = window.innerHeight
55
+
56
+ if (rect.right > viewportWidth) {
57
+ menuRef.current.style.left = `${x - rect.width}px`
58
+ }
59
+ if (rect.bottom > viewportHeight) {
60
+ menuRef.current.style.top = `${y - rect.height}px`
61
+ }
62
+ }
63
+ }, [x, y])
64
+
65
+ const handleItemClick = (item: ContextMenuItem) => {
66
+ if (item.disabled || item.submenu) return
67
+ item.onClick?.()
68
+ onClose()
69
+ }
70
+
71
+ return (
72
+ <div
73
+ ref={menuRef}
74
+ className="fixed z-50 min-w-[180px] bg-hex-surface border border-hex-border rounded-lg shadow-xl py-1"
75
+ style={{ left: x, top: y }}
76
+ >
77
+ {items.map((item) =>
78
+ item.divider ? (
79
+ <div key={item.id} className="h-px bg-hex-border my-1" />
80
+ ) : (
81
+ <button
82
+ key={item.id}
83
+ onClick={() => handleItemClick(item)}
84
+ disabled={item.disabled}
85
+ className={`
86
+ w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left
87
+ transition-colors
88
+ ${item.disabled
89
+ ? 'text-hex-text-muted cursor-not-allowed'
90
+ : item.danger
91
+ ? 'text-hex-error hover:bg-hex-error/10'
92
+ : 'text-hex-text hover:bg-hex-border/50'
93
+ }
94
+ `}
95
+ >
96
+ {item.icon && (
97
+ <span className="w-4 h-4 flex items-center justify-center">
98
+ {item.icon}
99
+ </span>
100
+ )}
101
+ <span className="flex-1">{item.label}</span>
102
+ {item.shortcut && (
103
+ <span className="text-hex-text-muted text-[10px]">{item.shortcut}</span>
104
+ )}
105
+ {item.submenu && (
106
+ <ChevronRight size={12} className="text-hex-text-muted" />
107
+ )}
108
+ </button>
109
+ )
110
+ )}
111
+ </div>
112
+ )
113
+ }
114
+
115
+ // Helper to create standard node context menu items
116
+ export function createNodeContextMenuItems(
117
+ _nodeId: string,
118
+ options: {
119
+ onEdit?: () => void
120
+ onDuplicate?: () => void
121
+ onDelete?: () => void
122
+ onDisconnect?: () => void
123
+ onViewCode?: () => void
124
+ }
125
+ ): ContextMenuItem[] {
126
+ const items: ContextMenuItem[] = []
127
+
128
+ if (options.onEdit) {
129
+ items.push({
130
+ id: 'edit',
131
+ label: 'Edit Node',
132
+ icon: <Edit3 size={14} />,
133
+ onClick: options.onEdit,
134
+ })
135
+ }
136
+
137
+ if (options.onViewCode) {
138
+ items.push({
139
+ id: 'view-code',
140
+ label: 'View Code',
141
+ icon: <ExternalLink size={14} />,
142
+ onClick: options.onViewCode,
143
+ })
144
+ }
145
+
146
+ if (options.onDuplicate) {
147
+ items.push({
148
+ id: 'duplicate',
149
+ label: 'Duplicate',
150
+ icon: <Copy size={14} />,
151
+ shortcut: 'Cmd+D',
152
+ onClick: options.onDuplicate,
153
+ })
154
+ }
155
+
156
+ if (options.onDisconnect) {
157
+ items.push({
158
+ id: 'divider-1',
159
+ label: '',
160
+ divider: true,
161
+ })
162
+ items.push({
163
+ id: 'disconnect',
164
+ label: 'Disconnect All',
165
+ icon: <Unlink size={14} />,
166
+ onClick: options.onDisconnect,
167
+ })
168
+ }
169
+
170
+ if (options.onDelete) {
171
+ items.push({
172
+ id: 'divider-2',
173
+ label: '',
174
+ divider: true,
175
+ })
176
+ items.push({
177
+ id: 'delete',
178
+ label: 'Delete',
179
+ icon: <Trash2 size={14} />,
180
+ shortcut: 'Del',
181
+ danger: true,
182
+ onClick: options.onDelete,
183
+ })
184
+ }
185
+
186
+ return items
187
+ }
@@ -0,0 +1,123 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { File, Folder, RefreshCw, ChevronRight, ChevronDown } from 'lucide-react'
3
+ import { listFiles, readFile } from '../lib/api'
4
+ import { useStudioStore } from '../lib/store'
5
+ import type { FileInfo } from '../types'
6
+
7
+ export default function FileBrowser() {
8
+ const [files, setFiles] = useState<FileInfo[]>([])
9
+ const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set())
10
+ const [isLoading, setIsLoading] = useState(true)
11
+ const [error, setError] = useState<string | null>(null)
12
+
13
+ const { currentFile, setCurrentFile, setYamlContent, syncYamlToCanvas, setIsDirty } = useStudioStore()
14
+
15
+ useEffect(() => {
16
+ loadFiles()
17
+ }, [])
18
+
19
+ const loadFiles = async () => {
20
+ setIsLoading(true)
21
+ setError(null)
22
+ try {
23
+ const fileList = await listFiles('')
24
+ setFiles(fileList)
25
+ } catch (err) {
26
+ setError(err instanceof Error ? err.message : 'Failed to load files')
27
+ } finally {
28
+ setIsLoading(false)
29
+ }
30
+ }
31
+
32
+ const handleFileClick = async (file: FileInfo) => {
33
+ if (file.is_directory) {
34
+ setExpandedDirs((prev) => {
35
+ const next = new Set(prev)
36
+ if (next.has(file.path)) {
37
+ next.delete(file.path)
38
+ } else {
39
+ next.add(file.path)
40
+ }
41
+ return next
42
+ })
43
+ return
44
+ }
45
+
46
+ try {
47
+ const content = await readFile(file.path)
48
+ setCurrentFile(file.path)
49
+ setYamlContent(content.content)
50
+ setIsDirty(false)
51
+ setTimeout(() => {
52
+ syncYamlToCanvas()
53
+ }, 0)
54
+ } catch (err) {
55
+ console.error('Failed to load file:', err)
56
+ }
57
+ }
58
+
59
+ const renderFile = (file: FileInfo) => {
60
+ const isSelected = currentFile === file.path
61
+ const isExpanded = expandedDirs.has(file.path)
62
+
63
+ return (
64
+ <button
65
+ key={file.path}
66
+ onClick={() => handleFileClick(file)}
67
+ className={
68
+ 'w-full flex items-center gap-2 py-1.5 px-2 text-left transition-colors rounded-sm ' +
69
+ (isSelected
70
+ ? 'bg-hex-accent/20 text-hex-accent'
71
+ : 'text-hex-text hover:bg-hex-border/30')
72
+ }
73
+ >
74
+ {file.is_directory ? (
75
+ <>
76
+ {isExpanded ? (
77
+ <ChevronDown size={12} className="text-hex-text-muted flex-shrink-0" />
78
+ ) : (
79
+ <ChevronRight size={12} className="text-hex-text-muted flex-shrink-0" />
80
+ )}
81
+ <Folder size={14} className="text-hex-warning flex-shrink-0" />
82
+ </>
83
+ ) : (
84
+ <>
85
+ <span className="w-3" />
86
+ <File size={14} className="text-hex-text-muted flex-shrink-0" />
87
+ </>
88
+ )}
89
+ <span className="text-xs truncate">{file.name}</span>
90
+ </button>
91
+ )
92
+ }
93
+
94
+ return (
95
+ <div className="flex flex-col border-b border-hex-border">
96
+ <div className="flex items-center justify-between p-2 border-b border-hex-border">
97
+ <h2 className="text-xs font-semibold uppercase text-hex-text-muted tracking-wider">
98
+ Files
99
+ </h2>
100
+ <button
101
+ onClick={loadFiles}
102
+ disabled={isLoading}
103
+ className="p-1 rounded hover:bg-hex-border/50 transition-colors text-hex-text-muted hover:text-hex-text"
104
+ title="Refresh files"
105
+ >
106
+ <RefreshCw size={12} className={isLoading ? 'animate-spin' : ''} />
107
+ </button>
108
+ </div>
109
+
110
+ <div className="max-h-48 overflow-y-auto p-1">
111
+ {error ? (
112
+ <div className="p-2 text-xs text-hex-error">{error}</div>
113
+ ) : isLoading ? (
114
+ <div className="p-2 text-xs text-hex-text-muted">Loading...</div>
115
+ ) : files.length === 0 ? (
116
+ <div className="p-2 text-xs text-hex-text-muted">No YAML files found</div>
117
+ ) : (
118
+ files.map((file) => renderFile(file))
119
+ )}
120
+ </div>
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,181 @@
1
+ import { Save, Play, RefreshCw, Settings, Package, Plug } from 'lucide-react'
2
+ import { useStudioStore } from '../lib/store'
3
+ import { saveFile, executePipeline, downloadProject } from '../lib/api'
4
+ import { useState } from 'react'
5
+ import PluginManager from './PluginManager'
6
+
7
+ export default function Header() {
8
+ const [isRunning, setIsRunning] = useState(false)
9
+ const [isExporting, setIsExporting] = useState(false)
10
+ const [isPluginManagerOpen, setIsPluginManagerOpen] = useState(false)
11
+
12
+ const {
13
+ currentFile,
14
+ yamlContent,
15
+ isDirty,
16
+ isSaving,
17
+ setIsSaving,
18
+ setIsDirty,
19
+ } = useStudioStore()
20
+
21
+ const handleSave = async () => {
22
+ if (!currentFile || !yamlContent) return
23
+
24
+ setIsSaving(true)
25
+ try {
26
+ await saveFile(currentFile, yamlContent)
27
+ setIsDirty(false)
28
+ } catch (error) {
29
+ console.error('Failed to save:', error)
30
+ alert(`Failed to save: ${error}`)
31
+ } finally {
32
+ setIsSaving(false)
33
+ }
34
+ }
35
+
36
+ const handleRun = async () => {
37
+ if (!yamlContent) {
38
+ alert('No content to run')
39
+ return
40
+ }
41
+
42
+ setIsRunning(true)
43
+ try {
44
+ const result = await executePipeline(yamlContent, {}, true)
45
+ if (result.success) {
46
+ alert(`Pipeline executed successfully in ${result.duration_ms.toFixed(0)}ms`)
47
+ } else {
48
+ alert(`Pipeline failed: ${result.error}`)
49
+ }
50
+ } catch (error) {
51
+ alert(`Execution error: ${error}`)
52
+ } finally {
53
+ setIsRunning(false)
54
+ }
55
+ }
56
+
57
+ const handleExport = async () => {
58
+ if (!yamlContent) {
59
+ alert('No pipeline to export')
60
+ return
61
+ }
62
+
63
+ setIsExporting(true)
64
+ try {
65
+ await downloadProject(yamlContent, undefined, true)
66
+ } catch (error) {
67
+ alert(`Export failed: ${error}`)
68
+ } finally {
69
+ setIsExporting(false)
70
+ }
71
+ }
72
+
73
+ return (
74
+ <header className="h-12 bg-hex-bg border-b border-hex-border flex items-center px-4 gap-4">
75
+ {/* Logo */}
76
+ <div className="flex items-center gap-2">
77
+ <div className="w-6 h-6 bg-hex-accent rounded flex items-center justify-center">
78
+ <span className="text-white text-xs font-bold">H</span>
79
+ </div>
80
+ <span className="text-sm font-semibold">
81
+ <span className="text-hex-accent">hexdag</span>
82
+ <span className="text-hex-text">studio</span>
83
+ </span>
84
+ </div>
85
+
86
+ {/* Current file */}
87
+ <div className="flex-1 flex items-center gap-2 min-w-0">
88
+ {currentFile && (
89
+ <>
90
+ <span className="text-xs text-hex-text-muted truncate">
91
+ {currentFile}
92
+ </span>
93
+ {isDirty && (
94
+ <span className="w-2 h-2 bg-hex-warning rounded-full" title="Unsaved changes" />
95
+ )}
96
+ </>
97
+ )}
98
+ </div>
99
+
100
+ {/* Actions */}
101
+ <div className="flex items-center gap-2">
102
+ <button
103
+ onClick={() => useStudioStore.getState().syncYamlToCanvas()}
104
+ className="p-2 rounded hover:bg-hex-border/50 transition-colors text-hex-text-muted hover:text-hex-text"
105
+ title="Refresh canvas from YAML"
106
+ >
107
+ <RefreshCw size={16} />
108
+ </button>
109
+
110
+ <button
111
+ onClick={handleRun}
112
+ disabled={isRunning || !yamlContent}
113
+ className={`
114
+ flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium
115
+ transition-colors
116
+ ${isRunning || !yamlContent
117
+ ? 'bg-hex-border/50 text-hex-text-muted cursor-not-allowed'
118
+ : 'bg-hex-success/20 text-hex-success hover:bg-hex-success/30'}
119
+ `}
120
+ >
121
+ <Play size={14} className={isRunning ? 'animate-pulse' : ''} />
122
+ {isRunning ? 'Running...' : 'Test Run'}
123
+ </button>
124
+
125
+ <button
126
+ onClick={handleSave}
127
+ disabled={isSaving || !currentFile || !isDirty}
128
+ className={`
129
+ flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium
130
+ transition-colors
131
+ ${isSaving || !currentFile || !isDirty
132
+ ? 'bg-hex-border/50 text-hex-text-muted cursor-not-allowed'
133
+ : 'bg-hex-accent text-white hover:bg-hex-accent-hover'}
134
+ `}
135
+ >
136
+ <Save size={14} className={isSaving ? 'animate-pulse' : ''} />
137
+ {isSaving ? 'Saving...' : 'Save'}
138
+ </button>
139
+
140
+ <div className="w-px h-6 bg-hex-border" />
141
+
142
+ <button
143
+ onClick={handleExport}
144
+ disabled={isExporting || !yamlContent}
145
+ className={`
146
+ flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium
147
+ transition-colors
148
+ ${isExporting || !yamlContent
149
+ ? 'bg-hex-border/50 text-hex-text-muted cursor-not-allowed'
150
+ : 'bg-purple-500/20 text-purple-400 hover:bg-purple-500/30'}
151
+ `}
152
+ title="Export as standalone Python project"
153
+ >
154
+ <Package size={14} className={isExporting ? 'animate-pulse' : ''} />
155
+ {isExporting ? 'Exporting...' : 'Export Project'}
156
+ </button>
157
+
158
+ <button
159
+ onClick={() => setIsPluginManagerOpen(true)}
160
+ className="p-2 rounded hover:bg-hex-border/50 transition-colors text-hex-text-muted hover:text-hex-text"
161
+ title="Plugin Manager"
162
+ >
163
+ <Plug size={16} />
164
+ </button>
165
+
166
+ <button
167
+ className="p-2 rounded hover:bg-hex-border/50 transition-colors text-hex-text-muted hover:text-hex-text"
168
+ title="Settings"
169
+ >
170
+ <Settings size={16} />
171
+ </button>
172
+ </div>
173
+
174
+ {/* Plugin Manager Modal */}
175
+ <PluginManager
176
+ isOpen={isPluginManagerOpen}
177
+ onClose={() => setIsPluginManagerOpen(false)}
178
+ />
179
+ </header>
180
+ )
181
+ }
@@ -0,0 +1,193 @@
1
+ import { memo } from 'react'
2
+ import { Handle, Position } from '@xyflow/react'
3
+ import {
4
+ Code,
5
+ Brain,
6
+ Cpu,
7
+ FileText,
8
+ Scissors,
9
+ Bot,
10
+ GitBranch,
11
+ Repeat,
12
+ Box,
13
+ AlertCircle,
14
+ Play,
15
+ FileInput,
16
+ FileOutput,
17
+ Table,
18
+ Package,
19
+ } from 'lucide-react'
20
+ import type { HexdagNodeData } from '../types'
21
+ import { getNodeColor, getNodeTemplate } from '../lib/nodeTemplates'
22
+
23
+ const iconMap: Record<string, typeof Code> = {
24
+ // Core/builtin nodes
25
+ function_node: Code,
26
+ llm_node: Brain,
27
+ raw_llm_node: Cpu,
28
+ prompt_node: FileText,
29
+ parser_node: Scissors,
30
+ agent_node: Bot,
31
+ conditional_node: GitBranch,
32
+ loop_node: Repeat,
33
+ }
34
+
35
+ // Dynamic icon selection for plugin nodes (used when not in iconMap)
36
+ function getPluginNodeIcon(kind: string): typeof Code {
37
+ const kindLower = kind.toLowerCase()
38
+ if (kindLower.includes('file_reader') || kindLower.includes('input')) return FileInput
39
+ if (kindLower.includes('file_writer') || kindLower.includes('output')) return FileOutput
40
+ if (kindLower.includes('transform') || kindLower.includes('pandas')) return Table
41
+ if (kindLower.includes('llm') || kindLower.includes('openai')) return Brain
42
+ if (kindLower.includes('database') || kindLower.includes('sql')) return Cpu
43
+ return Package
44
+ }
45
+
46
+ interface HexdagNodeProps {
47
+ data: HexdagNodeData
48
+ selected?: boolean
49
+ }
50
+
51
+ function HexdagNode({ data, selected }: HexdagNodeProps) {
52
+ // Use iconMap for core nodes, dynamic icon for plugin nodes
53
+ const Icon = iconMap[data.kind] || getPluginNodeIcon(data.kind) || Box
54
+ const color = getNodeColor(data.kind)
55
+ const template = getNodeTemplate(data.kind)
56
+
57
+ // Get key spec info to display
58
+ const specPreview = getSpecPreview(data.kind, data.spec)
59
+
60
+ return (
61
+ <div
62
+ className={`
63
+ relative rounded-lg min-w-[200px] max-w-[280px]
64
+ bg-hex-surface border-2 transition-all
65
+ ${selected ? 'border-hex-accent shadow-lg shadow-hex-accent/30 scale-[1.02]' : 'border-hex-border hover:border-hex-border/80'}
66
+ ${!data.isValid ? 'border-hex-error' : ''}
67
+ `}
68
+ style={{
69
+ borderTopColor: color,
70
+ borderTopWidth: 3,
71
+ }}
72
+ >
73
+ {/* Input handle */}
74
+ <Handle
75
+ type="target"
76
+ position={Position.Left}
77
+ className="!w-3 !h-3 !bg-hex-accent !border-2 !border-hex-surface !-left-1.5"
78
+ />
79
+
80
+ {/* Header */}
81
+ <div className="px-3 py-2 flex items-center gap-2 border-b border-hex-border/50">
82
+ <div
83
+ className="w-7 h-7 rounded-md flex items-center justify-center flex-shrink-0"
84
+ style={{ backgroundColor: `${color}15` }}
85
+ >
86
+ <Icon size={14} style={{ color }} />
87
+ </div>
88
+ <div className="flex-1 min-w-0">
89
+ <div className="text-xs font-semibold text-hex-text truncate">
90
+ {data.label}
91
+ </div>
92
+ <div className="text-[10px] text-hex-text-muted truncate">
93
+ {template?.label || data.kind.replace('_node', '')}
94
+ </div>
95
+ </div>
96
+ {!data.isValid && (
97
+ <AlertCircle size={14} className="text-hex-error flex-shrink-0" />
98
+ )}
99
+ </div>
100
+
101
+ {/* Spec Preview */}
102
+ {specPreview && (
103
+ <div className="px-3 py-2 space-y-1">
104
+ {specPreview.map((item, i) => (
105
+ <div key={i} className="flex items-start gap-2 text-[10px]">
106
+ <span className="text-hex-text-muted flex-shrink-0 w-16 truncate">
107
+ {item.key}:
108
+ </span>
109
+ <span className="text-hex-text truncate font-mono">
110
+ {item.value}
111
+ </span>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ )}
116
+
117
+ {/* Footer with status */}
118
+ <div className="px-3 py-1.5 bg-hex-bg/50 rounded-b-md flex items-center justify-between">
119
+ <div className="flex items-center gap-1">
120
+ <Play size={10} className="text-hex-text-muted" />
121
+ <span className="text-[9px] text-hex-text-muted uppercase tracking-wider">
122
+ Ready
123
+ </span>
124
+ </div>
125
+ {data.errors.length > 0 && (
126
+ <span className="text-[9px] text-hex-error">
127
+ {data.errors.length} error{data.errors.length !== 1 ? 's' : ''}
128
+ </span>
129
+ )}
130
+ </div>
131
+
132
+ {/* Output handle */}
133
+ <Handle
134
+ type="source"
135
+ position={Position.Right}
136
+ className="!w-3 !h-3 !bg-hex-accent !border-2 !border-hex-surface !-right-1.5"
137
+ />
138
+
139
+ {/* Selection indicator */}
140
+ {selected && (
141
+ <div className="absolute -inset-1 border-2 border-hex-accent/30 rounded-xl pointer-events-none" />
142
+ )}
143
+ </div>
144
+ )
145
+ }
146
+
147
+ // Extract key info to show in node preview
148
+ function getSpecPreview(kind: string, spec: Record<string, unknown>): Array<{ key: string; value: string }> | null {
149
+ const preview: Array<{ key: string; value: string }> = []
150
+
151
+ switch (kind) {
152
+ case 'function_node':
153
+ if (spec.fn) preview.push({ key: 'fn', value: truncate(String(spec.fn), 30) })
154
+ break
155
+ case 'llm_node':
156
+ if (spec.prompt_template) preview.push({ key: 'prompt', value: truncate(String(spec.prompt_template), 25) })
157
+ break
158
+ case 'raw_llm_node':
159
+ if (spec.messages_key) preview.push({ key: 'messages', value: String(spec.messages_key) })
160
+ break
161
+ case 'prompt_node':
162
+ if (spec.template) preview.push({ key: 'template', value: truncate(String(spec.template), 25) })
163
+ break
164
+ case 'parser_node':
165
+ if (spec.parser_type) preview.push({ key: 'parser', value: String(spec.parser_type) })
166
+ break
167
+ case 'agent_node':
168
+ if (spec.max_steps) preview.push({ key: 'max_steps', value: String(spec.max_steps) })
169
+ break
170
+ case 'conditional_node':
171
+ if (spec.condition) preview.push({ key: 'if', value: truncate(String(spec.condition), 25) })
172
+ break
173
+ case 'loop_node':
174
+ if (spec.items_key) preview.push({ key: 'items', value: String(spec.items_key) })
175
+ if (spec.max_iterations) preview.push({ key: 'max', value: String(spec.max_iterations) })
176
+ break
177
+ default:
178
+ // Show first 2 spec items
179
+ const entries = Object.entries(spec).slice(0, 2)
180
+ entries.forEach(([key, value]) => {
181
+ preview.push({ key, value: truncate(String(value), 20) })
182
+ })
183
+ }
184
+
185
+ return preview.length > 0 ? preview : null
186
+ }
187
+
188
+ function truncate(str: string, max: number): string {
189
+ if (str.length <= max) return str
190
+ return str.slice(0, max - 1) + '…'
191
+ }
192
+
193
+ export default memo(HexdagNode)