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,262 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Code,
|
|
4
|
+
Brain,
|
|
5
|
+
Cpu,
|
|
6
|
+
FileText,
|
|
7
|
+
Scissors,
|
|
8
|
+
Bot,
|
|
9
|
+
GitBranch,
|
|
10
|
+
Repeat,
|
|
11
|
+
Box,
|
|
12
|
+
GripVertical,
|
|
13
|
+
Plug,
|
|
14
|
+
ChevronDown,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
Loader2,
|
|
17
|
+
Package,
|
|
18
|
+
FileInput,
|
|
19
|
+
FileOutput,
|
|
20
|
+
Table,
|
|
21
|
+
Mail,
|
|
22
|
+
Send,
|
|
23
|
+
} from 'lucide-react'
|
|
24
|
+
import { nodeTemplates } from '../lib/nodeTemplates'
|
|
25
|
+
import { getAllPluginNodes, type PluginNode } from '../lib/api'
|
|
26
|
+
|
|
27
|
+
const iconMap: Record<string, typeof Code> = {
|
|
28
|
+
Code,
|
|
29
|
+
Brain,
|
|
30
|
+
Cpu,
|
|
31
|
+
FileText,
|
|
32
|
+
Scissors,
|
|
33
|
+
Bot,
|
|
34
|
+
GitBranch,
|
|
35
|
+
Repeat,
|
|
36
|
+
Box,
|
|
37
|
+
Plug,
|
|
38
|
+
Package,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PluginNodeSection {
|
|
42
|
+
plugin: string
|
|
43
|
+
nodes: PluginNode[]
|
|
44
|
+
expanded: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function NodePalette() {
|
|
48
|
+
const [pluginNodeSections, setPluginNodeSections] = useState<PluginNodeSection[]>([])
|
|
49
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
50
|
+
const [builtinExpanded, setBuiltinExpanded] = useState(true)
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
loadPlugins()
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const loadPlugins = async () => {
|
|
57
|
+
try {
|
|
58
|
+
setIsLoading(true)
|
|
59
|
+
const nodes = await getAllPluginNodes()
|
|
60
|
+
|
|
61
|
+
// Group nodes by plugin
|
|
62
|
+
const groupedNodes = nodes.reduce(
|
|
63
|
+
(acc, node) => {
|
|
64
|
+
const plugin = node.plugin || 'unknown'
|
|
65
|
+
if (!acc[plugin]) {
|
|
66
|
+
acc[plugin] = []
|
|
67
|
+
}
|
|
68
|
+
acc[plugin].push(node)
|
|
69
|
+
return acc
|
|
70
|
+
},
|
|
71
|
+
{} as Record<string, PluginNode[]>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
setPluginNodeSections(
|
|
75
|
+
Object.entries(groupedNodes).map(([plugin, nodes]) => ({
|
|
76
|
+
plugin,
|
|
77
|
+
nodes,
|
|
78
|
+
expanded: true,
|
|
79
|
+
}))
|
|
80
|
+
)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Failed to load plugins:', error)
|
|
83
|
+
} finally {
|
|
84
|
+
setIsLoading(false)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const toggleNodeSection = (plugin: string) => {
|
|
89
|
+
setPluginNodeSections((sections) =>
|
|
90
|
+
sections.map((s) => (s.plugin === plugin ? { ...s, expanded: !s.expanded } : s))
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const onDragStart = (event: React.DragEvent, kind: string) => {
|
|
95
|
+
event.dataTransfer.setData('application/hexdag-node', kind)
|
|
96
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getPluginNodeIcon = (node: PluginNode) => {
|
|
100
|
+
const kindLower = node.kind.toLowerCase()
|
|
101
|
+
if (kindLower.includes('file_reader') || kindLower.includes('input')) return FileInput
|
|
102
|
+
if (kindLower.includes('file_writer') || kindLower.includes('output')) return FileOutput
|
|
103
|
+
if (kindLower.includes('outlook_reader') || kindLower.includes('mail_reader')) return Mail
|
|
104
|
+
if (kindLower.includes('outlook_sender') || kindLower.includes('mail_sender')) return Send
|
|
105
|
+
if (kindLower.includes('transform') || kindLower.includes('pandas')) return Table
|
|
106
|
+
if (kindLower.includes('llm') || kindLower.includes('openai')) return Brain
|
|
107
|
+
if (kindLower.includes('database') || kindLower.includes('cosmos')) return Cpu
|
|
108
|
+
if (kindLower.includes('storage') || kindLower.includes('blob')) return FileText
|
|
109
|
+
return Package
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="h-full flex flex-col">
|
|
114
|
+
<div className="p-3 border-b border-hex-border">
|
|
115
|
+
<h2 className="text-xs font-semibold uppercase text-hex-text-muted tracking-wider">
|
|
116
|
+
Node Palette
|
|
117
|
+
</h2>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex-1 overflow-y-auto">
|
|
120
|
+
{/* Built-in Nodes Section */}
|
|
121
|
+
<div className="border-b border-hex-border">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => setBuiltinExpanded(!builtinExpanded)}
|
|
124
|
+
className="w-full flex items-center gap-2 p-2 hover:bg-hex-border/30 transition-colors"
|
|
125
|
+
>
|
|
126
|
+
{builtinExpanded ? (
|
|
127
|
+
<ChevronDown size={14} className="text-hex-text-muted" />
|
|
128
|
+
) : (
|
|
129
|
+
<ChevronRight size={14} className="text-hex-text-muted" />
|
|
130
|
+
)}
|
|
131
|
+
<Box size={14} className="text-hex-accent" />
|
|
132
|
+
<span className="text-xs font-medium text-hex-text">Built-in Nodes</span>
|
|
133
|
+
<span className="ml-auto text-[10px] text-hex-text-muted">{nodeTemplates.length}</span>
|
|
134
|
+
</button>
|
|
135
|
+
{builtinExpanded && (
|
|
136
|
+
<div className="px-2 pb-2 space-y-1">
|
|
137
|
+
{nodeTemplates.map((template) => {
|
|
138
|
+
const Icon = iconMap[template.icon] || Box
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
key={template.kind}
|
|
142
|
+
draggable
|
|
143
|
+
onDragStart={(e) => onDragStart(e, template.kind)}
|
|
144
|
+
className="
|
|
145
|
+
flex items-center gap-2 p-2 rounded-md cursor-grab
|
|
146
|
+
bg-hex-bg hover:bg-hex-border/50 transition-colors
|
|
147
|
+
border border-transparent hover:border-hex-border
|
|
148
|
+
group
|
|
149
|
+
"
|
|
150
|
+
>
|
|
151
|
+
<div className="text-hex-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
|
|
152
|
+
<GripVertical size={12} />
|
|
153
|
+
</div>
|
|
154
|
+
<div
|
|
155
|
+
className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0"
|
|
156
|
+
style={{ backgroundColor: `${template.color}20` }}
|
|
157
|
+
>
|
|
158
|
+
<Icon size={14} style={{ color: template.color }} />
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex-1 min-w-0">
|
|
161
|
+
<div className="text-xs font-medium text-hex-text truncate">
|
|
162
|
+
{template.label}
|
|
163
|
+
</div>
|
|
164
|
+
<div className="text-[10px] text-hex-text-muted truncate">
|
|
165
|
+
{template.description}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Plugin Nodes Sections */}
|
|
176
|
+
{isLoading ? (
|
|
177
|
+
<div className="flex items-center justify-center p-4">
|
|
178
|
+
<Loader2 size={16} className="animate-spin text-hex-text-muted" />
|
|
179
|
+
<span className="ml-2 text-xs text-hex-text-muted">Loading plugins...</span>
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<>
|
|
183
|
+
{pluginNodeSections.map((section) => (
|
|
184
|
+
<div key={section.plugin} className="border-b border-hex-border">
|
|
185
|
+
<button
|
|
186
|
+
onClick={() => toggleNodeSection(section.plugin)}
|
|
187
|
+
className="w-full flex items-center gap-2 p-2 hover:bg-hex-border/30 transition-colors"
|
|
188
|
+
>
|
|
189
|
+
{section.expanded ? (
|
|
190
|
+
<ChevronDown size={14} className="text-hex-text-muted" />
|
|
191
|
+
) : (
|
|
192
|
+
<ChevronRight size={14} className="text-hex-text-muted" />
|
|
193
|
+
)}
|
|
194
|
+
<Plug size={14} className="text-green-500" />
|
|
195
|
+
<span className="text-xs font-medium text-hex-text truncate">
|
|
196
|
+
{section.plugin}
|
|
197
|
+
</span>
|
|
198
|
+
<span className="ml-auto text-[10px] text-hex-text-muted">
|
|
199
|
+
{section.nodes.length}
|
|
200
|
+
</span>
|
|
201
|
+
</button>
|
|
202
|
+
{section.expanded && (
|
|
203
|
+
<div className="px-2 pb-2 space-y-1">
|
|
204
|
+
{section.nodes.map((node) => {
|
|
205
|
+
const Icon = getPluginNodeIcon(node)
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
key={`${section.plugin}:${node.kind}`}
|
|
209
|
+
draggable
|
|
210
|
+
onDragStart={(e) => onDragStart(e, node.kind)}
|
|
211
|
+
className="
|
|
212
|
+
flex items-center gap-2 p-2 rounded-md cursor-grab
|
|
213
|
+
bg-hex-bg hover:bg-hex-border/50 transition-colors
|
|
214
|
+
border border-transparent hover:border-hex-border
|
|
215
|
+
group
|
|
216
|
+
"
|
|
217
|
+
>
|
|
218
|
+
<div className="text-hex-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
|
|
219
|
+
<GripVertical size={12} />
|
|
220
|
+
</div>
|
|
221
|
+
<div
|
|
222
|
+
className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0"
|
|
223
|
+
style={{ backgroundColor: `${node.color}20` }}
|
|
224
|
+
>
|
|
225
|
+
<Icon size={14} style={{ color: node.color }} />
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex-1 min-w-0">
|
|
228
|
+
<div className="text-xs font-medium text-hex-text truncate">
|
|
229
|
+
{node.name}
|
|
230
|
+
</div>
|
|
231
|
+
<div className="text-[10px] text-hex-text-muted truncate">
|
|
232
|
+
{node.description || `From ${section.plugin}`}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
})}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
|
|
243
|
+
{pluginNodeSections.length === 0 && (
|
|
244
|
+
<div className="p-4 text-center">
|
|
245
|
+
<Plug size={24} className="mx-auto mb-2 text-hex-text-muted opacity-50" />
|
|
246
|
+
<p className="text-xs text-hex-text-muted">No plugin nodes found</p>
|
|
247
|
+
<p className="text-[10px] text-hex-text-muted mt-1">
|
|
248
|
+
Add node plugins to hexdag_plugins/
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
<div className="p-3 border-t border-hex-border">
|
|
256
|
+
<p className="text-[10px] text-hex-text-muted text-center">
|
|
257
|
+
Drag nodes onto the canvas
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Brain,
|
|
4
|
+
Key,
|
|
5
|
+
Database,
|
|
6
|
+
HardDrive,
|
|
7
|
+
Plug,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
Plus,
|
|
11
|
+
X,
|
|
12
|
+
Loader2,
|
|
13
|
+
Info,
|
|
14
|
+
Wrench,
|
|
15
|
+
} from 'lucide-react'
|
|
16
|
+
import { getAllPluginAdapters, type PluginAdapter } from '../lib/api'
|
|
17
|
+
|
|
18
|
+
interface PortConfig {
|
|
19
|
+
adapter: string
|
|
20
|
+
config: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type PortsConfig = Record<string, PortConfig | undefined>
|
|
24
|
+
|
|
25
|
+
interface NodePortsSectionProps {
|
|
26
|
+
nodeId: string
|
|
27
|
+
requiredPorts: string[]
|
|
28
|
+
nodePorts: PortsConfig
|
|
29
|
+
onPortsChange: (ports: PortsConfig) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PORT_INFO: Record<string, { icon: typeof Brain; color: string; description: string }> = {
|
|
33
|
+
llm: {
|
|
34
|
+
icon: Brain,
|
|
35
|
+
color: '#6366f1',
|
|
36
|
+
description: 'Language model for this node',
|
|
37
|
+
},
|
|
38
|
+
memory: {
|
|
39
|
+
icon: Database,
|
|
40
|
+
color: '#14b8a6',
|
|
41
|
+
description: 'Memory for this node',
|
|
42
|
+
},
|
|
43
|
+
database: {
|
|
44
|
+
icon: Database,
|
|
45
|
+
color: '#ec4899',
|
|
46
|
+
description: 'Database for this node',
|
|
47
|
+
},
|
|
48
|
+
storage: {
|
|
49
|
+
icon: HardDrive,
|
|
50
|
+
color: '#22c55e',
|
|
51
|
+
description: 'Storage for this node',
|
|
52
|
+
},
|
|
53
|
+
secret: {
|
|
54
|
+
icon: Key,
|
|
55
|
+
color: '#f59e0b',
|
|
56
|
+
description: 'Secrets for this node',
|
|
57
|
+
},
|
|
58
|
+
tool_router: {
|
|
59
|
+
icon: Wrench,
|
|
60
|
+
color: '#8b5cf6',
|
|
61
|
+
description: 'Tool router for agent tools',
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default function NodePortsSection({
|
|
66
|
+
nodeId: _nodeId, // For future use (per-node tracking)
|
|
67
|
+
requiredPorts,
|
|
68
|
+
nodePorts,
|
|
69
|
+
onPortsChange,
|
|
70
|
+
}: NodePortsSectionProps) {
|
|
71
|
+
const [adapters, setAdapters] = useState<PluginAdapter[]>([])
|
|
72
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
73
|
+
const [expandedPorts, setExpandedPorts] = useState<Set<string>>(new Set(requiredPorts))
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
loadAdapters()
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const loadAdapters = async () => {
|
|
80
|
+
try {
|
|
81
|
+
setIsLoading(true)
|
|
82
|
+
const data = await getAllPluginAdapters()
|
|
83
|
+
setAdapters(data)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Failed to load adapters:', error)
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const togglePort = (portType: string) => {
|
|
92
|
+
setExpandedPorts((prev) => {
|
|
93
|
+
const next = new Set(prev)
|
|
94
|
+
if (next.has(portType)) {
|
|
95
|
+
next.delete(portType)
|
|
96
|
+
} else {
|
|
97
|
+
next.add(portType)
|
|
98
|
+
}
|
|
99
|
+
return next
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const setPortAdapter = (portType: string, adapterName: string) => {
|
|
104
|
+
const adapter = adapters.find((a) => a.name === adapterName)
|
|
105
|
+
const newPorts = { ...nodePorts }
|
|
106
|
+
|
|
107
|
+
if (!adapterName) {
|
|
108
|
+
delete newPorts[portType]
|
|
109
|
+
} else {
|
|
110
|
+
newPorts[portType] = {
|
|
111
|
+
adapter: adapterName,
|
|
112
|
+
config: adapter ? getDefaultConfig(adapter) : {},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onPortsChange(newPorts)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const updatePortConfig = (portType: string, key: string, value: unknown) => {
|
|
120
|
+
const newPorts = { ...nodePorts }
|
|
121
|
+
if (newPorts[portType]) {
|
|
122
|
+
newPorts[portType] = {
|
|
123
|
+
...newPorts[portType]!,
|
|
124
|
+
config: {
|
|
125
|
+
...newPorts[portType]!.config,
|
|
126
|
+
[key]: value,
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
onPortsChange(newPorts)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const deletePortConfig = (portType: string, key: string) => {
|
|
134
|
+
const newPorts = { ...nodePorts }
|
|
135
|
+
if (newPorts[portType]) {
|
|
136
|
+
const newConfig = { ...newPorts[portType]!.config }
|
|
137
|
+
delete newConfig[key]
|
|
138
|
+
newPorts[portType] = {
|
|
139
|
+
...newPorts[portType]!,
|
|
140
|
+
config: newConfig,
|
|
141
|
+
}
|
|
142
|
+
onPortsChange(newPorts)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const getDefaultConfig = (adapter: PluginAdapter): Record<string, unknown> => {
|
|
147
|
+
const config: Record<string, unknown> = {}
|
|
148
|
+
const schema = adapter.config_schema as {
|
|
149
|
+
properties?: Record<string, { default?: unknown }>
|
|
150
|
+
required?: string[]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (schema?.properties) {
|
|
154
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
155
|
+
if (prop.default !== undefined && prop.default !== null) {
|
|
156
|
+
config[key] = prop.default
|
|
157
|
+
} else if (schema.required?.includes(key)) {
|
|
158
|
+
config[key] = `\${${key.toUpperCase()}}`
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return config
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const getAdaptersForPort = (portType: string) => {
|
|
167
|
+
return adapters.filter((a) => a.port_type === portType)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (requiredPorts.length === 0) {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isLoading) {
|
|
175
|
+
return (
|
|
176
|
+
<div className="flex items-center justify-center py-4">
|
|
177
|
+
<Loader2 size={16} className="animate-spin text-hex-text-muted" />
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="space-y-2">
|
|
184
|
+
{/* Info banner */}
|
|
185
|
+
<div className="flex items-start gap-2 p-2 bg-hex-accent/10 rounded text-[10px] text-hex-accent">
|
|
186
|
+
<Info size={12} className="flex-shrink-0 mt-0.5" />
|
|
187
|
+
<span>
|
|
188
|
+
Override pipeline-level ports for this node. Leave empty to use global configuration.
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Port List */}
|
|
193
|
+
{requiredPorts.map((portType) => {
|
|
194
|
+
const portInfo = PORT_INFO[portType] || {
|
|
195
|
+
icon: Plug,
|
|
196
|
+
color: '#6b7280',
|
|
197
|
+
description: `${portType} port`,
|
|
198
|
+
}
|
|
199
|
+
const Icon = portInfo.icon
|
|
200
|
+
const availableAdapters = getAdaptersForPort(portType)
|
|
201
|
+
const currentPort = nodePorts[portType]
|
|
202
|
+
const isExpanded = expandedPorts.has(portType)
|
|
203
|
+
const isConfigured = !!currentPort?.adapter
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div key={portType} className="border border-hex-border rounded overflow-hidden">
|
|
207
|
+
<button
|
|
208
|
+
onClick={() => togglePort(portType)}
|
|
209
|
+
className="w-full flex items-center gap-2 p-2 hover:bg-hex-border/30 transition-colors"
|
|
210
|
+
>
|
|
211
|
+
{isExpanded ? (
|
|
212
|
+
<ChevronDown size={12} className="text-hex-text-muted" />
|
|
213
|
+
) : (
|
|
214
|
+
<ChevronRight size={12} className="text-hex-text-muted" />
|
|
215
|
+
)}
|
|
216
|
+
<div
|
|
217
|
+
className="w-5 h-5 rounded flex items-center justify-center"
|
|
218
|
+
style={{ backgroundColor: `${portInfo.color}20` }}
|
|
219
|
+
>
|
|
220
|
+
<Icon size={10} style={{ color: portInfo.color }} />
|
|
221
|
+
</div>
|
|
222
|
+
<span className="flex-1 text-left text-[11px] font-medium text-hex-text capitalize">
|
|
223
|
+
{portType.replace('_', ' ')}
|
|
224
|
+
</span>
|
|
225
|
+
{isConfigured ? (
|
|
226
|
+
<span
|
|
227
|
+
className="text-[9px] px-1.5 py-0.5 rounded"
|
|
228
|
+
style={{ backgroundColor: `${portInfo.color}20`, color: portInfo.color }}
|
|
229
|
+
>
|
|
230
|
+
{currentPort.adapter}
|
|
231
|
+
</span>
|
|
232
|
+
) : (
|
|
233
|
+
<span className="text-[9px] text-hex-text-muted italic">Global</span>
|
|
234
|
+
)}
|
|
235
|
+
</button>
|
|
236
|
+
|
|
237
|
+
{isExpanded && (
|
|
238
|
+
<div className="px-2 pb-2 space-y-2 border-t border-hex-border bg-hex-bg/30">
|
|
239
|
+
{/* Description */}
|
|
240
|
+
<div className="flex items-start gap-2 p-1.5 text-[9px] text-hex-text-muted">
|
|
241
|
+
<span>{portInfo.description}</span>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{/* Adapter Selection */}
|
|
245
|
+
<div>
|
|
246
|
+
<label className="block text-[9px] font-medium text-hex-text-muted mb-1 uppercase tracking-wider">
|
|
247
|
+
Adapter Override
|
|
248
|
+
</label>
|
|
249
|
+
{availableAdapters.length === 0 ? (
|
|
250
|
+
<p className="text-[9px] text-hex-text-muted italic">
|
|
251
|
+
No adapters available for {portType}
|
|
252
|
+
</p>
|
|
253
|
+
) : (
|
|
254
|
+
<select
|
|
255
|
+
value={currentPort?.adapter || ''}
|
|
256
|
+
onChange={(e) => setPortAdapter(portType, e.target.value)}
|
|
257
|
+
className="w-full bg-hex-bg border border-hex-border rounded px-2 py-1 text-[11px] text-hex-text focus:border-hex-accent focus:outline-none"
|
|
258
|
+
>
|
|
259
|
+
<option value="">-- Use global config --</option>
|
|
260
|
+
{availableAdapters.map((adapter) => (
|
|
261
|
+
<option key={adapter.name} value={adapter.name}>
|
|
262
|
+
{adapter.name} ({adapter.plugin})
|
|
263
|
+
</option>
|
|
264
|
+
))}
|
|
265
|
+
</select>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Adapter Config */}
|
|
270
|
+
{currentPort && currentPort.config && Object.keys(currentPort.config).length > 0 && (
|
|
271
|
+
<div className="space-y-1.5">
|
|
272
|
+
<label className="block text-[9px] font-medium text-hex-text-muted uppercase tracking-wider">
|
|
273
|
+
Configuration
|
|
274
|
+
</label>
|
|
275
|
+
{Object.entries(currentPort.config).map(([key, value]) => (
|
|
276
|
+
<ConfigField
|
|
277
|
+
key={key}
|
|
278
|
+
name={key}
|
|
279
|
+
value={value}
|
|
280
|
+
onChange={(v) => updatePortConfig(portType, key, v)}
|
|
281
|
+
onDelete={() => deletePortConfig(portType, key)}
|
|
282
|
+
/>
|
|
283
|
+
))}
|
|
284
|
+
<AddConfigButton
|
|
285
|
+
onAdd={(key) => updatePortConfig(portType, key, '')}
|
|
286
|
+
existingKeys={Object.keys(currentPort.config)}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ConfigField({
|
|
300
|
+
name,
|
|
301
|
+
value,
|
|
302
|
+
onChange,
|
|
303
|
+
onDelete,
|
|
304
|
+
}: {
|
|
305
|
+
name: string
|
|
306
|
+
value: unknown
|
|
307
|
+
onChange: (value: unknown) => void
|
|
308
|
+
onDelete: () => void
|
|
309
|
+
}) {
|
|
310
|
+
const isSecret =
|
|
311
|
+
name.toLowerCase().includes('key') ||
|
|
312
|
+
name.toLowerCase().includes('secret') ||
|
|
313
|
+
name.toLowerCase().includes('password') ||
|
|
314
|
+
name.toLowerCase().includes('token')
|
|
315
|
+
|
|
316
|
+
const isEnvVar = typeof value === 'string' && value.startsWith('${') && value.endsWith('}')
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div className="group flex items-start gap-1.5">
|
|
320
|
+
<div className="flex-1">
|
|
321
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
322
|
+
<label className="text-[9px] text-hex-text-muted">{name}</label>
|
|
323
|
+
<button
|
|
324
|
+
onClick={onDelete}
|
|
325
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-hex-error/20 rounded transition-all"
|
|
326
|
+
>
|
|
327
|
+
<X size={8} className="text-hex-error" />
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
<div className="relative">
|
|
331
|
+
<input
|
|
332
|
+
type={isSecret && !isEnvVar ? 'password' : 'text'}
|
|
333
|
+
value={String(value)}
|
|
334
|
+
onChange={(e) => onChange(e.target.value)}
|
|
335
|
+
className="w-full bg-hex-bg border border-hex-border rounded px-1.5 py-0.5 text-[10px] text-hex-text focus:border-hex-accent focus:outline-none font-mono"
|
|
336
|
+
placeholder={isSecret ? '${ENV_VAR}' : ''}
|
|
337
|
+
/>
|
|
338
|
+
{isEnvVar && (
|
|
339
|
+
<span className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[8px] px-1 py-0.5 rounded bg-amber-500/20 text-amber-500">
|
|
340
|
+
env
|
|
341
|
+
</span>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function AddConfigButton({
|
|
350
|
+
onAdd,
|
|
351
|
+
existingKeys,
|
|
352
|
+
}: {
|
|
353
|
+
onAdd: (key: string) => void
|
|
354
|
+
existingKeys: string[]
|
|
355
|
+
}) {
|
|
356
|
+
const [isAdding, setIsAdding] = useState(false)
|
|
357
|
+
const [newKey, setNewKey] = useState('')
|
|
358
|
+
|
|
359
|
+
const handleAdd = () => {
|
|
360
|
+
if (!newKey || existingKeys.includes(newKey)) return
|
|
361
|
+
onAdd(newKey)
|
|
362
|
+
setNewKey('')
|
|
363
|
+
setIsAdding(false)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (isAdding) {
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex gap-1">
|
|
369
|
+
<input
|
|
370
|
+
type="text"
|
|
371
|
+
value={newKey}
|
|
372
|
+
onChange={(e) => setNewKey(e.target.value)}
|
|
373
|
+
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
|
374
|
+
placeholder="Config key"
|
|
375
|
+
autoFocus
|
|
376
|
+
className="flex-1 bg-hex-bg border border-hex-border rounded px-1.5 py-0.5 text-[10px] text-hex-text focus:border-hex-accent focus:outline-none"
|
|
377
|
+
/>
|
|
378
|
+
<button
|
|
379
|
+
onClick={handleAdd}
|
|
380
|
+
className="px-1.5 py-0.5 text-[9px] bg-hex-accent text-white rounded hover:bg-hex-accent-hover"
|
|
381
|
+
>
|
|
382
|
+
Add
|
|
383
|
+
</button>
|
|
384
|
+
<button
|
|
385
|
+
onClick={() => setIsAdding(false)}
|
|
386
|
+
className="px-1.5 py-0.5 text-[9px] bg-hex-border text-hex-text rounded"
|
|
387
|
+
>
|
|
388
|
+
Cancel
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<button
|
|
396
|
+
onClick={() => setIsAdding(true)}
|
|
397
|
+
className="w-full flex items-center justify-center gap-1 py-0.5 text-[9px] text-hex-text-muted hover:text-hex-text border border-dashed border-hex-border rounded hover:border-hex-accent transition-colors"
|
|
398
|
+
>
|
|
399
|
+
<Plus size={8} />
|
|
400
|
+
Add config
|
|
401
|
+
</button>
|
|
402
|
+
)
|
|
403
|
+
}
|