openfree commited on
Commit
d782a70
·
verified ·
1 Parent(s): f16700d

Upload Index.svelte

Browse files
Files changed (1) hide show
  1. src/frontend/Index.svelte +2583 -0
src/frontend/Index.svelte ADDED
@@ -0,0 +1,2583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onMount } from 'svelte';
3
+
4
+ export let value: { nodes: any[]; edges: any[] } = { nodes: [], edges: [] };
5
+ export let elem_id = "";
6
+ export let elem_classes: string[] = [];
7
+ export let visible = true;
8
+ export const container = true;
9
+ export const scale: number | null = null;
10
+ export let min_width: number | undefined = undefined;
11
+ export const gradio: any = {};
12
+
13
+ const dispatch = createEventDispatcher<{
14
+ change: { nodes: any[]; edges: any[] };
15
+ input: { nodes: any[]; edges: any[] };
16
+ }>();
17
+
18
+ // State management
19
+ let canvas: HTMLDivElement;
20
+ let canvasContainer: HTMLDivElement;
21
+ let isDragging = false;
22
+ let isDraggingFromSidebar = false;
23
+ let dragNode: any = null;
24
+ let dragOffset = { x: 0, y: 0 };
25
+ let isConnecting = false;
26
+ let connectionStart: any = null;
27
+ let mousePos = { x: 0, y: 0 };
28
+ let selectedNode: any = null;
29
+ let sidebarCollapsed = false;
30
+ let propertyPanelCollapsed = false;
31
+
32
+ // Workflow metadata
33
+ let workflowName = "My Workflow";
34
+ let workflowId = "workflow-" + Date.now();
35
+
36
+ // Zoom and pan state
37
+ let zoomLevel = 0.6;
38
+ let panOffset = { x: 0, y: 0 };
39
+ let isPanning = false;
40
+ let lastPanPoint = { x: 0, y: 0 };
41
+
42
+ // Define default workflow
43
+ const defaultWorkflow = {
44
+ workflow_id: "workflow-" + Date.now(),
45
+ workflow_name: "My Workflow",
46
+ nodes: [],
47
+ edges: []
48
+ };
49
+
50
+
51
+
52
+ // Initialize nodes and edges
53
+ let nodes = value?.nodes || [];
54
+ let edges = value?.edges || [];
55
+ // let nodes = value?.nodes?.length > 0 ? [...value.nodes] : defaultWorkflow.nodes;
56
+ // let edges = value?.edges?.length > 0 ? [...value.edges] : defaultWorkflow.edges;
57
+
58
+ // Initialize workflow metadata
59
+ if (value?.workflow_name) {
60
+ workflowName = value.workflow_name;
61
+ }
62
+ if (value?.workflow_id) {
63
+ workflowId = value.workflow_id;
64
+ }
65
+
66
+
67
+ $: if (!value) {
68
+ value = { nodes: [], edges: [] };
69
+ }
70
+
71
+ // Component categories with new node types
72
+ const componentCategories = {
73
+ 'Input/Output': {
74
+ icon: '📥',
75
+ components: {
76
+ ChatInput: {
77
+ label: 'Chat Input',
78
+ icon: '💬',
79
+ color: '#4CAF50',
80
+ defaultData: {
81
+ display_name: 'Chat Input',
82
+ template: {
83
+ input_value: {
84
+ display_name: 'User Message',
85
+ type: 'string',
86
+ value: '',
87
+ is_handle: true
88
+ }
89
+ },
90
+ resources: {
91
+ cpu: 0.1,
92
+ memory: '128Mi',
93
+ gpu: 'none'
94
+ }
95
+ }
96
+ },
97
+ ChatOutput: {
98
+ label: 'Chat Output',
99
+ icon: '💭',
100
+ color: '#F44336',
101
+ defaultData: {
102
+ display_name: 'Chat Output',
103
+ template: {
104
+ response: {
105
+ display_name: 'AI Response',
106
+ type: 'string',
107
+ is_handle: true
108
+ }
109
+ },
110
+ resources: {
111
+ cpu: 0.1,
112
+ memory: '128Mi',
113
+ gpu: 'none'
114
+ }
115
+ }
116
+ },
117
+ Input: {
118
+ label: 'Input',
119
+ icon: '📥',
120
+ color: '#2196F3',
121
+ defaultData: {
122
+ display_name: 'Source Data',
123
+ template: {
124
+ data_type: {
125
+ display_name: 'Data Type',
126
+ type: 'options',
127
+ options: ['string', 'image', 'video', 'audio', 'file'],
128
+ value: 'string'
129
+ },
130
+ value: {
131
+ display_name: 'Value or Path',
132
+ type: 'string',
133
+ value: 'This is the initial text.'
134
+ },
135
+ data: {
136
+ display_name: 'Output Data',
137
+ type: 'object',
138
+ is_handle: true
139
+ }
140
+ },
141
+ resources: {
142
+ cpu: 0.1,
143
+ memory: '128Mi',
144
+ gpu: 'none'
145
+ }
146
+ }
147
+ },
148
+ Output: {
149
+ label: 'Output',
150
+ icon: '📤',
151
+ color: '#FF9800',
152
+ defaultData: {
153
+ display_name: 'Final Result',
154
+ template: {
155
+ input_data: {
156
+ display_name: 'Input Data',
157
+ type: 'object',
158
+ is_handle: true
159
+ }
160
+ },
161
+ resources: {
162
+ cpu: 0.1,
163
+ memory: '128Mi',
164
+ gpu: 'none'
165
+ }
166
+ }
167
+ }
168
+ }
169
+ },
170
+ 'AI & Language': {
171
+ icon: '🤖',
172
+ components: {
173
+ llmNode: { // ① 새 노드
174
+ label: 'AI Processing',
175
+ icon: '🧠',
176
+ color: '#2563eb',
177
+ defaultData: {
178
+ display_name: 'AI Processing',
179
+ template: {
180
+ provider: { display_name: 'Provider', type: 'options',
181
+ options: ['VIDraft', 'OpenAI'], value: 'VIDraft' },
182
+ model: { display_name: 'Model', type: 'string',
183
+ value: 'Gemma-3-r1984-27B' },
184
+ temperature: { display_name: 'Temperature', type: 'number',
185
+ value: 0.7, min: 0, max: 2, step: 0.1 },
186
+ system_prompt:{ display_name: 'System Prompt', type: 'string',
187
+ value: 'You are a helpful assistant.' },
188
+ user_prompt: { display_name: 'User Prompt', type: 'string',
189
+ value: '', is_handle: true }, // ⬅ 입력 핸들
190
+ response: { display_name: 'Response', type: 'string',
191
+ value: '', is_handle: true } // ⬅ 출력 핸들
192
+ }
193
+ }
194
+ },
195
+ textNode: { // ② 새 노드
196
+ label: 'Markdown',
197
+ icon: '📝',
198
+ color: '#4b5563',
199
+ defaultData: {
200
+ display_name: 'Markdown',
201
+ template: {
202
+ text: { display_name: 'Markdown', type: 'string',
203
+ value: '### Write any markdown here', is_handle: true }
204
+ }
205
+ }
206
+ },
207
+
208
+
209
+
210
+ OpenAIModel: {
211
+ label: 'OpenAI Model',
212
+ icon: '🎯',
213
+ color: '#9C27B0',
214
+ defaultData: {
215
+ display_name: 'OpenAI Model',
216
+ template: {
217
+ model: {
218
+ display_name: 'Model',
219
+ type: 'options',
220
+ value: 'gpt-4',
221
+ options: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo']
222
+ },
223
+ temperature: {
224
+ display_name: 'Temperature',
225
+ type: 'number',
226
+ value: 0.7,
227
+ min: 0,
228
+ max: 1
229
+ },
230
+ max_tokens: {
231
+ display_name: 'Max Tokens',
232
+ type: 'number',
233
+ value: 2048,
234
+ min: 1,
235
+ max: 4096
236
+ },
237
+ api_key: {
238
+ display_name: 'API Key',
239
+ type: 'SecretStr',
240
+ value: '',
241
+ env_var: 'OPENAI_API_KEY'
242
+ },
243
+ prompt: {
244
+ display_name: 'Prompt',
245
+ type: 'string',
246
+ is_handle: true
247
+ },
248
+ response: {
249
+ display_name: 'Response',
250
+ type: 'string',
251
+ is_handle: true
252
+ }
253
+ },
254
+ resources: {
255
+ cpu: 0.5,
256
+ memory: '512Mi',
257
+ gpu: 'none'
258
+ }
259
+ }
260
+ },
261
+ ChatModel: {
262
+ label: 'Chat Model',
263
+ icon: '💭',
264
+ color: '#673AB7',
265
+ defaultData: {
266
+ display_name: 'Chat Model',
267
+ template: {
268
+ provider: {
269
+ display_name: 'Provider',
270
+ type: 'options',
271
+ options: ['OpenAI', 'Anthropic'],
272
+ value: 'OpenAI'
273
+ },
274
+ model: {
275
+ display_name: 'Model',
276
+ type: 'string',
277
+ value: 'gpt-4o-mini'
278
+ },
279
+ api_key: {
280
+ display_name: 'API Key',
281
+ type: 'SecretStr',
282
+ required: true,
283
+ env_var: 'OPENAI_API_KEY'
284
+ },
285
+ system_prompt: {
286
+ display_name: 'System Prompt',
287
+ type: 'string',
288
+ value: 'You are a helpful assistant.'
289
+ },
290
+ prompt: {
291
+ display_name: 'Prompt',
292
+ type: 'string',
293
+ is_handle: true
294
+ },
295
+ response: {
296
+ display_name: 'Response',
297
+ type: 'string',
298
+ is_handle: true
299
+ }
300
+ },
301
+ resources: {
302
+ cpu: 0.5,
303
+ memory: '512Mi',
304
+ gpu: 'none'
305
+ }
306
+ }
307
+ },
308
+ Prompt: {
309
+ label: 'Prompt',
310
+ icon: '📝',
311
+ color: '#3F51B5',
312
+ defaultData: {
313
+ display_name: 'Prompt',
314
+ template: {
315
+ prompt_template: {
316
+ display_name: 'Template',
317
+ type: 'string',
318
+ value: '{{input}}',
319
+ is_handle: true
320
+ }
321
+ },
322
+ resources: {
323
+ cpu: 0.1,
324
+ memory: '128Mi',
325
+ gpu: 'none'
326
+ }
327
+ }
328
+ },
329
+ HFTextGeneration: {
330
+ label: 'HF Text Generation',
331
+ icon: '🤗',
332
+ color: '#E91E63',
333
+ defaultData: {
334
+ display_name: 'HF Text Generation',
335
+ template: {
336
+ model: {
337
+ display_name: 'Model',
338
+ type: 'string',
339
+ value: 'gpt2'
340
+ },
341
+ temperature: {
342
+ display_name: 'Temperature',
343
+ type: 'number',
344
+ value: 0.7,
345
+ min: 0,
346
+ max: 1
347
+ },
348
+ max_tokens: {
349
+ display_name: 'Max Tokens',
350
+ type: 'number',
351
+ value: 2048,
352
+ min: 1,
353
+ max: 4096
354
+ },
355
+ api_key: {
356
+ display_name: 'API Key',
357
+ type: 'SecretStr',
358
+ value: '',
359
+ env_var: 'HF_API_KEY'
360
+ },
361
+ prompt: {
362
+ display_name: 'Prompt',
363
+ type: 'string',
364
+ is_handle: true
365
+ },
366
+ response: {
367
+ display_name: 'Response',
368
+ type: 'string',
369
+ is_handle: true
370
+ }
371
+ },
372
+ resources: {
373
+ cpu: 0.3,
374
+ memory: '256Mi',
375
+ gpu: 'none'
376
+ }
377
+ }
378
+ },
379
+ ReActAgent: {
380
+ label: 'ReAct Agent',
381
+ icon: '🤖',
382
+ color: '#9C27B0',
383
+ defaultData: {
384
+ display_name: 'LlamaIndex ReAct Agent',
385
+ template: {
386
+ tools_input: {
387
+ display_name: 'Available Tools',
388
+ type: 'list',
389
+ is_handle: true,
390
+ info: 'Connect WebSearch, ExecutePython, APIRequest, and other tool nodes'
391
+ },
392
+ llm_model: {
393
+ display_name: 'LLM Model',
394
+ type: 'options',
395
+ options: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo', 'gpt-4', 'gpt-3.5-turbo-16k'],
396
+ value: 'gpt-4o-mini'
397
+ },
398
+ api_key: {
399
+ display_name: 'OpenAI API Key',
400
+ type: 'SecretStr',
401
+ required: true,
402
+ env_var: 'OPENAI_API_KEY'
403
+ },
404
+ system_prompt: {
405
+ display_name: 'System Prompt',
406
+ type: 'string',
407
+ value: 'You are a helpful AI assistant with access to various tools. Use the available tools to answer user questions accurately and efficiently.',
408
+ multiline: true
409
+ },
410
+ user_query: {
411
+ display_name: 'User Query',
412
+ type: 'string',
413
+ is_handle: true
414
+ },
415
+ max_iterations: {
416
+ display_name: 'Max Iterations',
417
+ type: 'number',
418
+ value: 8
419
+ },
420
+ temperature: {
421
+ display_name: 'Temperature',
422
+ type: 'number',
423
+ value: 0.1,
424
+ min: 0,
425
+ max: 2,
426
+ step: 0.1
427
+ },
428
+ verbose: {
429
+ display_name: 'Verbose Output',
430
+ type: 'boolean',
431
+ value: true
432
+ },
433
+ agent_response: {
434
+ display_name: 'Agent Response',
435
+ type: 'string',
436
+ is_handle: true
437
+ }
438
+ },
439
+ resources: {
440
+ cpu: 0.5,
441
+ memory: '512Mi',
442
+ gpu: 'none'
443
+ }
444
+ }
445
+ }
446
+ }
447
+ },
448
+ 'API & Web': {
449
+ icon: '🌐',
450
+ components: {
451
+ APIRequest: {
452
+ label: 'API Request',
453
+ icon: '🔌',
454
+ color: '#00BCD4',
455
+ defaultData: {
456
+ display_name: 'API Request',
457
+ template: {
458
+ url: {
459
+ display_name: 'URL',
460
+ type: 'string',
461
+ value: ''
462
+ },
463
+ method: {
464
+ display_name: 'Method',
465
+ type: 'options',
466
+ value: 'GET',
467
+ options: ['GET', 'POST', 'PUT', 'DELETE']
468
+ },
469
+ headers: {
470
+ display_name: 'Headers',
471
+ type: 'dict',
472
+ value: {}
473
+ },
474
+ body: {
475
+ display_name: 'Body',
476
+ type: 'string',
477
+ value: ''
478
+ },
479
+ response: {
480
+ display_name: 'Response',
481
+ type: 'object',
482
+ is_handle: true
483
+ }
484
+ },
485
+ resources: {
486
+ cpu: 0.2,
487
+ memory: '256Mi',
488
+ gpu: 'none'
489
+ }
490
+ }
491
+ },
492
+ WebSearch: {
493
+ label: 'Web Search',
494
+ icon: '🔍',
495
+ color: '#009688',
496
+ defaultData: {
497
+ display_name: 'Web Search',
498
+ template: {
499
+ query: {
500
+ display_name: 'Query',
501
+ type: 'string',
502
+ value: '',
503
+ is_handle: true
504
+ },
505
+ num_results: {
506
+ display_name: 'Number of Results',
507
+ type: 'number',
508
+ value: 5,
509
+ min: 1,
510
+ max: 10
511
+ },
512
+ api_key: {
513
+ display_name: 'API Key',
514
+ type: 'SecretStr',
515
+ value: '',
516
+ env_var: 'SERPAPI_KEY'
517
+ },
518
+ results: {
519
+ display_name: 'Search Results',
520
+ type: 'list',
521
+ is_handle: true
522
+ }
523
+ },
524
+ resources: {
525
+ cpu: 0.2,
526
+ memory: '256Mi',
527
+ gpu: 'none'
528
+ }
529
+ }
530
+ }
531
+ }
532
+ },
533
+ 'Data Processing': {
534
+ icon: '⚙️',
535
+ components: {
536
+ ExecutePython: {
537
+ label: 'Execute Python',
538
+ icon: '🐍',
539
+ color: '#FF5722',
540
+ defaultData: {
541
+ display_name: 'Execute Python',
542
+ template: {
543
+ code: {
544
+ display_name: 'Python Code',
545
+ type: 'string',
546
+ value: 'def process(input_data):\n return input_data'
547
+ },
548
+ timeout: {
549
+ display_name: 'Timeout',
550
+ type: 'number',
551
+ value: 30,
552
+ min: 1,
553
+ max: 300
554
+ },
555
+ input_data: {
556
+ display_name: 'Input Data',
557
+ type: 'object',
558
+ is_handle: true
559
+ },
560
+ output_data: {
561
+ display_name: 'Output Data',
562
+ type: 'object',
563
+ is_handle: true
564
+ }
565
+ },
566
+ resources: {
567
+ cpu: 0.3,
568
+ memory: '256Mi',
569
+ gpu: 'none'
570
+ }
571
+ }
572
+ },
573
+ ConditionalLogic: {
574
+ label: 'Conditional Logic',
575
+ icon: '🔀',
576
+ color: '#795548',
577
+ defaultData: {
578
+ display_name: 'Conditional Logic',
579
+ template: {
580
+ condition: {
581
+ display_name: 'Condition',
582
+ type: 'string',
583
+ value: '{{input}} == True'
584
+ },
585
+ input: {
586
+ display_name: 'Input',
587
+ type: 'object',
588
+ is_handle: true
589
+ },
590
+ true_output: {
591
+ display_name: 'True Output',
592
+ type: 'object',
593
+ is_handle: true
594
+ },
595
+ false_output: {
596
+ display_name: 'False Output',
597
+ type: 'object',
598
+ is_handle: true
599
+ }
600
+ },
601
+ resources: {
602
+ cpu: 0.1,
603
+ memory: '128Mi',
604
+ gpu: 'none'
605
+ }
606
+ }
607
+ },
608
+ Wait: {
609
+ label: 'Wait',
610
+ icon: '⏳',
611
+ color: '#607D8B',
612
+ defaultData: {
613
+ display_name: 'Wait',
614
+ template: {
615
+ seconds: {
616
+ display_name: 'Seconds',
617
+ type: 'number',
618
+ value: 1,
619
+ min: 1,
620
+ max: 3600
621
+ },
622
+ input: {
623
+ display_name: 'Input',
624
+ type: 'object',
625
+ is_handle: true
626
+ },
627
+ output: {
628
+ display_name: 'Output',
629
+ type: 'object',
630
+ is_handle: true
631
+ }
632
+ },
633
+ resources: {
634
+ cpu: 0.1,
635
+ memory: '128Mi',
636
+ gpu: 'none'
637
+ }
638
+ }
639
+ }
640
+ }
641
+ },
642
+ 'RAG & Knowledge': {
643
+ icon: '📚',
644
+ components: {
645
+ KnowledgeBase: {
646
+ label: 'Knowledge Base',
647
+ icon: '📖',
648
+ color: '#8BC34A',
649
+ defaultData: {
650
+ display_name: 'Knowledge Base',
651
+ template: {
652
+ kb_name: {
653
+ display_name: 'Knowledge Base Name',
654
+ type: 'string',
655
+ value: ''
656
+ },
657
+ source_type: {
658
+ display_name: 'Source Type',
659
+ type: 'options',
660
+ options: ['Directory', 'URL'],
661
+ value: 'Directory'
662
+ },
663
+ path_or_url: {
664
+ display_name: 'Path or URL',
665
+ type: 'string',
666
+ value: ''
667
+ },
668
+ knowledge_base: {
669
+ display_name: 'Knowledge Base',
670
+ type: 'object',
671
+ is_handle: true
672
+ }
673
+ },
674
+ resources: {
675
+ cpu: 0.2,
676
+ memory: '512Mi',
677
+ gpu: 'none'
678
+ }
679
+ }
680
+ },
681
+ RAGQuery: {
682
+ label: 'RAG Query',
683
+ icon: '🔎',
684
+ color: '#FFC107',
685
+ defaultData: {
686
+ display_name: 'RAG Query',
687
+ template: {
688
+ query: {
689
+ display_name: 'Query',
690
+ type: 'string',
691
+ is_handle: true
692
+ },
693
+ knowledge_base: {
694
+ display_name: 'Knowledge Base',
695
+ type: 'object',
696
+ is_handle: true
697
+ },
698
+ num_results: {
699
+ display_name: 'Number of Results',
700
+ type: 'number',
701
+ value: 3,
702
+ min: 1,
703
+ max: 10
704
+ },
705
+ rag_prompt: {
706
+ display_name: 'RAG Prompt',
707
+ type: 'string',
708
+ is_handle: true
709
+ }
710
+ },
711
+ resources: {
712
+ cpu: 0.3,
713
+ memory: '512Mi',
714
+ gpu: 'none'
715
+ }
716
+ }
717
+ }
718
+ }
719
+ },
720
+ 'Speech & Vision': {
721
+ icon: '👁️',
722
+ components: {
723
+ HFSpeechToText: {
724
+ label: 'HF Speech to Text',
725
+ icon: '🎤',
726
+ color: '#9E9E9E',
727
+ defaultData: {
728
+ display_name: 'HF Speech to Text',
729
+ template: {
730
+ model: {
731
+ display_name: 'Model',
732
+ type: 'string',
733
+ value: 'facebook/wav2vec2-base-960h'
734
+ },
735
+ api_key: {
736
+ display_name: 'API Key',
737
+ type: 'SecretStr',
738
+ value: '',
739
+ env_var: 'HF_API_KEY'
740
+ },
741
+ audio_input: {
742
+ display_name: 'Audio Input',
743
+ type: 'file',
744
+ is_handle: true
745
+ },
746
+ text_output: {
747
+ display_name: 'Text Output',
748
+ type: 'string',
749
+ is_handle: true
750
+ }
751
+ },
752
+ resources: {
753
+ cpu: 0.4,
754
+ memory: '512Mi',
755
+ gpu: 'optional'
756
+ }
757
+ }
758
+ },
759
+ HFTextToSpeech: {
760
+ label: 'HF Text to Speech',
761
+ icon: '🔊',
762
+ color: '#CDDC39',
763
+ defaultData: {
764
+ display_name: 'HF Text to Speech',
765
+ template: {
766
+ model: {
767
+ display_name: 'Model',
768
+ type: 'string',
769
+ value: 'facebook/fastspeech2-en-ljspeech'
770
+ },
771
+ api_key: {
772
+ display_name: 'API Key',
773
+ type: 'SecretStr',
774
+ value: '',
775
+ env_var: 'HF_API_KEY'
776
+ },
777
+ text_input: {
778
+ display_name: 'Text Input',
779
+ type: 'string',
780
+ is_handle: true
781
+ },
782
+ audio_output: {
783
+ display_name: 'Audio Output',
784
+ type: 'file',
785
+ is_handle: true
786
+ }
787
+ },
788
+ resources: {
789
+ cpu: 0.4,
790
+ memory: '512Mi',
791
+ gpu: 'optional'
792
+ }
793
+ }
794
+ },
795
+ HFSVisionModel: {
796
+ label: 'HF Vision Model',
797
+ icon: '👁️',
798
+ color: '#FF9800',
799
+ defaultData: {
800
+ display_name: 'HF Vision Model',
801
+ template: {
802
+ model: {
803
+ display_name: 'Model',
804
+ type: 'string',
805
+ value: 'google/vit-base-patch16-224'
806
+ },
807
+ api_key: {
808
+ display_name: 'API Key',
809
+ type: 'SecretStr',
810
+ value: '',
811
+ env_var: 'HF_API_KEY'
812
+ },
813
+ image_input: {
814
+ display_name: 'Image Input',
815
+ type: 'file',
816
+ is_handle: true
817
+ },
818
+ prediction: {
819
+ display_name: 'Prediction',
820
+ type: 'object',
821
+ is_handle: true
822
+ }
823
+ },
824
+ resources: {
825
+ cpu: 0.4,
826
+ memory: '512Mi',
827
+ gpu: 'required'
828
+ }
829
+ }
830
+ }
831
+ }
832
+ },
833
+ 'Image Generation': {
834
+ icon: '🎨',
835
+ components: {
836
+ HFImageGeneration: {
837
+ label: 'HF Image Generation',
838
+ icon: '🎨',
839
+ color: '#E91E63',
840
+ defaultData: {
841
+ display_name: 'HF Image Generation',
842
+ template: {
843
+ model: {
844
+ display_name: 'Model',
845
+ type: 'string',
846
+ value: 'stabilityai/stable-diffusion-2'
847
+ },
848
+ prompt: {
849
+ display_name: 'Prompt',
850
+ type: 'string',
851
+ value: '',
852
+ is_handle: true
853
+ },
854
+ num_images: {
855
+ display_name: 'Number of Images',
856
+ type: 'number',
857
+ value: 1,
858
+ min: 1,
859
+ max: 4
860
+ },
861
+ api_key: {
862
+ display_name: 'API Key',
863
+ type: 'SecretStr',
864
+ value: '',
865
+ env_var: 'HF_API_KEY'
866
+ },
867
+ images: {
868
+ display_name: 'Generated Images',
869
+ type: 'list',
870
+ is_handle: true
871
+ }
872
+ },
873
+ resources: {
874
+ cpu: 0.5,
875
+ memory: '1Gi',
876
+ gpu: 'required'
877
+ }
878
+ }
879
+ },
880
+ NebiusImage: {
881
+ label: 'Nebius Image',
882
+ icon: '🖼️',
883
+ color: '#2196F3',
884
+ defaultData: {
885
+ display_name: 'Nebius Image',
886
+ template: {
887
+ model: {
888
+ display_name: 'Model',
889
+ type: 'options',
890
+ options: ['black-forest-labs/flux-dev', 'black-forest-labs/flux-schnell', 'stability-ai/sdxl'],
891
+ value: 'black-forest-labs/flux-dev'
892
+ },
893
+ prompt: {
894
+ display_name: 'Prompt',
895
+ type: 'string',
896
+ value: '',
897
+ is_handle: true
898
+ },
899
+ negative_prompt: {
900
+ display_name: 'Negative Prompt',
901
+ type: 'string',
902
+ value: ''
903
+ },
904
+ width: {
905
+ display_name: 'Width',
906
+ type: 'number',
907
+ value: 1024
908
+ },
909
+ height: {
910
+ display_name: 'Height',
911
+ type: 'number',
912
+ value: 1024
913
+ },
914
+ num_inference_steps: {
915
+ display_name: 'Inference Steps',
916
+ type: 'number',
917
+ value: 28
918
+ },
919
+ seed: {
920
+ display_name: 'Seed',
921
+ type: 'number',
922
+ value: -1
923
+ },
924
+ api_key: {
925
+ display_name: 'API Key',
926
+ type: 'SecretStr',
927
+ value: '',
928
+ env_var: 'NEBIUS_API_KEY'
929
+ },
930
+ image: {
931
+ display_name: 'Generated Image',
932
+ type: 'file',
933
+ is_handle: true
934
+ }
935
+ },
936
+ resources: {
937
+ cpu: 0.5,
938
+ memory: '1Gi',
939
+ gpu: 'required'
940
+ }
941
+ }
942
+ }
943
+ }
944
+ },
945
+ 'MCP Integration': {
946
+ icon: '🤝',
947
+ components: {
948
+ MCPConnection: {
949
+ label: 'MCP Connection',
950
+ icon: '🔌',
951
+ color: '#673AB7',
952
+ defaultData: {
953
+ display_name: 'MCP Connection',
954
+ template: {
955
+ server_url: {
956
+ display_name: 'Server URL',
957
+ type: 'string',
958
+ value: ''
959
+ },
960
+ connection_type: {
961
+ display_name: 'Connection Type',
962
+ type: 'options',
963
+ options: ['http', 'stdio'],
964
+ value: 'http'
965
+ },
966
+ allowed_tools: {
967
+ display_name: 'Allowed Tools',
968
+ type: 'string',
969
+ value: ''
970
+ },
971
+ api_key: {
972
+ display_name: 'API Key',
973
+ type: 'SecretStr',
974
+ value: '',
975
+ env_var: 'MCP_API_KEY'
976
+ },
977
+ connection: {
978
+ display_name: 'MCP Connection',
979
+ type: 'object',
980
+ is_handle: true
981
+ }
982
+ },
983
+ resources: {
984
+ cpu: 0.2,
985
+ memory: '256Mi',
986
+ gpu: 'none'
987
+ }
988
+ }
989
+ },
990
+ MCPAgent: {
991
+ label: 'MCP Agent',
992
+ icon: '🤖',
993
+ color: '#3F51B5',
994
+ defaultData: {
995
+ display_name: 'MCP Agent',
996
+ template: {
997
+ llm_model: {
998
+ display_name: 'LLM Model',
999
+ type: 'options',
1000
+ options: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo', 'gpt-4', 'gpt-3.5-turbo-16k'],
1001
+ value: 'gpt-4o'
1002
+ },
1003
+ api_key: {
1004
+ display_name: 'OpenAI API Key',
1005
+ type: 'SecretStr',
1006
+ required: true,
1007
+ env_var: 'OPENAI_API_KEY'
1008
+ },
1009
+ system_prompt: {
1010
+ display_name: 'System Prompt',
1011
+ type: 'string',
1012
+ value: 'You are a helpful AI assistant.',
1013
+ multiline: true
1014
+ },
1015
+ max_iterations: {
1016
+ display_name: 'Max Iterations',
1017
+ type: 'number',
1018
+ value: 10,
1019
+ min: 1,
1020
+ max: 20
1021
+ },
1022
+ temperature: {
1023
+ display_name: 'Temperature',
1024
+ type: 'number',
1025
+ value: 0.1,
1026
+ min: 0,
1027
+ max: 2,
1028
+ step: 0.1
1029
+ },
1030
+ verbose: {
1031
+ display_name: 'Verbose Output',
1032
+ type: 'boolean',
1033
+ value: false
1034
+ },
1035
+ user_query: {
1036
+ display_name: 'User Query',
1037
+ type: 'string',
1038
+ is_handle: true
1039
+ },
1040
+ mcp_connection: {
1041
+ display_name: 'MCP Connection',
1042
+ type: 'object',
1043
+ is_handle: true
1044
+ },
1045
+ agent_response: {
1046
+ display_name: 'Agent Response',
1047
+ type: 'string',
1048
+ is_handle: true
1049
+ }
1050
+ },
1051
+ resources: {
1052
+ cpu: 0.5,
1053
+ memory: '512Mi',
1054
+ gpu: 'none'
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ };
1061
+
1062
+ // Property fields for each node type
1063
+ const propertyFields = {
1064
+
1065
+ llmNode: [ // ③ AI Processing 속성 폼
1066
+ { key: 'display_name', label: 'Display Name', type: 'text' },
1067
+ { key: 'template.provider.value', label: 'Provider', type: 'select',
1068
+ options: ['VIDraft', 'OpenAI'] },
1069
+ { key: 'template.model.value', label: 'Model', type: 'text' },
1070
+ { key: 'template.temperature.value', label: 'Temperature', type: 'number',
1071
+ min: 0, max: 2, step: 0.1 },
1072
+ { key: 'template.system_prompt.value', label: 'System Prompt',
1073
+ type: 'textarea' }
1074
+ ],
1075
+
1076
+ textNode: [ // ④ Markdown 노드 속성 폼
1077
+ { key: 'display_name', label: 'Display Name', type: 'text' },
1078
+ { key: 'template.text.value', label: 'Markdown Text', type: 'textarea' }
1079
+ ],
1080
+
1081
+
1082
+
1083
+
1084
+ // Input/Output nodes
1085
+ ChatInput: [
1086
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1087
+ { key: 'template.input_value.display_name', label: 'Input Field Label', type: 'text', help: 'Label shown in the chat input field' },
1088
+ { key: 'template.input_value.value', label: 'Default Message', type: 'textarea', help: 'Default message shown in the input field' }
1089
+ ],
1090
+ ChatOutput: [
1091
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1092
+ { key: 'template.response.display_name', label: 'Response Field Label', type: 'text', help: 'Label shown in the chat output field' }
1093
+ ],
1094
+ Input: [
1095
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1096
+ { key: 'template.data_type.value', label: 'Data Type', type: 'select', options: ['string', 'image', 'video', 'audio', 'file'], help: 'Type of data this node will handle' },
1097
+ { key: 'template.value.value', label: 'Default Value', type: 'textarea', help: 'Default value or path' }
1098
+ ],
1099
+ Output: [
1100
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' }
1101
+ ],
1102
+
1103
+ // AI & Language nodes
1104
+ OpenAIModel: [
1105
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1106
+ { key: 'template.model.value', label: 'Model', type: 'select', options: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'] },
1107
+ { key: 'template.temperature.value', label: 'Temperature', type: 'number', min: 0, max: 1, step: 0.1 },
1108
+ { key: 'template.max_tokens.value', label: 'Max Tokens', type: 'number', min: 1, max: 4096 }
1109
+ ],
1110
+ ChatModel: [
1111
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1112
+ { key: 'template.provider.value', label: 'Provider', type: 'select', options: ['OpenAI', 'Anthropic'], help: 'AI model provider' },
1113
+ { key: 'template.model.value', label: 'Model', type: 'text', help: 'Model name' },
1114
+ { key: 'template.system_prompt.value', label: 'System Prompt', type: 'textarea', help: 'Optional system prompt' }
1115
+ ],
1116
+ Prompt: [
1117
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1118
+ { key: 'template.prompt_template.value', label: 'Prompt Template', type: 'textarea', help: 'Prompt template' }
1119
+ ],
1120
+ HFTextGeneration: [
1121
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1122
+ { key: 'template.model.value', label: 'Model', type: 'text', help: 'Model name' },
1123
+ { key: 'template.temperature.value', label: 'Temperature', type: 'number', min: 0, max: 1, step: 0.1, help: 'Model temperature' },
1124
+ { key: 'template.max_tokens.value', label: 'Max Tokens', type: 'number', min: 1, max: 4096, help: 'Maximum tokens' }
1125
+ ],
1126
+ ReActAgent: [
1127
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1128
+ { key: 'template.llm_model.value', label: 'LLM Model', type: 'select', options: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo', 'gpt-4', 'gpt-3.5-turbo-16k'], help: 'Model to use for the agent' },
1129
+ { key: 'template.system_prompt.value', label: 'System Prompt', type: 'textarea', help: 'System prompt for the agent', multiline: true },
1130
+ { key: 'template.max_iterations.value', label: 'Max Iterations', type: 'number', min: 1, max: 20, help: 'Maximum number of agent iterations' },
1131
+ { key: 'template.temperature.value', label: 'Temperature', type: 'number', min: 0, max: 2, step: 0.1, help: 'Model temperature (0-2)' },
1132
+ { key: 'template.verbose.value', label: 'Verbose Output', type: 'checkbox', help: 'Show detailed agent reasoning' }
1133
+ ],
1134
+
1135
+ // API & Web nodes
1136
+ APIRequest: [
1137
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1138
+ { key: 'template.url.value', label: 'URL', type: 'text', help: 'API endpoint URL' },
1139
+ { key: 'template.method.value', label: 'Method', type: 'select', options: ['GET', 'POST', 'PUT', 'DELETE'], help: 'HTTP method' }
1140
+ ],
1141
+ WebSearch: [
1142
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1143
+ { key: 'template.num_results.value', label: 'Number of Results', type: 'number', help: 'Number of search results' }
1144
+ ],
1145
+
1146
+ // Data Processing nodes
1147
+ ExecutePython: [
1148
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1149
+ { key: 'template.code.value', label: 'Python Code', type: 'textarea', help: 'Python code to execute' },
1150
+ { key: 'template.timeout.value', label: 'Timeout', type: 'number', help: 'Execution timeout' }
1151
+ ],
1152
+ ConditionalLogic: [
1153
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1154
+ { key: 'template.condition.value', label: 'Condition', type: 'text', help: 'Condition expression' }
1155
+ ],
1156
+ Wait: [
1157
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1158
+ { key: 'template.seconds.value', label: 'Seconds', type: 'number', help: 'Wait time in seconds' }
1159
+ ],
1160
+
1161
+ // RAG nodes
1162
+ KnowledgeBase: [
1163
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1164
+ { key: 'template.kb_name.value', label: 'Knowledge Base Name', type: 'text', help: 'Name for the knowledge base' },
1165
+ { key: 'template.source_type.value', label: 'Source Type', type: 'select', options: ['Directory', 'URL'], help: 'Type of source' },
1166
+ { key: 'template.path_or_url.value', label: 'Path or URL', type: 'text', help: 'Source location' }
1167
+ ],
1168
+ RAGQuery: [
1169
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1170
+ { key: 'template.num_results.value', label: 'Number of Results', type: 'number', help: 'Number of results to retrieve' }
1171
+ ],
1172
+
1173
+ // Speech & Vision nodes
1174
+ HFSpeechToText: [
1175
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1176
+ { key: 'template.model.value', label: 'Model', type: 'text', help: 'HuggingFace model ID' }
1177
+ ],
1178
+ HFTextToSpeech: [
1179
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1180
+ { key: 'template.model.value', label: 'Model', type: 'text', help: 'HuggingFace model ID' }
1181
+ ],
1182
+ HFSVisionModel: [
1183
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1184
+ { key: 'template.model.value', label: 'Model', type: 'text', help: 'HuggingFace model ID' }
1185
+ ],
1186
+
1187
+ // Image Generation nodes
1188
+ HFImageGeneration: [
1189
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1190
+ { key: 'template.model.value', label: 'Model', type: 'text', help: 'HuggingFace model ID' },
1191
+ { key: 'template.num_images.value', label: 'Number of Images', type: 'number', help: 'Number of images to generate' }
1192
+ ],
1193
+ NebiusImage: [
1194
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1195
+ { key: 'template.model.value', label: 'Model', type: 'select', options: ['black-forest-labs/flux-dev', 'black-forest-labs/flux-schnell', 'stability-ai/sdxl'], help: 'Nebius model to use' },
1196
+ { key: 'template.width.value', label: 'Width', type: 'number', help: 'Image width' },
1197
+ { key: 'template.height.value', label: 'Height', type: 'number', help: 'Image height' },
1198
+ { key: 'template.num_inference_steps.value', label: 'Inference Steps', type: 'number', help: 'Number of inference steps' },
1199
+ { key: 'template.seed.value', label: 'Seed', type: 'number', help: 'Random seed (-1 for random)' }
1200
+ ],
1201
+
1202
+ // MCP nodes
1203
+ MCPConnection: [
1204
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1205
+ { key: 'template.server_url.value', label: 'Server URL', type: 'text', help: 'MCP server URL' },
1206
+ { key: 'template.connection_type.value', label: 'Connection Type', type: 'select', options: ['http', 'stdio'], help: 'Connection type' },
1207
+ { key: 'template.allowed_tools.value', label: 'Allowed Tools', type: 'text', help: 'Optional list of allowed tools' }
1208
+ ],
1209
+ MCPAgent: [
1210
+ { key: 'display_name', label: 'Display Name', type: 'text', help: 'Name shown in the workflow' },
1211
+ { key: 'template.llm_model.value', label: 'LLM Model', type: 'select', options: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo', 'gpt-4', 'gpt-3.5-turbo-16k'], help: 'Model to use for the agent' },
1212
+ { key: 'template.system_prompt.value', label: 'System Prompt', type: 'textarea', help: 'System prompt for the agent', multiline: true },
1213
+ { key: 'template.max_iterations.value', label: 'Max Iterations', type: 'number', min: 1, max: 20, help: 'Maximum number of agent iterations' },
1214
+ { key: 'template.temperature.value', label: 'Temperature', type: 'number', min: 0, max: 2, step: 0.1, help: 'Model temperature (0-2)' },
1215
+ { key: 'template.verbose.value', label: 'Verbose Output', type: 'checkbox', help: 'Show detailed agent reasoning' }
1216
+ ]
1217
+ };
1218
+
1219
+ // Update parent component when data changes
1220
+ $: {
1221
+ const newValue = { nodes, edges };
1222
+ if (JSON.stringify(newValue) !== JSON.stringify(value)) {
1223
+ value = newValue;
1224
+ dispatch('change', newValue);
1225
+ }
1226
+ }
1227
+
1228
+ // Export workflow to JSON
1229
+
1230
+ // Clear workflow function
1231
+ function clearWorkflow() {
1232
+ nodes = [];
1233
+ edges = [];
1234
+ selectedNode = null;
1235
+ workflowName = "My Workflow";
1236
+ workflowId = "workflow-" + Date.now();
1237
+ }
1238
+
1239
+
1240
+ function exportWorkflow() {
1241
+ const exportData = {
1242
+ workflow_id: workflowId,
1243
+ workflow_name: workflowName,
1244
+ nodes: nodes.map(node => ({
1245
+ id: node.id,
1246
+ type: node.type,
1247
+ data: {
1248
+ display_name: node.data.display_name,
1249
+ template: node.data.template,
1250
+ resources: node.data.resources || {
1251
+ cpu: 0.1,
1252
+ memory: "128Mi",
1253
+ gpu: "none"
1254
+ }
1255
+ }
1256
+ })),
1257
+ edges: edges.map(edge => ({
1258
+ source: edge.source,
1259
+ source_handle: edge.source_handle || 'output',
1260
+ target: edge.target,
1261
+ target_handle: edge.target_handle || 'input'
1262
+ }))
1263
+ };
1264
+
1265
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
1266
+ const url = URL.createObjectURL(blob);
1267
+ const a = document.createElement('a');
1268
+ a.href = url;
1269
+ a.download = `${workflowName.replace(/\s+/g, '-').toLowerCase()}.json`;
1270
+ document.body.appendChild(a);
1271
+ a.click();
1272
+ document.body.removeChild(a);
1273
+ URL.revokeObjectURL(url);
1274
+ }
1275
+
1276
+ // Zoom functions
1277
+ function zoomIn() {
1278
+ zoomLevel = Math.min(zoomLevel * 1.2, 3);
1279
+ }
1280
+
1281
+ function zoomOut() {
1282
+ zoomLevel = Math.max(zoomLevel / 1.2, 0.3);
1283
+ }
1284
+
1285
+ function resetZoom() {
1286
+ zoomLevel = 1;
1287
+ panOffset = { x: 0, y: 0 };
1288
+ }
1289
+
1290
+ function handleWheel(event: WheelEvent) {
1291
+ event.preventDefault();
1292
+ if (event.ctrlKey || event.metaKey) {
1293
+ const delta = event.deltaY > 0 ? 0.9 : 1.1;
1294
+ zoomLevel = Math.max(0.3, Math.min(3, zoomLevel * delta));
1295
+ } else {
1296
+ panOffset.x -= event.deltaX * 0.5;
1297
+ panOffset.y -= event.deltaY * 0.5;
1298
+ panOffset = { ...panOffset };
1299
+ }
1300
+ }
1301
+
1302
+ // Pan functions
1303
+ function startPanning(event: MouseEvent) {
1304
+ if (event.button === 1 || (event.button === 0 && event.altKey)) {
1305
+ isPanning = true;
1306
+ lastPanPoint = { x: event.clientX, y: event.clientY };
1307
+ event.preventDefault();
1308
+ }
1309
+ }
1310
+
1311
+ function handlePanning(event: MouseEvent) {
1312
+ if (isPanning) {
1313
+ const deltaX = event.clientX - lastPanPoint.x;
1314
+ const deltaY = event.clientY - lastPanPoint.y;
1315
+ panOffset.x += deltaX;
1316
+ panOffset.y += deltaY;
1317
+ panOffset = { ...panOffset };
1318
+ lastPanPoint = { x: event.clientX, y: event.clientY };
1319
+ }
1320
+ }
1321
+
1322
+ function stopPanning() {
1323
+ isPanning = false;
1324
+ }
1325
+
1326
+ // Drag and drop from sidebar
1327
+ function handleSidebarDragStart(event: DragEvent, componentType: string, componentData: any) {
1328
+ if (event.dataTransfer) {
1329
+ event.dataTransfer.setData('application/json', JSON.stringify({
1330
+ type: componentType,
1331
+ data: componentData
1332
+ }));
1333
+ isDraggingFromSidebar = true;
1334
+ }
1335
+ }
1336
+
1337
+ function handleCanvasDropFromSidebar(event: DragEvent) {
1338
+ event.preventDefault();
1339
+ if (!isDraggingFromSidebar) return;
1340
+
1341
+ const rect = canvas.getBoundingClientRect();
1342
+ const x = (event.clientX - rect.left - panOffset.x) / zoomLevel;
1343
+ const y = (event.clientY - rect.top - panOffset.y) / zoomLevel;
1344
+
1345
+ try {
1346
+ const dropData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}');
1347
+ if (dropData.type && dropData.data) {
1348
+ const newNode = {
1349
+ id: `${dropData.type}-${Date.now()}`,
1350
+ type: dropData.type,
1351
+ position: { x: Math.max(20, x - 160), y: Math.max(20, y - 80) },
1352
+ data: { ...dropData.data.defaultData, label: dropData.data.label }
1353
+ };
1354
+ nodes = [...nodes, newNode];
1355
+ }
1356
+ } catch (error) {
1357
+ console.error('Failed to parse drop data:', error);
1358
+ }
1359
+
1360
+ isDraggingFromSidebar = false;
1361
+ }
1362
+
1363
+ function handleCanvasDragOver(event: DragEvent) {
1364
+ event.preventDefault();
1365
+ }
1366
+
1367
+ // Node interaction handlers with proper event handling
1368
+ function handleMouseDown(event: MouseEvent, node: any) {
1369
+ // Only start dragging if clicking on the node header or empty areas
1370
+ if (event.target.closest('.node-property') ||
1371
+ event.target.closest('.property-input') ||
1372
+ event.target.closest('.property-select') ||
1373
+ event.target.closest('.property-checkbox')) {
1374
+ return; // Don't start dragging if clicking on form controls
1375
+ }
1376
+
1377
+ if (event.button !== 0) return;
1378
+
1379
+ isDragging = true;
1380
+ dragNode = node;
1381
+ const rect = canvas.getBoundingClientRect();
1382
+ const nodeScreenX = node.position.x * zoomLevel + panOffset.x;
1383
+ const nodeScreenY = node.position.y * zoomLevel + panOffset.y;
1384
+ dragOffset.x = event.clientX - rect.left - nodeScreenX;
1385
+ dragOffset.y = event.clientY - rect.top - nodeScreenY;
1386
+
1387
+ event.preventDefault();
1388
+ event.stopPropagation();
1389
+ }
1390
+
1391
+ function handleNodeClick(event: MouseEvent, node: any) {
1392
+ event.stopPropagation();
1393
+ selectedNode = { ...node };
1394
+ }
1395
+
1396
+ function handleMouseMove(event: MouseEvent) {
1397
+ const rect = canvas.getBoundingClientRect();
1398
+ mousePos.x = (event.clientX - rect.left - panOffset.x) / zoomLevel;
1399
+ mousePos.y = (event.clientY - rect.top - panOffset.y) / zoomLevel;
1400
+
1401
+ if (isDragging && dragNode) {
1402
+ const nodeIndex = nodes.findIndex(n => n.id === dragNode.id);
1403
+ if (nodeIndex >= 0) {
1404
+ const newX = Math.max(0, (event.clientX - rect.left - dragOffset.x - panOffset.x) / zoomLevel);
1405
+ const newY = Math.max(0, (event.clientY - rect.top - dragOffset.y - panOffset.y) / zoomLevel);
1406
+ nodes[nodeIndex].position.x = newX;
1407
+ nodes[nodeIndex].position.y = newY;
1408
+ nodes = [...nodes];
1409
+
1410
+ if (selectedNode?.id === dragNode.id) {
1411
+ selectedNode = { ...nodes[nodeIndex] };
1412
+ }
1413
+ }
1414
+ }
1415
+
1416
+ handlePanning(event);
1417
+ }
1418
+
1419
+ function handleMouseUp() {
1420
+ isDragging = false;
1421
+ dragNode = null;
1422
+ isConnecting = false;
1423
+ connectionStart = null;
1424
+ stopPanning();
1425
+ }
1426
+
1427
+ // Connection handling
1428
+ function startConnection(event: MouseEvent, nodeId: string) {
1429
+ event.stopPropagation();
1430
+ isConnecting = true;
1431
+ connectionStart = nodeId;
1432
+ }
1433
+
1434
+ function endConnection(event: MouseEvent, nodeId: string) {
1435
+ event.stopPropagation();
1436
+ if (isConnecting && connectionStart && connectionStart !== nodeId) {
1437
+ const existingEdge = edges.find(e =>
1438
+ (e.source === connectionStart && e.target === nodeId) ||
1439
+ (e.source === nodeId && e.target === connectionStart)
1440
+ );
1441
+
1442
+ if (!existingEdge) {
1443
+ const newEdge = {
1444
+ id: `e-${connectionStart}-${nodeId}-${Date.now()}`,
1445
+ source: connectionStart,
1446
+ target: nodeId
1447
+ };
1448
+ edges = [...edges, newEdge];
1449
+ }
1450
+ }
1451
+ isConnecting = false;
1452
+ connectionStart = null;
1453
+ }
1454
+
1455
+ // Node and edge management
1456
+ function deleteNode(nodeId: string) {
1457
+ nodes = nodes.filter(n => n.id !== nodeId);
1458
+ edges = edges.filter(e => e.source !== nodeId && e.target !== nodeId);
1459
+ if (selectedNode?.id === nodeId) {
1460
+ selectedNode = null;
1461
+ }
1462
+ }
1463
+
1464
+ function deleteEdge(edgeId: string) {
1465
+ edges = edges.filter(e => e.id !== edgeId);
1466
+ }
1467
+
1468
+ // Property updates with proper reactivity
1469
+ function updateNodeProperty(nodeId: string, key: string, value: any) {
1470
+ const nodeIndex = nodes.findIndex(n => n.id === nodeId);
1471
+ if (nodeIndex >= 0) {
1472
+ // Handle nested property paths
1473
+ const keyParts = key.split('.');
1474
+ let target = nodes[nodeIndex].data;
1475
+
1476
+ for (let i = 0; i < keyParts.length - 1; i++) {
1477
+ if (!target[keyParts[i]]) {
1478
+ target[keyParts[i]] = {};
1479
+ }
1480
+ target = target[keyParts[i]];
1481
+ }
1482
+
1483
+ target[keyParts[keyParts.length - 1]] = value;
1484
+ nodes = [...nodes]; // Trigger reactivity
1485
+
1486
+ if (selectedNode?.id === nodeId) {
1487
+ selectedNode = { ...nodes[nodeIndex] };
1488
+ }
1489
+ }
1490
+ }
1491
+
1492
+ function getNodeProperty(node: any, key: string) {
1493
+ const keyParts = key.split('.');
1494
+ let value = node.data;
1495
+
1496
+ for (const part of keyParts) {
1497
+ value = value?.[part];
1498
+ }
1499
+
1500
+ return value;
1501
+ }
1502
+
1503
+ // Panel toggle functions
1504
+ function toggleSidebar() {
1505
+ sidebarCollapsed = !sidebarCollapsed;
1506
+ }
1507
+
1508
+ function togglePropertyPanel() {
1509
+ propertyPanelCollapsed = !propertyPanelCollapsed;
1510
+ }
1511
+
1512
+ // Helper functions
1513
+ function getComponentConfig(type: string) {
1514
+ for (const category of Object.values(componentCategories)) {
1515
+ if (category.components[type]) {
1516
+ return category.components[type];
1517
+ }
1518
+ }
1519
+ return { label: type, icon: '⚡', color: '#6b7280' };
1520
+ }
1521
+
1522
+ function getConnectionPoints(sourceNode: any, targetNode: any) {
1523
+ const sourceX = sourceNode.position.x + 320;
1524
+ const sourceY = sourceNode.position.y + 80;
1525
+ const targetX = targetNode.position.x;
1526
+ const targetY = targetNode.position.y + 80;
1527
+
1528
+ return { sourceX, sourceY, targetX, targetY };
1529
+ }
1530
+
1531
+ // Canvas setup
1532
+ onMount(() => {
1533
+ document.addEventListener('mousemove', handleMouseMove);
1534
+ document.addEventListener('mouseup', handleMouseUp);
1535
+
1536
+ return () => {
1537
+ document.removeEventListener('mousemove', handleMouseMove);
1538
+ document.removeEventListener('mouseup', handleMouseUp);
1539
+ };
1540
+ });
1541
+ </script>
1542
+
1543
+ <div
1544
+ class="workflow-builder {elem_classes.join(' ')}"
1545
+ class:hide={!visible}
1546
+ style:min-width={min_width && min_width + "px"}
1547
+ id={elem_id}
1548
+ >
1549
+ <!-- Top Section: Main Workflow Area -->
1550
+ <div class="top-section">
1551
+ <!-- Left Sidebar -->
1552
+ <div class="sidebar" class:collapsed={sidebarCollapsed}>
1553
+ <div class="sidebar-header">
1554
+ {#if !sidebarCollapsed}
1555
+ <h3>Components</h3>
1556
+ {/if}
1557
+ <button
1558
+ class="toggle-btn sidebar-toggle"
1559
+ on:click={toggleSidebar}
1560
+ title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
1561
+ >
1562
+ {sidebarCollapsed ? '→' : '←'}
1563
+ </button>
1564
+ </div>
1565
+
1566
+ {#if !sidebarCollapsed}
1567
+ <div class="sidebar-content">
1568
+ {#each Object.entries(componentCategories) as [categoryName, category]}
1569
+ <div class="category">
1570
+ <div class="category-header">
1571
+ <span class="category-icon">{category.icon}</span>
1572
+ <span class="category-name">{categoryName}</span>
1573
+ </div>
1574
+
1575
+ <div class="category-components">
1576
+ {#each Object.entries(category.components) as [componentType, component]}
1577
+ <div
1578
+ class="component-item"
1579
+ draggable="true"
1580
+ on:dragstart={(e) => handleSidebarDragStart(e, componentType, component)}
1581
+ >
1582
+ <span class="component-icon">{component.icon}</span>
1583
+ <span class="component-label">{component.label}</span>
1584
+ </div>
1585
+ {/each}
1586
+ </div>
1587
+ </div>
1588
+ {/each}
1589
+ </div>
1590
+ {/if}
1591
+ </div>
1592
+
1593
+ <!-- Main Canvas Area -->
1594
+ <div class="canvas-area">
1595
+ <!-- Toolbar -->
1596
+ <div class="toolbar">
1597
+ <div class="toolbar-left">
1598
+ <input
1599
+ class="workflow-name-input"
1600
+ type="text"
1601
+ bind:value={workflowName}
1602
+ placeholder="Workflow Name"
1603
+ title="Enter workflow name"
1604
+ />
1605
+ </div>
1606
+ <div class="toolbar-center">
1607
+ <!-- Zoom Controls -->
1608
+ <div class="zoom-controls">
1609
+ <button class="zoom-btn" on:click={zoomOut} title="Zoom Out">-</button>
1610
+ <span class="zoom-level">{Math.round(zoomLevel * 100)}%</span>
1611
+ <button class="zoom-btn" on:click={zoomIn} title="Zoom In">+</button>
1612
+ <button class="zoom-btn reset" on:click={resetZoom} title="Reset View">⌂</button>
1613
+ </div>
1614
+ </div>
1615
+ <div class="toolbar-right">
1616
+ <span class="node-count">Nodes: {nodes.length}</span>
1617
+ <span class="edge-count">Edges: {edges.length}</span>
1618
+ <button class="clear-btn" on:click={clearWorkflow} title="Clear Workflow">
1619
+ 🗑️ Clear
1620
+ </button>
1621
+ </div>
1622
+ </div>
1623
+
1624
+ <!-- Canvas Container -->
1625
+ <div class="canvas-container" bind:this={canvasContainer}>
1626
+ <div
1627
+ class="canvas"
1628
+ bind:this={canvas}
1629
+ style="transform: scale({zoomLevel}) translate({panOffset.x / zoomLevel}px, {panOffset.y / zoomLevel}px);"
1630
+ on:drop={handleCanvasDropFromSidebar}
1631
+ on:dragover={handleCanvasDragOver}
1632
+ on:wheel={handleWheel}
1633
+ on:mousedown={startPanning}
1634
+ on:click={() => { selectedNode = null; }}
1635
+ >
1636
+ <!-- Grid Background -->
1637
+ <div class="grid-background"></div>
1638
+
1639
+ <!-- Edges (SVG) -->
1640
+ <svg class="edges-layer">
1641
+ {#each edges as edge (edge.id)}
1642
+ {@const sourceNode = nodes.find(n => n.id === edge.source)}
1643
+ {@const targetNode = nodes.find(n => n.id === edge.target)}
1644
+ {#if sourceNode && targetNode}
1645
+ {@const points = getConnectionPoints(sourceNode, targetNode)}
1646
+ <g class="edge-group">
1647
+ <path
1648
+ d="M {points.sourceX} {points.sourceY} C {points.sourceX + 80} {points.sourceY} {points.targetX - 80} {points.targetY} {points.targetX} {points.targetY}"
1649
+ stroke="#64748b"
1650
+ stroke-width="2"
1651
+ fill="none"
1652
+ class="edge-path"
1653
+ />
1654
+ <circle
1655
+ cx={points.targetX}
1656
+ cy={points.targetY}
1657
+ r="4"
1658
+ fill="#64748b"
1659
+ />
1660
+ <circle
1661
+ cx={(points.sourceX + points.targetX) / 2}
1662
+ cy={(points.sourceY + points.targetY) / 2}
1663
+ r="10"
1664
+ fill="#ef4444"
1665
+ class="edge-delete"
1666
+ on:click|stopPropagation={() => deleteEdge(edge.id)}
1667
+ />
1668
+ <text
1669
+ x={(points.sourceX + points.targetX) / 2}
1670
+ y={(points.sourceY + points.targetY) / 2 + 4}
1671
+ text-anchor="middle"
1672
+ class="edge-delete-text"
1673
+ on:click|stopPropagation={() => deleteEdge(edge.id)}
1674
+ >
1675
+
1676
+ </text>
1677
+ </g>
1678
+ {/if}
1679
+ {/each}
1680
+
1681
+ <!-- Connection preview -->
1682
+ {#if isConnecting && connectionStart}
1683
+ {@const startNode = nodes.find(n => n.id === connectionStart)}
1684
+ {#if startNode}
1685
+ <path
1686
+ d="M {startNode.position.x + 320} {startNode.position.y + 80} L {mousePos.x} {mousePos.y}"
1687
+ stroke="#3b82f6"
1688
+ stroke-width="3"
1689
+ stroke-dasharray="8,4"
1690
+ fill="none"
1691
+ opacity="0.8"
1692
+ />
1693
+ {/if}
1694
+ {/if}
1695
+ </svg>
1696
+
1697
+ <!-- FIXED: Nodes with guaranteed connection points -->
1698
+ {#each nodes as node (node.id)}
1699
+ {@const config = getComponentConfig(node.type)}
1700
+ <div
1701
+ class="node"
1702
+ class:selected={selectedNode?.id === node.id}
1703
+ style="left: {node.position.x}px; top: {node.position.y}px; border-color: {config.color};"
1704
+ on:mousedown={(e) => handleMouseDown(e, node)}
1705
+ on:click={(e) => handleNodeClick(e, node)}
1706
+ >
1707
+ <div class="node-header" style="background: {config.color};">
1708
+ <span class="node-icon">{config.icon}</span>
1709
+ <span class="node-title">{node.data.display_name || node.data.label}</span>
1710
+ <button
1711
+ class="node-delete"
1712
+ on:click|stopPropagation={() => deleteNode(node.id)}
1713
+ title="Delete node"
1714
+ >
1715
+
1716
+ </button>
1717
+ </div>
1718
+
1719
+ <div class="node-content">
1720
+ <!-- Dynamic property rendering based on node type -->
1721
+ {#if propertyFields[node.type]}
1722
+ {#each propertyFields[node.type].slice(0, 3) as field}
1723
+ <div class="node-property">
1724
+ <label class="property-label">{field.label}:</label>
1725
+ {#if field.type === 'select'}
1726
+ <select
1727
+ class="property-select"
1728
+ value={getNodeProperty(node, field.key) || ''}
1729
+ on:change={(e) => updateNodeProperty(node.id, field.key, e.target.value)}
1730
+ on:click|stopPropagation
1731
+ >
1732
+ {#each field.options as option}
1733
+ <option value={option}>{option}</option>
1734
+ {/each}
1735
+ </select>
1736
+ {:else if field.type === 'number'}
1737
+ <input
1738
+ class="property-input"
1739
+ type="number"
1740
+ min={field.min}
1741
+ max={field.max}
1742
+ step={field.step}
1743
+ value={getNodeProperty(node, field.key) || 0}
1744
+ on:input={(e) => updateNodeProperty(node.id, field.key, Number(e.target.value))}
1745
+ on:click|stopPropagation
1746
+ />
1747
+ {:else if field.type === 'checkbox'}
1748
+ <label class="property-checkbox">
1749
+ <input
1750
+ type="checkbox"
1751
+ checked={getNodeProperty(node, field.key) || false}
1752
+ on:change={(e) => updateNodeProperty(node.id, field.key, e.target.checked)}
1753
+ on:click|stopPropagation
1754
+ />
1755
+ <span>Yes</span>
1756
+ </label>
1757
+ {:else if field.type === 'textarea'}
1758
+ <textarea
1759
+ class="property-input"
1760
+ value={getNodeProperty(node, field.key) || ''}
1761
+ on:input={(e) => updateNodeProperty(node.id, field.key, e.target.value)}
1762
+ on:click|stopPropagation
1763
+ rows="2"
1764
+ ></textarea>
1765
+ {:else}
1766
+ <input
1767
+ class="property-input"
1768
+ type="text"
1769
+ value={getNodeProperty(node, field.key) || ''}
1770
+ on:input={(e) => updateNodeProperty(node.id, field.key, e.target.value)}
1771
+ on:click|stopPropagation
1772
+ />
1773
+ {/if}
1774
+ </div>
1775
+ {/each}
1776
+ {:else}
1777
+ <div class="node-status">Ready</div>
1778
+ {/if}
1779
+ </div>
1780
+
1781
+ <!-- FIXED: Connection points with fallback system -->
1782
+ {#if node.data.template}
1783
+ <!-- Try to create dynamic connection points based on template -->
1784
+ {@const templateHandles = Object.entries(node.data.template).filter(([_, handle]) => handle.is_handle)}
1785
+ {#each templateHandles as [handleId, handle], index}
1786
+ {#if handle.type === 'string' || handle.type === 'object' || handle.type === 'list' || handle.type === 'file'}
1787
+ <div
1788
+ class="connection-point {handle.type === 'string' || handle.type === 'list' || handle.type === 'file' ? 'output' : 'input'}"
1789
+ style="top: {index * 25 + 40}px; {(handle.type === 'string' || handle.type === 'list' || handle.type === 'file') ? 'right: -6px;' : 'left: -6px;'}"
1790
+ on:mouseup={(e) => (handle.type === 'object') && endConnection(e, node.id)}
1791
+ on:mousedown={(e) => (handle.type === 'string' || handle.type === 'list' || handle.type === 'file') && startConnection(e, node.id)}
1792
+ title={`${handle.display_name || handleId} (${handle.type})`}
1793
+ ></div>
1794
+ {/if}
1795
+ {/each}
1796
+
1797
+ <!-- FALLBACK: Ensure every node has at least basic connection points -->
1798
+ {@const hasInputHandles = templateHandles.some(([_, h]) => h.type === 'object')}
1799
+ {@const hasOutputHandles = templateHandles.some(([_, h]) => h.type === 'string' || h.type === 'list' || h.type === 'file')}
1800
+
1801
+ {#if !hasInputHandles}
1802
+ <div
1803
+ class="connection-point input"
1804
+ style="top: 50%; left: -6px; transform: translateY(-50%);"
1805
+ on:mouseup={(e) => endConnection(e, node.id)}
1806
+ title="Input"
1807
+ ></div>
1808
+ {/if}
1809
+
1810
+ {#if !hasOutputHandles}
1811
+ <div
1812
+ class="connection-point output"
1813
+ style="top: 50%; right: -6px; transform: translateY(-50%);"
1814
+ on:mousedown={(e) => startConnection(e, node.id)}
1815
+ title="Output"
1816
+ ></div>
1817
+ {/if}
1818
+ {:else}
1819
+ <!-- FALLBACK: Nodes without templates get basic connection points -->
1820
+ <div
1821
+ class="connection-point input"
1822
+ style="top: 50%; left: -6px; transform: translateY(-50%);"
1823
+ on:mouseup={(e) => endConnection(e, node.id)}
1824
+ title="Input"
1825
+ ></div>
1826
+ <div
1827
+ class="connection-point output"
1828
+ style="top: 50%; right: -6px; transform: translateY(-50%);"
1829
+ on:mousedown={(e) => startConnection(e, node.id)}
1830
+ title="Output"
1831
+ ></div>
1832
+ {/if}
1833
+ </div>
1834
+ {/each}
1835
+ </div>
1836
+ </div>
1837
+ </div>
1838
+
1839
+ <!-- Right Property Panel -->
1840
+ <div class="property-panel" class:collapsed={propertyPanelCollapsed}>
1841
+ <div class="property-header">
1842
+ {#if !propertyPanelCollapsed}
1843
+ <h3>Properties</h3>
1844
+ {/if}
1845
+ <button
1846
+ class="toggle-btn property-toggle"
1847
+ on:click={togglePropertyPanel}
1848
+ title={propertyPanelCollapsed ? 'Expand properties' : 'Collapse properties'}
1849
+ >
1850
+ {propertyPanelCollapsed ? '←' : '→'}
1851
+ </button>
1852
+ </div>
1853
+
1854
+ {#if !propertyPanelCollapsed}
1855
+ <div class="property-content">
1856
+ {#if selectedNode && propertyFields[selectedNode.type]}
1857
+ <div class="property-node-info">
1858
+ <h4>{selectedNode.data.display_name || selectedNode.data.label}</h4>
1859
+ <p class="property-node-type">TYPE: {selectedNode.type.toUpperCase()}</p>
1860
+ </div>
1861
+
1862
+ <div class="property-fields">
1863
+ {#each propertyFields[selectedNode.type] as field}
1864
+ <div class="property-field">
1865
+ <label for={field.key}>{field.label}</label>
1866
+ {#if field.help}
1867
+ <small class="field-help">{field.help}</small>
1868
+ {/if}
1869
+
1870
+ {#if field.type === 'text'}
1871
+ <input
1872
+ type="text"
1873
+ id={field.key}
1874
+ value={getNodeProperty(selectedNode, field.key) || ''}
1875
+ on:input={(e) => updateNodeProperty(selectedNode.id, field.key, e.target.value)}
1876
+ />
1877
+ {:else if field.type === 'number'}
1878
+ <input
1879
+ type="number"
1880
+ id={field.key}
1881
+ value={getNodeProperty(selectedNode, field.key) || 0}
1882
+ min={field.min}
1883
+ max={field.max}
1884
+ step={field.step}
1885
+ on:input={(e) => updateNodeProperty(selectedNode.id, field.key, Number(e.target.value))}
1886
+ />
1887
+ {:else if field.type === 'checkbox'}
1888
+ <label class="checkbox-label">
1889
+ <input
1890
+ type="checkbox"
1891
+ id={field.key}
1892
+ checked={getNodeProperty(selectedNode, field.key) || false}
1893
+ on:change={(e) => updateNodeProperty(selectedNode.id, field.key, e.target.checked)}
1894
+ />
1895
+ <span class="checkbox-text">Enable</span>
1896
+ </label>
1897
+ {:else if field.type === 'select'}
1898
+ <select
1899
+ id={field.key}
1900
+ value={getNodeProperty(selectedNode, field.key) || ''}
1901
+ on:change={(e) => updateNodeProperty(selectedNode.id, field.key, e.target.value)}
1902
+ >
1903
+ {#each field.options as option}
1904
+ <option value={option}>{option}</option>
1905
+ {/each}
1906
+ </select>
1907
+ {:else if field.type === 'textarea'}
1908
+ <textarea
1909
+ id={field.key}
1910
+ value={getNodeProperty(selectedNode, field.key) || ''}
1911
+ on:input={(e) => updateNodeProperty(selectedNode.id, field.key, e.target.value)}
1912
+ rows="4"
1913
+ ></textarea>
1914
+ {/if}
1915
+ </div>
1916
+ {/each}
1917
+ </div>
1918
+ {:else}
1919
+ <div class="property-empty">
1920
+ <div class="empty-icon">🎯</div>
1921
+ <p>Select a node to edit properties</p>
1922
+ <small>Click on any node to configure its detailed settings</small>
1923
+ </div>
1924
+ {/if}
1925
+ </div>
1926
+ {/if}
1927
+ </div>
1928
+ </div>
1929
+ </div>
1930
+
1931
+ <style>
1932
+ /* Base styles with proper sizing */
1933
+ .workflow-builder {
1934
+ width: 100%;
1935
+ height: 700px;
1936
+ border: 1px solid #e2e8f0;
1937
+ border-radius: 12px;
1938
+ display: flex;
1939
+ flex-direction: column;
1940
+ background: #ffffff;
1941
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
1942
+ overflow: hidden;
1943
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
1944
+ }
1945
+
1946
+ .hide {
1947
+ display: none;
1948
+ }
1949
+
1950
+ .top-section {
1951
+ flex: 1;
1952
+ display: flex;
1953
+ min-height: 0;
1954
+ }
1955
+
1956
+ /* Sidebar Styles */
1957
+ .sidebar {
1958
+ width: 240px;
1959
+ min-width: 240px;
1960
+ background: #f8fafc;
1961
+ border-right: 1px solid #e2e8f0;
1962
+ display: flex;
1963
+ flex-direction: column;
1964
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1965
+ position: relative;
1966
+ }
1967
+
1968
+ .sidebar.collapsed {
1969
+ width: 48px;
1970
+ min-width: 48px;
1971
+ }
1972
+
1973
+ .sidebar-header {
1974
+ padding: 12px;
1975
+ border-bottom: 1px solid #e2e8f0;
1976
+ display: flex;
1977
+ align-items: center;
1978
+ justify-content: space-between;
1979
+ background: white;
1980
+ min-height: 50px;
1981
+ box-sizing: border-box;
1982
+ }
1983
+
1984
+ .sidebar-header h3 {
1985
+ margin: 0;
1986
+ font-size: 15px;
1987
+ font-weight: 600;
1988
+ color: #1e293b;
1989
+ }
1990
+
1991
+ .toggle-btn {
1992
+ background: #f1f5f9;
1993
+ border: 1px solid #e2e8f0;
1994
+ border-radius: 6px;
1995
+ padding: 6px 8px;
1996
+ cursor: pointer;
1997
+ color: #64748b;
1998
+ font-size: 14px;
1999
+ transition: all 0.2s;
2000
+ min-width: 28px;
2001
+ height: 28px;
2002
+ display: flex;
2003
+ align-items: center;
2004
+ justify-content: center;
2005
+ z-index: 10;
2006
+ position: relative;
2007
+ }
2008
+
2009
+ .toggle-btn:hover {
2010
+ background: #e2e8f0;
2011
+ color: #475569;
2012
+ }
2013
+
2014
+ .sidebar-toggle {
2015
+ position: absolute;
2016
+ right: 8px;
2017
+ top: 50%;
2018
+ transform: translateY(-50%);
2019
+ }
2020
+
2021
+ .sidebar-content {
2022
+ flex: 1;
2023
+ overflow-y: auto;
2024
+ padding: 12px;
2025
+ }
2026
+
2027
+ .category {
2028
+ margin-bottom: 12px;
2029
+ }
2030
+
2031
+ .category-header {
2032
+ display: flex;
2033
+ align-items: center;
2034
+ padding: 6px 0;
2035
+ font-weight: 600;
2036
+ font-size: 12px;
2037
+ color: #374151;
2038
+ border-bottom: 1px solid #e5e7eb;
2039
+ margin-bottom: 6px;
2040
+ }
2041
+
2042
+ .category-icon {
2043
+ margin-right: 6px;
2044
+ font-size: 14px;
2045
+ }
2046
+
2047
+ .component-item {
2048
+ display: flex;
2049
+ align-items: center;
2050
+ padding: 6px 8px;
2051
+ margin-bottom: 3px;
2052
+ background: white;
2053
+ border: 1px solid #e5e7eb;
2054
+ border-radius: 6px;
2055
+ cursor: grab;
2056
+ transition: all 0.2s ease;
2057
+ font-size: 12px;
2058
+ }
2059
+
2060
+ .component-item:hover {
2061
+ background: #f8fafc;
2062
+ border-color: #cbd5e1;
2063
+ transform: translateX(2px);
2064
+ }
2065
+
2066
+ .component-item:active {
2067
+ cursor: grabbing;
2068
+ }
2069
+
2070
+ .component-icon {
2071
+ margin-right: 6px;
2072
+ font-size: 14px;
2073
+ }
2074
+
2075
+ .component-label {
2076
+ font-weight: 500;
2077
+ color: #374151;
2078
+ }
2079
+
2080
+ /* Canvas Area Styles */
2081
+ .canvas-area {
2082
+ flex: 1;
2083
+ display: flex;
2084
+ flex-direction: column;
2085
+ min-width: 400px;
2086
+ }
2087
+
2088
+ .toolbar {
2089
+ height: 50px;
2090
+ border-bottom: 1px solid #e2e8f0;
2091
+ display: flex;
2092
+ align-items: center;
2093
+ justify-content: space-between;
2094
+ padding: 0 16px;
2095
+ background: white;
2096
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
2097
+ }
2098
+
2099
+ .workflow-name-input {
2100
+ font-size: 16px;
2101
+ font-weight: 600;
2102
+ color: #1e293b;
2103
+ border: none;
2104
+ background: transparent;
2105
+ outline: none;
2106
+ padding: 4px 8px;
2107
+ border-radius: 4px;
2108
+ transition: background 0.2s;
2109
+ }
2110
+
2111
+ .workflow-name-input:hover,
2112
+ .workflow-name-input:focus {
2113
+ background: #f1f5f9;
2114
+ }
2115
+
2116
+ .toolbar-center {
2117
+ display: flex;
2118
+ align-items: center;
2119
+ }
2120
+
2121
+ .zoom-controls {
2122
+ display: flex;
2123
+ align-items: center;
2124
+ gap: 4px;
2125
+ background: #f1f5f9;
2126
+ padding: 4px;
2127
+ border-radius: 8px;
2128
+ border: 1px solid #e2e8f0;
2129
+ }
2130
+
2131
+ .zoom-btn {
2132
+ background: white;
2133
+ border: none;
2134
+ width: 28px;
2135
+ height: 28px;
2136
+ border-radius: 4px;
2137
+ cursor: pointer;
2138
+ font-weight: 600;
2139
+ display: flex;
2140
+ align-items: center;
2141
+ justify-content: center;
2142
+ transition: all 0.2s;
2143
+ font-size: 14px;
2144
+ }
2145
+
2146
+ .zoom-btn:hover {
2147
+ background: #e2e8f0;
2148
+ }
2149
+
2150
+ .zoom-btn.reset {
2151
+ font-size: 12px;
2152
+ }
2153
+
2154
+ .zoom-level {
2155
+ font-size: 12px;
2156
+ font-weight: 600;
2157
+ color: #64748b;
2158
+ min-width: 40px;
2159
+ text-align: center;
2160
+ }
2161
+
2162
+ .toolbar-right {
2163
+ display: flex;
2164
+ gap: 12px;
2165
+ font-size: 12px;
2166
+ align-items: center;
2167
+ }
2168
+
2169
+ .node-count, .edge-count {
2170
+ color: #64748b;
2171
+ background: #f1f5f9;
2172
+ padding: 4px 8px;
2173
+ border-radius: 12px;
2174
+ font-weight: 500;
2175
+ }
2176
+
2177
+ .export-btn {
2178
+ background: #3b82f6;
2179
+ color: white;
2180
+ border: none;
2181
+ padding: 6px 12px;
2182
+ border-radius: 6px;
2183
+ font-size: 12px;
2184
+ font-weight: 500;
2185
+ cursor: pointer;
2186
+ transition: all 0.2s;
2187
+ display: flex;
2188
+ align-items: center;
2189
+ gap: 4px;
2190
+ }
2191
+
2192
+ .export-btn:hover {
2193
+ background: #2563eb;
2194
+ transform: translateY(-1px);
2195
+ }
2196
+
2197
+ .canvas-container {
2198
+ flex: 1;
2199
+ position: relative;
2200
+ overflow: hidden;
2201
+ background: #fafbfc;
2202
+ cursor: grab;
2203
+ }
2204
+
2205
+ .canvas-container:active {
2206
+ cursor: grabbing;
2207
+ }
2208
+
2209
+ .canvas {
2210
+ position: absolute;
2211
+ top: 0;
2212
+ left: 0;
2213
+ width: 4000px;
2214
+ height: 4000px;
2215
+ transform-origin: 0 0;
2216
+ }
2217
+
2218
+ .grid-background {
2219
+ position: absolute;
2220
+ top: 0;
2221
+ left: 0;
2222
+ width: 100%;
2223
+ height: 100%;
2224
+ background-image:
2225
+ radial-gradient(circle, #e2e8f0 1px, transparent 1px);
2226
+ background-size: 20px 20px;
2227
+ pointer-events: none;
2228
+ opacity: 0.6;
2229
+ }
2230
+
2231
+ .edges-layer {
2232
+ position: absolute;
2233
+ top: 0;
2234
+ left: 0;
2235
+ width: 100%;
2236
+ height: 100%;
2237
+ pointer-events: none;
2238
+ z-index: 1;
2239
+ }
2240
+
2241
+ .edge-delete, .edge-delete-text {
2242
+ pointer-events: all;
2243
+ cursor: pointer;
2244
+ }
2245
+
2246
+ .edge-delete-text {
2247
+ font-size: 10px;
2248
+ fill: white;
2249
+ text-anchor: middle;
2250
+ user-select: none;
2251
+ }
2252
+
2253
+ .edge-delete:hover {
2254
+ fill: #dc2626;
2255
+ }
2256
+
2257
+ /* Node styles with proper sizing and no overflow */
2258
+ .node {
2259
+ position: absolute;
2260
+ width: 320px;
2261
+ min-height: 160px;
2262
+ background: white;
2263
+ border: 2px solid #e2e8f0;
2264
+ border-radius: 10px;
2265
+ cursor: move;
2266
+ user-select: none;
2267
+ z-index: 2;
2268
+ transition: all 0.2s ease;
2269
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2270
+ overflow: visible;
2271
+ }
2272
+
2273
+ .node:hover {
2274
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
2275
+ transform: translateY(-1px);
2276
+ }
2277
+
2278
+ .node.selected {
2279
+ border-color: #3b82f6;
2280
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 4px 16px rgba(0, 0, 0, 0.15);
2281
+ }
2282
+
2283
+ .node-header {
2284
+ display: flex;
2285
+ align-items: center;
2286
+ padding: 12px 16px;
2287
+ color: white;
2288
+ font-weight: 600;
2289
+ font-size: 14px;
2290
+ border-radius: 8px 8px 0 0;
2291
+ min-height: 24px;
2292
+ }
2293
+
2294
+ .node-icon {
2295
+ margin-right: 8px;
2296
+ font-size: 16px;
2297
+ flex-shrink: 0;
2298
+ }
2299
+
2300
+ .node-title {
2301
+ flex: 1;
2302
+ overflow: hidden;
2303
+ text-overflow: ellipsis;
2304
+ white-space: nowrap;
2305
+ }
2306
+
2307
+ .node-delete {
2308
+ background: rgba(255, 255, 255, 0.2);
2309
+ border: none;
2310
+ color: white;
2311
+ cursor: pointer;
2312
+ font-size: 12px;
2313
+ padding: 4px 6px;
2314
+ border-radius: 4px;
2315
+ transition: all 0.2s;
2316
+ flex-shrink: 0;
2317
+ }
2318
+
2319
+ .node-delete:hover {
2320
+ background: rgba(255, 255, 255, 0.3);
2321
+ }
2322
+
2323
+ .node-content {
2324
+ padding: 12px 16px;
2325
+ max-height: 200px;
2326
+ overflow-y: auto;
2327
+ overflow-x: hidden;
2328
+ }
2329
+
2330
+ .node-property {
2331
+ display: flex;
2332
+ flex-direction: column;
2333
+ gap: 4px;
2334
+ margin-bottom: 12px;
2335
+ font-size: 12px;
2336
+ }
2337
+
2338
+ .property-label {
2339
+ font-weight: 600;
2340
+ color: #374151;
2341
+ font-size: 11px;
2342
+ margin-bottom: 2px;
2343
+ }
2344
+
2345
+ .property-input, .property-select {
2346
+ width: 100%;
2347
+ padding: 6px 8px;
2348
+ border: 1px solid #d1d5db;
2349
+ border-radius: 4px;
2350
+ font-size: 11px;
2351
+ background: white;
2352
+ transition: all 0.2s;
2353
+ box-sizing: border-box;
2354
+ resize: vertical;
2355
+ }
2356
+
2357
+ .property-input:focus, .property-select:focus {
2358
+ outline: none;
2359
+ border-color: #3b82f6;
2360
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
2361
+ }
2362
+
2363
+ .property-input:hover, .property-select:hover {
2364
+ border-color: #9ca3af;
2365
+ }
2366
+
2367
+ .property-checkbox {
2368
+ display: flex;
2369
+ align-items: center;
2370
+ gap: 6px;
2371
+ font-size: 11px;
2372
+ color: #374151;
2373
+ cursor: pointer;
2374
+ }
2375
+
2376
+ .property-checkbox input[type="checkbox"] {
2377
+ width: auto;
2378
+ margin: 0;
2379
+ cursor: pointer;
2380
+ }
2381
+
2382
+ .node-status {
2383
+ font-size: 12px;
2384
+ color: #64748b;
2385
+ text-align: center;
2386
+ padding: 20px;
2387
+ font-style: italic;
2388
+ }
2389
+
2390
+ /* FIXED: Connection points that work for ALL nodes */
2391
+ .connection-point {
2392
+ position: absolute;
2393
+ width: 12px;
2394
+ height: 12px;
2395
+ border-radius: 50%;
2396
+ background: #3b82f6;
2397
+ border: 2px solid white;
2398
+ cursor: crosshair;
2399
+ z-index: 3;
2400
+ transition: all 0.2s ease;
2401
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
2402
+ }
2403
+
2404
+ .connection-point.input {
2405
+ left: -6px;
2406
+ }
2407
+
2408
+ .connection-point.output {
2409
+ right: -6px;
2410
+ }
2411
+
2412
+ .connection-point:hover {
2413
+ background: #2563eb;
2414
+ transform: scale(1.2);
2415
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
2416
+ }
2417
+
2418
+ /* Property Panel Styles */
2419
+ .property-panel {
2420
+ width: 280px;
2421
+ min-width: 280px;
2422
+ background: #f8fafc;
2423
+ border-left: 1px solid #e2e8f0;
2424
+ display: flex;
2425
+ flex-direction: column;
2426
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2427
+ position: relative;
2428
+ }
2429
+
2430
+ .property-panel.collapsed {
2431
+ width: 48px;
2432
+ min-width: 48px;
2433
+ }
2434
+
2435
+ .property-header {
2436
+ padding: 12px;
2437
+ border-bottom: 1px solid #e2e8f0;
2438
+ display: flex;
2439
+ align-items: center;
2440
+ justify-content: space-between;
2441
+ background: white;
2442
+ min-height: 50px;
2443
+ box-sizing: border-box;
2444
+ }
2445
+
2446
+ .property-header h3 {
2447
+ margin: 0;
2448
+ font-size: 15px;
2449
+ font-weight: 600;
2450
+ color: #1e293b;
2451
+ }
2452
+
2453
+ .property-toggle {
2454
+ position: absolute;
2455
+ left: 8px;
2456
+ top: 50%;
2457
+ transform: translateY(-50%);
2458
+ }
2459
+
2460
+ .property-content {
2461
+ flex: 1;
2462
+ overflow-y: auto;
2463
+ padding: 16px;
2464
+ }
2465
+
2466
+ .property-node-info {
2467
+ margin-bottom: 20px;
2468
+ padding: 12px;
2469
+ background: white;
2470
+ border-radius: 8px;
2471
+ border: 1px solid #e2e8f0;
2472
+ }
2473
+
2474
+ .property-node-info h4 {
2475
+ margin: 0 0 4px 0;
2476
+ font-size: 16px;
2477
+ color: #1e293b;
2478
+ }
2479
+
2480
+ .property-node-type {
2481
+ margin: 0;
2482
+ font-size: 11px;
2483
+ color: #64748b;
2484
+ text-transform: uppercase;
2485
+ font-weight: 600;
2486
+ }
2487
+
2488
+ .property-field {
2489
+ margin-bottom: 16px;
2490
+ }
2491
+
2492
+ .property-field label {
2493
+ display: block;
2494
+ margin-bottom: 6px;
2495
+ font-size: 13px;
2496
+ font-weight: 600;
2497
+ color: #374151;
2498
+ }
2499
+
2500
+ .field-help {
2501
+ display: block;
2502
+ margin-bottom: 4px;
2503
+ font-size: 11px;
2504
+ color: #64748b;
2505
+ font-style: italic;
2506
+ }
2507
+
2508
+ .property-field input,
2509
+ .property-field select,
2510
+ .property-field textarea {
2511
+ width: 100%;
2512
+ padding: 8px 10px;
2513
+ border: 1px solid #d1d5db;
2514
+ border-radius: 6px;
2515
+ font-size: 13px;
2516
+ background: white;
2517
+ transition: border-color 0.2s;
2518
+ box-sizing: border-box;
2519
+ }
2520
+
2521
+ .property-field input:focus,
2522
+ .property-field select:focus,
2523
+ .property-field textarea:focus {
2524
+ outline: none;
2525
+ border-color: #3b82f6;
2526
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
2527
+ }
2528
+
2529
+ .checkbox-label {
2530
+ display: flex !important;
2531
+ align-items: center;
2532
+ margin-bottom: 0 !important;
2533
+ cursor: pointer;
2534
+ }
2535
+
2536
+ .checkbox-label input[type="checkbox"] {
2537
+ width: auto !important;
2538
+ margin-right: 8px !important;
2539
+ }
2540
+
2541
+ .property-empty {
2542
+ text-align: center;
2543
+ padding: 40px 16px;
2544
+ color: #64748b;
2545
+ }
2546
+
2547
+ .empty-icon {
2548
+ font-size: 32px;
2549
+ margin-bottom: 12px;
2550
+ opacity: 0.5;
2551
+ }
2552
+
2553
+ .property-empty p {
2554
+ margin: 0 0 6px 0;
2555
+ font-size: 14px;
2556
+ font-weight: 500;
2557
+ }
2558
+
2559
+ .property-empty small {
2560
+ font-size: 12px;
2561
+ opacity: 0.7;
2562
+ }
2563
+ .clear-btn {
2564
+ background: #ef4444;
2565
+ color: white;
2566
+ border: none;
2567
+ padding: 6px 12px;
2568
+ border-radius: 6px;
2569
+ font-size: 12px;
2570
+ font-weight: 500;
2571
+ cursor: pointer;
2572
+ transition: all 0.2s;
2573
+ display: flex;
2574
+ align-items: center;
2575
+ gap: 4px;
2576
+ }
2577
+
2578
+ .clear-btn:hover {
2579
+ background: #dc2626;
2580
+ transform: translateY(-1px);
2581
+ }
2582
+
2583
+ </style>