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.
@@ -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 Editor</title>
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
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e0e0e0; }
10
- #app { display: flex; flex-direction: column; height: 100vh; }
11
-
12
- .header { padding: 12px 20px; background: #141414; border-bottom: 1px solid #262626; display: flex; align-items: center; gap: 12px; }
13
- .header h1 { font-size: 16px; font-weight: 600; }
14
- .header .status { margin-left: auto; font-size: 12px; color: #888; }
15
- .header .status.connected { color: #4ade80; }
16
-
17
- .toolbar { padding: 8px 20px; background: #1a1a1a; border-bottom: 1px solid #262626; display: flex; gap: 8px; }
18
- .toolbar button { padding: 6px 14px; border: 1px solid #333; border-radius: 6px; background: #222; color: #ccc; cursor: pointer; font-size: 13px; }
19
- .toolbar button:hover { background: #2a2a2a; border-color: #444; }
20
- .toolbar button.primary { background: #2563eb; border-color: #2563eb; color: #fff; }
21
- .toolbar button.primary:hover { background: #1d4ed8; }
22
- .toolbar button.danger { background: #dc2626; border-color: #dc2626; color: #fff; }
23
-
24
- .canvas-area { flex: 1; display: flex; align-items: center; justify-content: center; position: relative; }
25
- .placeholder { text-align: center; color: #555; }
26
- .placeholder h2 { font-size: 20px; margin-bottom: 8px; color: #888; }
27
- .placeholder p { font-size: 14px; }
28
-
29
- .panel { position: absolute; right: 16px; top: 16px; bottom: 16px; width: 300px; background: #141414; border: 1px solid #262626; border-radius: 8px; overflow: hidden; display: none; }
30
- .panel.open { display: flex; flex-direction: column; }
31
- .panel-header { padding: 12px 16px; border-bottom: 1px solid #262626; font-size: 14px; font-weight: 600; }
32
- .panel-body { flex: 1; overflow-y: auto; padding: 12px 16px; }
33
- .panel-body label { display: block; font-size: 12px; color: #888; margin-bottom: 4px; margin-top: 12px; }
34
- .panel-body input, .panel-body textarea, .panel-body select {
35
- width: 100%; padding: 6px 8px; background: #222; border: 1px solid #333; border-radius: 4px; color: #e0e0e0; font-size: 13px;
36
- }
37
- .panel-body textarea { min-height: 80px; resize: vertical; }
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
- <div class="header">
43
- <h1>⚡ Lyu-Code Workflow</h1>
44
- <span class="status" id="status">Connecting…</span>
45
- </div>
46
-
47
- <div class="toolbar">
48
- <button class="primary" onclick="createWorkflow()">+ New Workflow</button>
49
- <button onclick="addAgentNode()">Add Agent</button>
50
- <button class="primary" onclick="runWorkflow()">▶ Run</button>
51
- <button onclick="stopWorkflow()">■ Stop</button>
52
- <button class="danger" onclick="clearWorkflow()">✕ Clear</button>
53
- <span style="margin-left:auto;font-size:12px;color:#666;">
54
- Max Loops: <input type="number" id="maxLoops" value="10" min="1" max="100" style="width:60px;padding:2px 4px;background:#222;border:1px solid #333;border-radius:3px;color:#e0e0e0;font-size:12px;">
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
- <div class="canvas-area">
59
- <div class="placeholder">
60
- <h2>🎨 Workflow Canvas</h2>
61
- <p>Replace this placeholder with your React Flow frontend.<br>
62
- Put built files in <code>frontend/dist/</code></p>
63
- <p style="margin-top:16px;font-size:12px;color:#444;">
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
- <div class="panel" id="nodePanel">
71
- <div class="panel-header">Node Config</div>
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="nodeLabel" placeholder="Agent name">
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="nodeModel">
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
- <label>Max Loops (this node)</label>
88
- <input type="number" id="nodeMaxLoops" value="1" min="1" max="100">
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
- // ─── WebSocket 连接 ─────────────────────────────────
450
+ // ─── State ──────────────────────────────────────────
95
451
  let ws = null;
96
- let currentWorkflow = null;
97
- let msgId = 0;
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.onopen = () => {
104
- document.getElementById('status').textContent = 'Connected';
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 !== WebSocket.OPEN) return;
122
- ws.send(JSON.stringify({ type, data, id: String(++msgId) }));
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
- currentWorkflow = msg.data;
474
+ nodes = msg.data.nodes || nodes;
475
+ edges = msg.data.edges || edges;
476
+ render();
136
477
  break;
137
478
  case 'run:state':
138
- console.log('Run state:', msg.data);
479
+ updateRunState(msg.data);
139
480
  break;
140
481
  case 'run:nodeResult':
141
- console.log('Node result:', msg.data);
482
+ log(`✓ ${msg.data.nodeId}: ${String(msg.data.output).slice(0, 80)}…`, 'success');
142
483
  break;
143
484
  }
144
485
  }
145
486
 
146
- // ─── UI 操作 ─────────────────────────────────────────
147
- function createWorkflow() {
148
- send('workflow:create', {
149
- name: 'New Workflow',
150
- globalMaxLoops: parseInt(document.getElementById('maxLoops').value) || 10,
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 addAgentNode() {
155
- if (!currentWorkflow) { createWorkflow(); setTimeout(addAgentNode, 100); return; }
156
- const id = 'agent-' + Date.now();
157
- send('node:add', {
158
- workflowId: currentWorkflow.id,
159
- node: {
160
- id,
161
- type: 'agent',
162
- label: 'Agent ' + (currentWorkflow.nodes.length + 1),
163
- config: { systemPrompt: '', maxLoops: 1 },
164
- position: { x: 100 + currentWorkflow.nodes.length * 200, y: 200 },
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
- if (!currentWorkflow) return;
171
- // 先更新 globalMaxLoops
172
- send('workflow:update', {
173
- id: currentWorkflow.id,
174
- globalMaxLoops: parseInt(document.getElementById('maxLoops').value) || 10,
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: currentWorkflow.id });
178
- }, 50);
638
+ send('workflow:run', { workflowId: currentWfId() });
639
+ log('▶ Workflow started', 'success');
640
+ }, 100);
179
641
  }
180
642
 
181
643
  function stopWorkflow() {
182
- if (!currentWorkflow) return;
183
- send('workflow:stop', { workflowId: currentWorkflow.id });
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 clearWorkflow() {
187
- if (!currentWorkflow) return;
188
- send('workflow:delete', { id: currentWorkflow.id });
189
- currentWorkflow = null;
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lyu-extension-workflow",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Workflow extension for lyu-code — visually compose multi-agent pipelines",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",