openfree commited on
Commit
44bc00c
·
verified ·
1 Parent(s): 143e728

Upload Index (2).svelte

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