lyu-extension-workflow 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend/dist/index.html +624 -134
- package/package.json +1 -1
package/frontend/dist/index.html
CHANGED
|
@@ -1,196 +1,686 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="UTF-8"
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"
|
|
6
|
-
<title>Lyu-Code Workflow
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Lyu-Code Workflow</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
7
9
|
<style>
|
|
10
|
+
/* ===========================================
|
|
11
|
+
CSS CUSTOM PROPERTIES (NEON CYBER THEME)
|
|
12
|
+
=========================================== */
|
|
13
|
+
:root {
|
|
14
|
+
--bg-primary: #0a0f1c;
|
|
15
|
+
--bg-secondary: #111827;
|
|
16
|
+
--bg-tertiary: #1a2332;
|
|
17
|
+
--text-primary: #e2e8f0;
|
|
18
|
+
--text-secondary: #94a3b8;
|
|
19
|
+
--text-muted: #64748b;
|
|
20
|
+
--accent: #22d3ee;
|
|
21
|
+
--accent-glow: rgba(34, 211, 238, 0.3);
|
|
22
|
+
--accent2: #a78bfa;
|
|
23
|
+
--accent2-glow: rgba(167, 139, 250, 0.3);
|
|
24
|
+
--success: #4ade80;
|
|
25
|
+
--warning: #fbbf24;
|
|
26
|
+
--danger: #f87171;
|
|
27
|
+
--border: rgba(148, 163, 184, 0.12);
|
|
28
|
+
--border-active: rgba(34, 211, 238, 0.4);
|
|
29
|
+
--font-display: "Space Grotesk", sans-serif;
|
|
30
|
+
--font-mono: "JetBrains Mono", monospace;
|
|
31
|
+
--radius: 10px;
|
|
32
|
+
--radius-sm: 6px;
|
|
33
|
+
--shadow: 0 4px 24px rgba(0,0,0,0.4);
|
|
34
|
+
--shadow-glow: 0 0 20px var(--accent-glow);
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
html, body {
|
|
40
|
+
height: 100%;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
font-family: var(--font-display);
|
|
43
|
+
background: var(--bg-primary);
|
|
44
|
+
color: var(--text-primary);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ===========================================
|
|
48
|
+
LAYOUT
|
|
49
|
+
=========================================== */
|
|
50
|
+
#app {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-rows: 48px 1fr;
|
|
53
|
+
grid-template-columns: 240px 1fr 280px;
|
|
54
|
+
height: 100vh;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ─── Header ──────────────────────────────── */
|
|
58
|
+
header {
|
|
59
|
+
grid-column: 1 / -1;
|
|
60
|
+
background: var(--bg-secondary);
|
|
61
|
+
border-bottom: 1px solid var(--border);
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
padding: 0 16px;
|
|
65
|
+
gap: 12px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
header .logo {
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
letter-spacing: -0.3px;
|
|
72
|
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
73
|
+
-webkit-background-clip: text;
|
|
74
|
+
-webkit-text-fill-color: transparent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
header .wf-name {
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
color: var(--text-secondary);
|
|
80
|
+
border: 1px solid var(--border);
|
|
81
|
+
border-radius: var(--radius-sm);
|
|
82
|
+
padding: 3px 8px;
|
|
83
|
+
background: transparent;
|
|
84
|
+
outline: none;
|
|
85
|
+
font-family: var(--font-display);
|
|
86
|
+
width: 180px;
|
|
87
|
+
}
|
|
88
|
+
header .wf-name:focus { border-color: var(--accent); }
|
|
89
|
+
|
|
90
|
+
.toolbar {
|
|
91
|
+
display: flex;
|
|
92
|
+
gap: 4px;
|
|
93
|
+
margin-left: auto;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.btn {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 5px;
|
|
100
|
+
padding: 5px 10px;
|
|
101
|
+
border: 1px solid var(--border);
|
|
102
|
+
border-radius: var(--radius-sm);
|
|
103
|
+
background: transparent;
|
|
104
|
+
color: var(--text-secondary);
|
|
105
|
+
font-size: 12px;
|
|
106
|
+
font-family: var(--font-display);
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
transition: all 0.15s;
|
|
109
|
+
}
|
|
110
|
+
.btn:hover { background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--text-muted); }
|
|
111
|
+
.btn.primary { background: rgba(34,211,238,0.12); color: var(--accent); border-color: var(--accent); }
|
|
112
|
+
.btn.primary:hover { background: rgba(34,211,238,0.22); box-shadow: var(--shadow-glow); }
|
|
113
|
+
.btn.danger { color: var(--danger); border-color: rgba(248,113,113,0.3); }
|
|
114
|
+
.btn.danger:hover { background: rgba(248,113,113,0.1); }
|
|
115
|
+
|
|
116
|
+
.status-pill {
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
padding: 2px 8px;
|
|
119
|
+
border-radius: 99px;
|
|
120
|
+
font-family: var(--font-mono);
|
|
121
|
+
}
|
|
122
|
+
.status-pill.idle { background: rgba(100,116,139,0.15); color: var(--text-muted); }
|
|
123
|
+
.status-pill.running { background: rgba(74,222,128,0.15); color: var(--success); animation: pulse 1.5s infinite; }
|
|
124
|
+
.status-pill.done { background: rgba(34,211,238,0.15); color: var(--accent); }
|
|
125
|
+
.status-pill.error { background: rgba(248,113,113,0.15); color: var(--danger); }
|
|
126
|
+
|
|
127
|
+
@keyframes pulse { 0%,100%{ opacity:1 } 50%{ opacity:0.6 } }
|
|
128
|
+
|
|
129
|
+
/* ─── Sidebar (Node Palette) ──────────────── */
|
|
130
|
+
aside {
|
|
131
|
+
background: var(--bg-secondary);
|
|
132
|
+
border-right: 1px solid var(--border);
|
|
133
|
+
padding: 12px;
|
|
134
|
+
overflow-y: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
aside h3 {
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
text-transform: uppercase;
|
|
140
|
+
letter-spacing: 1px;
|
|
141
|
+
color: var(--text-muted);
|
|
142
|
+
margin-bottom: 10px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.palette-item {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 8px;
|
|
149
|
+
padding: 8px 10px;
|
|
150
|
+
margin-bottom: 4px;
|
|
151
|
+
border-radius: var(--radius-sm);
|
|
152
|
+
border: 1px solid var(--border);
|
|
153
|
+
cursor: grab;
|
|
154
|
+
transition: all 0.15s;
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
}
|
|
157
|
+
.palette-item:hover {
|
|
158
|
+
border-color: var(--accent);
|
|
159
|
+
background: rgba(34,211,238,0.06);
|
|
160
|
+
}
|
|
161
|
+
.palette-item .icon {
|
|
162
|
+
width: 28px;
|
|
163
|
+
height: 28px;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
font-size: 14px;
|
|
169
|
+
}
|
|
170
|
+
.palette-item.agent .icon { background: rgba(34,211,238,0.15); }
|
|
171
|
+
|
|
172
|
+
.config-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
|
|
173
|
+
.config-section label {
|
|
174
|
+
display: block;
|
|
175
|
+
font-size: 11px;
|
|
176
|
+
color: var(--text-muted);
|
|
177
|
+
margin-bottom: 4px;
|
|
178
|
+
margin-top: 8px;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.5px;
|
|
181
|
+
}
|
|
182
|
+
.config-section input, .config-section textarea, .config-section select {
|
|
183
|
+
width: 100%;
|
|
184
|
+
padding: 5px 8px;
|
|
185
|
+
background: var(--bg-primary);
|
|
186
|
+
border: 1px solid var(--border);
|
|
187
|
+
border-radius: var(--radius-sm);
|
|
188
|
+
color: var(--text-primary);
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
font-family: var(--font-display);
|
|
191
|
+
outline: none;
|
|
192
|
+
}
|
|
193
|
+
.config-section input:focus, .config-section textarea:focus, .config-section select:focus {
|
|
194
|
+
border-color: var(--accent);
|
|
195
|
+
}
|
|
196
|
+
.config-section textarea { min-height: 60px; resize: vertical; font-family: var(--font-mono); font-size: 11px; }
|
|
197
|
+
|
|
198
|
+
/* ─── Canvas ──────────────────────────────── */
|
|
199
|
+
.canvas-wrap {
|
|
200
|
+
position: relative;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
background:
|
|
203
|
+
radial-gradient(ellipse at 30% 70%, rgba(34,211,238,0.04) 0%, transparent 50%),
|
|
204
|
+
radial-gradient(ellipse at 70% 30%, rgba(167,139,250,0.04) 0%, transparent 50%),
|
|
205
|
+
var(--bg-primary);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Grid pattern */
|
|
209
|
+
.canvas-wrap::before {
|
|
210
|
+
content: '';
|
|
211
|
+
position: absolute;
|
|
212
|
+
inset: 0;
|
|
213
|
+
background-image:
|
|
214
|
+
linear-gradient(rgba(148,163,184,0.04) 1px, transparent 1px),
|
|
215
|
+
linear-gradient(90deg, rgba(148,163,184,0.04) 1px, transparent 1px);
|
|
216
|
+
background-size: 40px 40px;
|
|
217
|
+
pointer-events: none;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#canvas {
|
|
221
|
+
position: absolute;
|
|
222
|
+
inset: 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* SVG arrows */
|
|
226
|
+
.canvas-wrap svg {
|
|
227
|
+
position: absolute;
|
|
228
|
+
inset: 0;
|
|
229
|
+
width: 100%;
|
|
230
|
+
height: 100%;
|
|
231
|
+
pointer-events: none;
|
|
232
|
+
}
|
|
233
|
+
.canvas-wrap svg path {
|
|
234
|
+
fill: none;
|
|
235
|
+
stroke: var(--accent);
|
|
236
|
+
stroke-width: 2;
|
|
237
|
+
stroke-dasharray: 8 4;
|
|
238
|
+
opacity: 0.5;
|
|
239
|
+
filter: drop-shadow(0 0 4px var(--accent-glow));
|
|
240
|
+
}
|
|
241
|
+
.canvas-wrap svg path.active {
|
|
242
|
+
stroke: var(--success);
|
|
243
|
+
opacity: 1;
|
|
244
|
+
stroke-dasharray: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ─── Nodes on canvas ─────────────────────── */
|
|
248
|
+
.node {
|
|
249
|
+
position: absolute;
|
|
250
|
+
width: 180px;
|
|
251
|
+
background: var(--bg-secondary);
|
|
252
|
+
border: 1px solid var(--border);
|
|
253
|
+
border-radius: var(--radius);
|
|
254
|
+
box-shadow: var(--shadow);
|
|
255
|
+
cursor: move;
|
|
256
|
+
user-select: none;
|
|
257
|
+
transition: box-shadow 0.15s, border-color 0.15s;
|
|
258
|
+
}
|
|
259
|
+
.node:hover { border-color: var(--text-muted); }
|
|
260
|
+
.node.selected { border-color: var(--accent); box-shadow: var(--shadow), var(--shadow-glow); }
|
|
261
|
+
.node.running { border-color: var(--success); animation: pulse 1.5s infinite; }
|
|
262
|
+
.node.done { border-color: rgba(74,222,128,0.4); }
|
|
263
|
+
|
|
264
|
+
.node-header {
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 6px;
|
|
268
|
+
padding: 8px 10px;
|
|
269
|
+
border-bottom: 1px solid var(--border);
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
}
|
|
273
|
+
.node-header .dot {
|
|
274
|
+
width: 8px;
|
|
275
|
+
height: 8px;
|
|
276
|
+
border-radius: 50%;
|
|
277
|
+
background: var(--accent);
|
|
278
|
+
}
|
|
279
|
+
.node.running .node-header .dot { background: var(--success); }
|
|
280
|
+
.node.done .node-header .dot { background: var(--success); opacity: 0.5; }
|
|
281
|
+
|
|
282
|
+
.node-body {
|
|
283
|
+
padding: 8px 10px;
|
|
284
|
+
font-size: 11px;
|
|
285
|
+
color: var(--text-muted);
|
|
286
|
+
}
|
|
287
|
+
.node-body .model-tag {
|
|
288
|
+
display: inline-block;
|
|
289
|
+
padding: 1px 6px;
|
|
290
|
+
background: rgba(167,139,250,0.12);
|
|
291
|
+
color: var(--accent2);
|
|
292
|
+
border-radius: 4px;
|
|
293
|
+
font-family: var(--font-mono);
|
|
294
|
+
font-size: 10px;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Connection ports */
|
|
298
|
+
.port {
|
|
299
|
+
width: 10px;
|
|
300
|
+
height: 10px;
|
|
301
|
+
border-radius: 50%;
|
|
302
|
+
background: var(--bg-tertiary);
|
|
303
|
+
border: 2px solid var(--text-muted);
|
|
304
|
+
position: absolute;
|
|
305
|
+
cursor: crosshair;
|
|
306
|
+
transition: all 0.15s;
|
|
307
|
+
}
|
|
308
|
+
.port:hover { border-color: var(--accent); background: var(--accent-glow); transform: scale(1.3); }
|
|
309
|
+
.port.in { left: -5px; top: 50%; margin-top: -5px; }
|
|
310
|
+
.port.out { right: -5px; top: 50%; margin-top: -5px; }
|
|
311
|
+
|
|
312
|
+
/* ─── Right Panel (Run & Debug) ───────────── */
|
|
313
|
+
.panel {
|
|
314
|
+
background: var(--bg-secondary);
|
|
315
|
+
border-left: 1px solid var(--border);
|
|
316
|
+
padding: 12px;
|
|
317
|
+
overflow-y: auto;
|
|
318
|
+
font-size: 12px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.panel h3 {
|
|
322
|
+
font-size: 11px;
|
|
323
|
+
text-transform: uppercase;
|
|
324
|
+
letter-spacing: 1px;
|
|
325
|
+
color: var(--text-muted);
|
|
326
|
+
margin-bottom: 10px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.run-log {
|
|
330
|
+
font-family: var(--font-mono);
|
|
331
|
+
font-size: 11px;
|
|
332
|
+
line-height: 1.6;
|
|
333
|
+
}
|
|
334
|
+
.run-log .entry {
|
|
335
|
+
padding: 4px 6px;
|
|
336
|
+
margin-bottom: 2px;
|
|
337
|
+
border-radius: 4px;
|
|
338
|
+
background: var(--bg-primary);
|
|
339
|
+
}
|
|
340
|
+
.run-log .entry.error { color: var(--danger); }
|
|
341
|
+
.run-log .entry.success { color: var(--success); }
|
|
342
|
+
|
|
343
|
+
.iter-badge {
|
|
344
|
+
display: inline-block;
|
|
345
|
+
padding: 1px 6px;
|
|
346
|
+
background: rgba(251,191,36,0.12);
|
|
347
|
+
color: var(--warning);
|
|
348
|
+
border-radius: 4px;
|
|
349
|
+
font-family: var(--font-mono);
|
|
350
|
+
font-size: 10px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* ─── Empty State ─────────────────────────── */
|
|
354
|
+
.empty-state {
|
|
355
|
+
position: absolute;
|
|
356
|
+
inset: 0;
|
|
357
|
+
display: flex;
|
|
358
|
+
flex-direction: column;
|
|
359
|
+
align-items: center;
|
|
360
|
+
justify-content: center;
|
|
361
|
+
color: var(--text-muted);
|
|
362
|
+
gap: 8px;
|
|
363
|
+
}
|
|
364
|
+
.empty-state .big-icon { font-size: 48px; opacity: 0.3; }
|
|
365
|
+
.empty-state p { font-size: 14px; }
|
|
366
|
+
.empty-state .hint { font-size: 11px; color: var(--text-muted); opacity: 0.6; }
|
|
367
|
+
|
|
368
|
+
/* ─── Connection line being drawn ─────────── */
|
|
369
|
+
.temp-line {
|
|
370
|
+
stroke: var(--accent);
|
|
371
|
+
stroke-width: 2;
|
|
372
|
+
stroke-dasharray: 4 4;
|
|
373
|
+
opacity: 0.6;
|
|
374
|
+
}
|
|
38
375
|
</style>
|
|
39
376
|
</head>
|
|
40
377
|
<body>
|
|
41
378
|
<div id="app">
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<span class="
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</span>
|
|
56
|
-
</div>
|
|
379
|
+
<!-- Header -->
|
|
380
|
+
<header>
|
|
381
|
+
<span class="logo">⚡ lyu-code</span>
|
|
382
|
+
<span style="color:var(--text-muted);font-size:12px">/</span>
|
|
383
|
+
<input class="wf-name" id="wfName" value="Untitled Workflow" placeholder="Workflow name" />
|
|
384
|
+
<span class="status-pill idle" id="statusP#ill">IDLE</span>
|
|
385
|
+
<div class="toolbar">
|
|
386
|
+
<button class="btn" onclick="addNode('agent')">+ Agent</button>
|
|
387
|
+
<button class="btn primary" id="runBtn" onclick="runWorkflow()">▶ Run</button>
|
|
388
|
+
<button class="btn" onclick="stopWorkflow()">■ Stop</button>
|
|
389
|
+
<button class="btn danger" onclick="clearAll()">✕ Clear</button>
|
|
390
|
+
</div>
|
|
391
|
+
</header>
|
|
57
392
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
WebSocket: <code id="wsUrl">ws://127.0.0.1:18789/ws</code><br>
|
|
65
|
-
API: <code id="apiUrl">http://127.0.0.1:18789/api</code>
|
|
66
|
-
</p>
|
|
393
|
+
<!-- Left Sidebar: Palette & Config -->
|
|
394
|
+
<aside>
|
|
395
|
+
<h3>Nodes</h3>
|
|
396
|
+
<div class="palette-item agent" onclick="addNode('agent')">
|
|
397
|
+
<div class="icon">🤖</div>
|
|
398
|
+
<span>Agent</span>
|
|
67
399
|
</div>
|
|
68
|
-
</div>
|
|
69
400
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<div class="panel-body">
|
|
401
|
+
<div class="config-section" id="nodeConfig" style="display:none">
|
|
402
|
+
<h3>Selected Node</h3>
|
|
73
403
|
<label>Label</label>
|
|
74
|
-
<input type="text" id="
|
|
75
|
-
|
|
76
|
-
<label>System Prompt</label>
|
|
77
|
-
<textarea id="nodePrompt" placeholder="You are a helpful coding agent..."></textarea>
|
|
78
|
-
|
|
404
|
+
<input type="text" id="cfgLabel" oninput="updateSelectedNode()" />
|
|
79
405
|
<label>Model</label>
|
|
80
|
-
<select id="
|
|
406
|
+
<select id="cfgModel" onchange="updateSelectedNode()">
|
|
81
407
|
<option value="">default</option>
|
|
82
408
|
<option value="gpt-6-sol">GPT-6 Sol</option>
|
|
83
409
|
<option value="gpt-5.6-luna">GPT-5.6 Luna</option>
|
|
84
410
|
<option value="deepseek-v4-pro">DeepSeek V4 Pro</option>
|
|
411
|
+
<option value="glm-5.3">GLM 5.3</option>
|
|
85
412
|
</select>
|
|
413
|
+
<label>System Prompt</label>
|
|
414
|
+
<textarea id="cfgPrompt" oninput="updateSelectedNode()" placeholder="You are a helpful agent..."></textarea>
|
|
415
|
+
<label>Max Loops</label>
|
|
416
|
+
<input type="number" id="cfgMaxLoops" value="1" min="1" max="100" oninput="updateSelectedNode()" />
|
|
417
|
+
<label style="margin-top:12px">Global Max Loops</label>
|
|
418
|
+
<input type="number" id="cfgGlobalLoops" value="10" min="1" max="100" />
|
|
419
|
+
<div style="margin-top:12px">
|
|
420
|
+
<button class="btn danger" onclick="deleteSelectedNode()" style="width:100%">Delete Node</button>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</aside>
|
|
86
424
|
|
|
87
|
-
|
|
88
|
-
|
|
425
|
+
<!-- Canvas -->
|
|
426
|
+
<div class="canvas-wrap">
|
|
427
|
+
<div class="empty-state" id="emptyState">
|
|
428
|
+
<div class="big-icon">⚡</div>
|
|
429
|
+
<p>Drag an Agent node to start</p>
|
|
430
|
+
<span class="hint">or click "+ Agent" in the toolbar</span>
|
|
431
|
+
</div>
|
|
432
|
+
<div id="canvas"></div>
|
|
433
|
+
<svg id="arrows"></svg>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<!-- Right Panel: Run Log -->
|
|
437
|
+
<div class="panel">
|
|
438
|
+
<h3>Run Log</h3>
|
|
439
|
+
<div style="margin-bottom:8px">
|
|
440
|
+
<span style="color:var(--text-muted);font-size:11px">Iteration:</span>
|
|
441
|
+
<span class="iter-badge" id="iterBadge">0</span>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="run-log" id="runLog">
|
|
444
|
+
<div class="entry" style="color:var(--text-muted)">Ready.</div>
|
|
89
445
|
</div>
|
|
90
446
|
</div>
|
|
91
447
|
</div>
|
|
92
448
|
|
|
93
449
|
<script>
|
|
94
|
-
// ───
|
|
450
|
+
// ─── State ──────────────────────────────────────────
|
|
95
451
|
let ws = null;
|
|
96
|
-
let
|
|
97
|
-
let
|
|
452
|
+
let nodes = [];
|
|
453
|
+
let edges = [];
|
|
454
|
+
let selectedNodeId = null;
|
|
455
|
+
let dragging = null; // { nodeId, offsetX, offsetY }
|
|
456
|
+
let connecting = null; // { fromNodeId, startX, startY }
|
|
457
|
+
let nextId = 1;
|
|
98
458
|
|
|
459
|
+
// ─── WebSocket ──────────────────────────────────────
|
|
99
460
|
function connectWS() {
|
|
100
461
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
101
462
|
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
|
102
|
-
|
|
103
|
-
ws.
|
|
104
|
-
|
|
105
|
-
document.getElementById('status').classList.add('connected');
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
ws.onclose = () => {
|
|
109
|
-
document.getElementById('status').textContent = 'Disconnected';
|
|
110
|
-
document.getElementById('status').classList.remove('connected');
|
|
111
|
-
setTimeout(connectWS, 2000);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
ws.onmessage = (e) => {
|
|
115
|
-
const msg = JSON.parse(e.data);
|
|
116
|
-
handleMessage(msg);
|
|
117
|
-
};
|
|
463
|
+
ws.onopen = () => log('Connected to workflow server', 'success');
|
|
464
|
+
ws.onclose = () => { log('Disconnected, reconnecting…', 'error'); setTimeout(connectWS, 2000); };
|
|
465
|
+
ws.onmessage = (e) => handleMsg(JSON.parse(e.data));
|
|
118
466
|
}
|
|
119
|
-
|
|
120
467
|
function send(type, data = {}) {
|
|
121
|
-
if (!ws || ws.readyState !==
|
|
122
|
-
ws.send(JSON.stringify({ type, data, id: String(++
|
|
468
|
+
if (!ws || ws.readyState !== 1) return;
|
|
469
|
+
ws.send(JSON.stringify({ type, data, id: String(++nextId) }));
|
|
123
470
|
}
|
|
124
|
-
|
|
125
|
-
function handleMessage(msg) {
|
|
471
|
+
function handleMsg(msg) {
|
|
126
472
|
switch (msg.type) {
|
|
127
|
-
case 'connected':
|
|
128
|
-
console.log('Connected to workflow server', msg.data);
|
|
129
|
-
break;
|
|
130
|
-
case 'workflow:created':
|
|
131
|
-
currentWorkflow = msg.data;
|
|
132
|
-
console.log('Workflow created:', msg.data.id);
|
|
133
|
-
break;
|
|
134
473
|
case 'workflow:updated':
|
|
135
|
-
|
|
474
|
+
nodes = msg.data.nodes || nodes;
|
|
475
|
+
edges = msg.data.edges || edges;
|
|
476
|
+
render();
|
|
136
477
|
break;
|
|
137
478
|
case 'run:state':
|
|
138
|
-
|
|
479
|
+
updateRunState(msg.data);
|
|
139
480
|
break;
|
|
140
481
|
case 'run:nodeResult':
|
|
141
|
-
|
|
482
|
+
log(`✓ ${msg.data.nodeId}: ${String(msg.data.output).slice(0, 80)}…`, 'success');
|
|
142
483
|
break;
|
|
143
484
|
}
|
|
144
485
|
}
|
|
145
486
|
|
|
146
|
-
// ───
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
487
|
+
// ─── Node operations ────────────────────────────────
|
|
488
|
+
function addNode(type) {
|
|
489
|
+
const id = `n${Date.now()}`;
|
|
490
|
+
const cx = 200 + Math.random() * 300;
|
|
491
|
+
const cy = 100 + Math.random() * 200;
|
|
492
|
+
const node = {
|
|
493
|
+
id, type, label: `Agent ${nodes.length + 1}`,
|
|
494
|
+
config: { systemPrompt: '', model: '', maxLoops: 1 },
|
|
495
|
+
position: { x: cx, y: cy },
|
|
496
|
+
};
|
|
497
|
+
nodes.push(node);
|
|
498
|
+
send('node:add', { workflowId: currentWfId(), node });
|
|
499
|
+
selectedNodeId = id;
|
|
500
|
+
render();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function deleteSelectedNode() {
|
|
504
|
+
if (!selectedNodeId) return;
|
|
505
|
+
nodes = nodes.filter(n => n.id !== selectedNodeId);
|
|
506
|
+
edges = edges.filter(e => e.source !== selectedNodeId && e.target !== selectedNodeId);
|
|
507
|
+
send('node:remove', { workflowId: currentWfId(), nodeId: selectedNodeId });
|
|
508
|
+
selectedNodeId = null;
|
|
509
|
+
render();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function updateSelectedNode() {
|
|
513
|
+
const n = nodes.find(n => n.id === selectedNodeId);
|
|
514
|
+
if (!n) return;
|
|
515
|
+
n.label = document.getElementById('cfgLabel').value;
|
|
516
|
+
n.config.model = document.getElementById('cfgModel').value;
|
|
517
|
+
n.config.systemPrompt = document.getElementById('cfgPrompt').value;
|
|
518
|
+
n.config.maxLoops = parseInt(document.getElementById('cfgMaxLoops').value) || 1;
|
|
519
|
+
send('node:update', { workflowId: currentWfId(), node: n });
|
|
520
|
+
render();
|
|
152
521
|
}
|
|
153
522
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
523
|
+
function currentWfId() { return 'wf-current'; }
|
|
524
|
+
|
|
525
|
+
// ─── Rendering ──────────────────────────────────────
|
|
526
|
+
function render() {
|
|
527
|
+
const canvas = document.getElementById('canvas');
|
|
528
|
+
const empty = document.getElementById('emptyState');
|
|
529
|
+
empty.style.display = nodes.length ? 'none' : 'flex';
|
|
530
|
+
|
|
531
|
+
canvas.innerHTML = '';
|
|
532
|
+
for (const n of nodes) {
|
|
533
|
+
const el = document.createElement('div');
|
|
534
|
+
el.className = `node${n.id === selectedNodeId ? ' selected' : ''}${n._status || ''}`;
|
|
535
|
+
el.style.left = n.position.x + 'px';
|
|
536
|
+
el.style.top = n.position.y + 'px';
|
|
537
|
+
el.dataset.id = n.id;
|
|
538
|
+
el.innerHTML = `
|
|
539
|
+
<div class="port in" data-port="in" data-node="${n.id}"></div>
|
|
540
|
+
<div class="port out" data-port="out" data-node="${n.id}"></div>
|
|
541
|
+
<div class="node-header"><span class="dot"></span>${n.label}</div>
|
|
542
|
+
<div class="node-body">
|
|
543
|
+
${n.config.model ? `<span class="model-tag">${n.config.model}</span>` : '<span style="opacity:0.4">no model</span>'}
|
|
544
|
+
<div style="margin-top:4px">loops: ${n.config.maxLoops ?? 1}</div>
|
|
545
|
+
</div>`;
|
|
546
|
+
canvas.appendChild(el);
|
|
547
|
+
|
|
548
|
+
// Drag
|
|
549
|
+
el.addEventListener('mousedown', (e) => {
|
|
550
|
+
if (e.target.classList.contains('port')) return;
|
|
551
|
+
selectedNodeId = n.id;
|
|
552
|
+
dragging = { nodeId: n.id, offsetX: e.clientX - n.position.x, offsetY: e.clientY - n.position.y };
|
|
553
|
+
render();
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Select on click
|
|
558
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
559
|
+
const node = e.target.closest('.node');
|
|
560
|
+
if (node) {
|
|
561
|
+
selectedNodeId = node.dataset.id;
|
|
562
|
+
showConfig();
|
|
563
|
+
render();
|
|
564
|
+
}
|
|
166
565
|
});
|
|
566
|
+
|
|
567
|
+
drawArrows();
|
|
568
|
+
showConfig();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function showConfig() {
|
|
572
|
+
const panel = document.getElementById('nodeConfig');
|
|
573
|
+
const n = nodes.find(n => n.id === selectedNodeId);
|
|
574
|
+
if (!n) { panel.style.display = 'none'; return; }
|
|
575
|
+
panel.style.display = 'block';
|
|
576
|
+
document.getElementById('cfgLabel').value = n.label;
|
|
577
|
+
document.getElementById('cfgModel').value = n.config.model || '';
|
|
578
|
+
document.getElementById('cfgPrompt').value = n.config.systemPrompt || '';
|
|
579
|
+
document.getElementById('cfgMaxLoops').value = n.config.maxLoops || 1;
|
|
167
580
|
}
|
|
168
581
|
|
|
582
|
+
function drawArrows() {
|
|
583
|
+
const svg = document.getElementById('arrows');
|
|
584
|
+
svg.innerHTML = '';
|
|
585
|
+
for (const e of edges) {
|
|
586
|
+
const src = nodes.find(n => n.id === e.source);
|
|
587
|
+
const tgt = nodes.find(n => n.id === e.target);
|
|
588
|
+
if (!src || !tgt) continue;
|
|
589
|
+
const x1 = src.position.x + 180, y1 = src.position.y + 25;
|
|
590
|
+
const x2 = tgt.position.x, y2 = tgt.position.y + 25;
|
|
591
|
+
const mx = (x1 + x2) / 2;
|
|
592
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
593
|
+
path.setAttribute('d', `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`);
|
|
594
|
+
if (e._active) path.classList.add('active');
|
|
595
|
+
svg.appendChild(path);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ─── Drag ───────────────────────────────────────────
|
|
600
|
+
document.addEventListener('mousemove', (e) => {
|
|
601
|
+
if (dragging) {
|
|
602
|
+
const n = nodes.find(n => n.id === dragging.nodeId);
|
|
603
|
+
if (n) {
|
|
604
|
+
n.position.x = e.clientX - dragging.offsetX;
|
|
605
|
+
n.position.y = e.clientY - dragging.offsetY;
|
|
606
|
+
render();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
document.addEventListener('mouseup', () => { dragging = null; });
|
|
611
|
+
|
|
612
|
+
// ─── Port connections ───────────────────────────────
|
|
613
|
+
document.addEventListener('mousedown', (e) => {
|
|
614
|
+
const port = e.target.closest('.port');
|
|
615
|
+
if (!port) return;
|
|
616
|
+
e.stopPropagation();
|
|
617
|
+
if (port.dataset.port === 'out') {
|
|
618
|
+
connecting = { fromNodeId: port.dataset.node };
|
|
619
|
+
} else if (port.dataset.port === 'in' && connecting) {
|
|
620
|
+
const edgeId = `e${Date.now()}`;
|
|
621
|
+
edges.push({ id: edgeId, source: connecting.fromNodeId, target: port.dataset.node });
|
|
622
|
+
send('edge:add', { workflowId: currentWfId(), edge: edges[edges.length - 1] });
|
|
623
|
+
connecting = null;
|
|
624
|
+
render();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// ─── Run / Stop ─────────────────────────────────────
|
|
169
629
|
function runWorkflow() {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
630
|
+
// Ensure workflow exists
|
|
631
|
+
send('workflow:create', {
|
|
632
|
+
id: currentWfId(),
|
|
633
|
+
name: document.getElementById('wfName').value,
|
|
634
|
+
nodes, edges,
|
|
635
|
+
globalMaxLoops: parseInt(document.getElementById('cfgGlobalLoops').value) || 10,
|
|
175
636
|
});
|
|
176
637
|
setTimeout(() => {
|
|
177
|
-
send('workflow:run', { workflowId:
|
|
178
|
-
|
|
638
|
+
send('workflow:run', { workflowId: currentWfId() });
|
|
639
|
+
log('▶ Workflow started', 'success');
|
|
640
|
+
}, 100);
|
|
179
641
|
}
|
|
180
642
|
|
|
181
643
|
function stopWorkflow() {
|
|
182
|
-
|
|
183
|
-
|
|
644
|
+
send('workflow:stop', { workflowId: currentWfId() });
|
|
645
|
+
log('■ Stopped', 'error');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function clearAll() {
|
|
649
|
+
nodes = [];
|
|
650
|
+
edges = [];
|
|
651
|
+
selectedNodeId = null;
|
|
652
|
+
send('workflow:delete', { id: currentWfId() });
|
|
653
|
+
render();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function updateRunState(data) {
|
|
657
|
+
const state = data.state;
|
|
658
|
+
if (!state) return;
|
|
659
|
+
const pill = document.getElementById('statusP#ill');
|
|
660
|
+
pill.className = `status-pill ${state.status}`;
|
|
661
|
+
pill.textContent = state.status.toUpperCase();
|
|
662
|
+
document.getElementById('iterBadge').textContent = state.iteration;
|
|
663
|
+
|
|
664
|
+
// Update node visual states
|
|
665
|
+
for (const n of nodes) {
|
|
666
|
+
const ns = state.results?.[n.id];
|
|
667
|
+
n._status = ns ? ns.status : '';
|
|
668
|
+
}
|
|
669
|
+
render();
|
|
184
670
|
}
|
|
185
671
|
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
672
|
+
function log(text, cls = '') {
|
|
673
|
+
const el = document.getElementById('runLog');
|
|
674
|
+
const entry = document.createElement('div');
|
|
675
|
+
entry.className = `entry ${cls}`;
|
|
676
|
+
entry.textContent = text;
|
|
677
|
+
el.appendChild(entry);
|
|
678
|
+
el.scrollTop = el.scrollHeight;
|
|
190
679
|
}
|
|
191
680
|
|
|
192
|
-
// ───
|
|
681
|
+
// ─── Init ───────────────────────────────────────────
|
|
193
682
|
connectWS();
|
|
683
|
+
render();
|
|
194
684
|
</script>
|
|
195
685
|
</body>
|
|
196
686
|
</html>
|