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,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
|
+
}
|