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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- 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,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
|
+
}
|