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,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)
|