minimal-workflow 0.2.0
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/index.html +12 -0
- package/package.json +44 -0
- package/public/manifest.fallback.json +482 -0
- package/scripts/cli.ts +318 -0
- package/scripts/server.ts +131 -0
- package/src/App.tsx +119 -0
- package/src/api/files.ts +32 -0
- package/src/api/manifest.ts +17 -0
- package/src/canvas/WorkflowCanvas.tsx +161 -0
- package/src/canvas/edges/SequentialEdge.tsx +14 -0
- package/src/canvas/nodes/AssertNode.tsx +17 -0
- package/src/canvas/nodes/BaseNode.tsx +55 -0
- package/src/canvas/nodes/BranchNode.tsx +21 -0
- package/src/canvas/nodes/LlmNode.tsx +29 -0
- package/src/canvas/nodes/LoopNode.tsx +22 -0
- package/src/canvas/nodes/PauseNode.tsx +17 -0
- package/src/canvas/nodes/SkillNode.tsx +27 -0
- package/src/canvas/nodes/ToolNode.tsx +27 -0
- package/src/index.css +379 -0
- package/src/inspector/Inspector.tsx +224 -0
- package/src/inspector/LlmNodeForm.tsx +59 -0
- package/src/inspector/SkillNodeForm.tsx +62 -0
- package/src/inspector/ToolNodeForm.tsx +120 -0
- package/src/main.tsx +10 -0
- package/src/palette/ControlFlowSection.tsx +45 -0
- package/src/palette/Palette.tsx +65 -0
- package/src/palette/SkillPaletteSection.tsx +31 -0
- package/src/palette/ToolPaletteSection.tsx +31 -0
- package/src/store/filesStore.ts +29 -0
- package/src/store/manifestStore.ts +25 -0
- package/src/store/workflowStore.ts +140 -0
- package/src/types.ts +83 -0
- package/src/utils/safePath.ts +49 -0
- package/src/yaml-view/YamlEditor.tsx +71 -0
- package/src/yaml-view/sync.ts +246 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +21 -0
package/src/index.css
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
html,
|
|
6
|
+
body,
|
|
7
|
+
#root {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
height: 100vh;
|
|
11
|
+
width: 100vw;
|
|
12
|
+
font-family:
|
|
13
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
14
|
+
Arial, sans-serif;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
color: #1f2937;
|
|
17
|
+
background: #f9fafb;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
button {
|
|
21
|
+
font: inherit;
|
|
22
|
+
color: inherit;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
input,
|
|
27
|
+
textarea,
|
|
28
|
+
select {
|
|
29
|
+
font: inherit;
|
|
30
|
+
color: inherit;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.app-shell {
|
|
34
|
+
display: grid;
|
|
35
|
+
grid-template-columns: 240px 1fr 320px;
|
|
36
|
+
grid-template-rows: 44px 1fr;
|
|
37
|
+
grid-template-areas:
|
|
38
|
+
'toolbar toolbar toolbar'
|
|
39
|
+
'palette canvas inspector';
|
|
40
|
+
height: 100vh;
|
|
41
|
+
width: 100vw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.toolbar {
|
|
45
|
+
grid-area: toolbar;
|
|
46
|
+
background: #1f2937;
|
|
47
|
+
color: #f3f4f6;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
padding: 0 16px;
|
|
51
|
+
gap: 12px;
|
|
52
|
+
font-size: 13px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.toolbar h1 {
|
|
56
|
+
margin: 0;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.toolbar .sep {
|
|
62
|
+
flex: 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.toolbar .file-label {
|
|
66
|
+
font-family:
|
|
67
|
+
'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
68
|
+
font-size: 12px;
|
|
69
|
+
color: #d1d5db;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.toolbar button {
|
|
73
|
+
background: #374151;
|
|
74
|
+
color: #f3f4f6;
|
|
75
|
+
border: 1px solid #4b5563;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
padding: 4px 10px;
|
|
78
|
+
font-size: 12px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.toolbar button:hover {
|
|
82
|
+
background: #4b5563;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.toolbar button.primary {
|
|
86
|
+
background: #2563eb;
|
|
87
|
+
border-color: #2563eb;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.toolbar button.primary:hover {
|
|
91
|
+
background: #1d4ed8;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.toolbar button:disabled {
|
|
95
|
+
background: #4b5563;
|
|
96
|
+
color: #9ca3af;
|
|
97
|
+
cursor: not-allowed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.palette {
|
|
101
|
+
grid-area: palette;
|
|
102
|
+
background: #ffffff;
|
|
103
|
+
border-right: 1px solid #e5e7eb;
|
|
104
|
+
overflow-y: auto;
|
|
105
|
+
padding: 8px 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.palette-section {
|
|
109
|
+
margin-bottom: 12px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.palette-section h2 {
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
text-transform: uppercase;
|
|
116
|
+
letter-spacing: 0.05em;
|
|
117
|
+
color: #6b7280;
|
|
118
|
+
padding: 6px 12px;
|
|
119
|
+
margin: 0;
|
|
120
|
+
background: #f9fafb;
|
|
121
|
+
border-top: 1px solid #e5e7eb;
|
|
122
|
+
border-bottom: 1px solid #e5e7eb;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.palette-item {
|
|
126
|
+
padding: 6px 12px;
|
|
127
|
+
cursor: grab;
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
border-bottom: 1px solid #f3f4f6;
|
|
130
|
+
user-select: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.palette-item:hover {
|
|
134
|
+
background: #eff6ff;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.palette-item:active {
|
|
138
|
+
cursor: grabbing;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.palette-item .name {
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
color: #1f2937;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.palette-item .desc {
|
|
147
|
+
color: #6b7280;
|
|
148
|
+
font-size: 11px;
|
|
149
|
+
margin-top: 2px;
|
|
150
|
+
line-height: 1.3;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
text-overflow: ellipsis;
|
|
153
|
+
display: -webkit-box;
|
|
154
|
+
-webkit-line-clamp: 2;
|
|
155
|
+
-webkit-box-orient: vertical;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.canvas {
|
|
159
|
+
grid-area: canvas;
|
|
160
|
+
position: relative;
|
|
161
|
+
background: #f3f4f6;
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.inspector {
|
|
166
|
+
grid-area: inspector;
|
|
167
|
+
background: #ffffff;
|
|
168
|
+
border-left: 1px solid #e5e7eb;
|
|
169
|
+
overflow-y: auto;
|
|
170
|
+
padding: 12px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.inspector h2 {
|
|
174
|
+
font-size: 13px;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
margin: 0 0 8px;
|
|
177
|
+
color: #1f2937;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.inspector .empty {
|
|
181
|
+
color: #9ca3af;
|
|
182
|
+
font-style: italic;
|
|
183
|
+
font-size: 12px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.field {
|
|
187
|
+
margin-bottom: 10px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.field label {
|
|
191
|
+
display: block;
|
|
192
|
+
font-size: 11px;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
color: #4b5563;
|
|
195
|
+
margin-bottom: 2px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.field input,
|
|
199
|
+
.field textarea,
|
|
200
|
+
.field select {
|
|
201
|
+
width: 100%;
|
|
202
|
+
padding: 4px 6px;
|
|
203
|
+
border: 1px solid #d1d5db;
|
|
204
|
+
border-radius: 3px;
|
|
205
|
+
font-size: 12px;
|
|
206
|
+
background: #ffffff;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.field textarea {
|
|
210
|
+
font-family:
|
|
211
|
+
'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
212
|
+
resize: vertical;
|
|
213
|
+
min-height: 60px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.field input:focus,
|
|
217
|
+
.field textarea:focus,
|
|
218
|
+
.field select:focus {
|
|
219
|
+
outline: none;
|
|
220
|
+
border-color: #2563eb;
|
|
221
|
+
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* canvas 节点样式 */
|
|
225
|
+
.canvas-node {
|
|
226
|
+
background: #ffffff;
|
|
227
|
+
border: 2px solid #d1d5db;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
padding: 8px 10px;
|
|
230
|
+
min-width: 180px;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.canvas-node.selected {
|
|
236
|
+
border-color: #2563eb;
|
|
237
|
+
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.canvas-node .node-header {
|
|
241
|
+
display: flex;
|
|
242
|
+
align-items: center;
|
|
243
|
+
gap: 6px;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
margin-bottom: 4px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.canvas-node .node-id {
|
|
249
|
+
font-family:
|
|
250
|
+
'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
251
|
+
font-size: 11px;
|
|
252
|
+
color: #6b7280;
|
|
253
|
+
margin-left: auto;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.canvas-node .node-body {
|
|
257
|
+
color: #4b5563;
|
|
258
|
+
font-size: 11px;
|
|
259
|
+
line-height: 1.4;
|
|
260
|
+
overflow: hidden;
|
|
261
|
+
text-overflow: ellipsis;
|
|
262
|
+
display: -webkit-box;
|
|
263
|
+
-webkit-line-clamp: 2;
|
|
264
|
+
-webkit-box-orient: vertical;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* 节点颜色编码 */
|
|
268
|
+
.canvas-node.tool {
|
|
269
|
+
border-color: #9ca3af;
|
|
270
|
+
}
|
|
271
|
+
.canvas-node.llm {
|
|
272
|
+
border-color: #3b82f6;
|
|
273
|
+
}
|
|
274
|
+
.canvas-node.skill {
|
|
275
|
+
border-color: #8b5cf6;
|
|
276
|
+
}
|
|
277
|
+
.canvas-node.assert {
|
|
278
|
+
border-color: #ef4444;
|
|
279
|
+
background: #fef2f2;
|
|
280
|
+
}
|
|
281
|
+
.canvas-node.pause {
|
|
282
|
+
border-color: #f59e0b;
|
|
283
|
+
background: #fffbeb;
|
|
284
|
+
}
|
|
285
|
+
.canvas-node.branch {
|
|
286
|
+
border-color: #10b981;
|
|
287
|
+
}
|
|
288
|
+
.canvas-node.loop {
|
|
289
|
+
border-color: #06b6d4;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* 文件列表 / 工作流列表(叠在 palette 上方) */
|
|
293
|
+
.file-list {
|
|
294
|
+
background: #f9fafb;
|
|
295
|
+
border-bottom: 1px solid #e5e7eb;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.file-list .file-item {
|
|
299
|
+
padding: 6px 12px;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
border-bottom: 1px solid #f3f4f6;
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: space-between;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.file-list .file-item:hover {
|
|
309
|
+
background: #eff6ff;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.file-list .file-item.active {
|
|
313
|
+
background: #dbeafe;
|
|
314
|
+
color: #1e40af;
|
|
315
|
+
font-weight: 500;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* yaml 视图 */
|
|
319
|
+
.yaml-view {
|
|
320
|
+
position: absolute;
|
|
321
|
+
inset: 0;
|
|
322
|
+
background: #1e293b;
|
|
323
|
+
color: #e2e8f0;
|
|
324
|
+
display: flex;
|
|
325
|
+
flex-direction: column;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.yaml-view textarea {
|
|
329
|
+
flex: 1;
|
|
330
|
+
background: #0f172a;
|
|
331
|
+
color: #e2e8f0;
|
|
332
|
+
border: none;
|
|
333
|
+
padding: 12px;
|
|
334
|
+
font-family:
|
|
335
|
+
'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
336
|
+
font-size: 13px;
|
|
337
|
+
line-height: 1.5;
|
|
338
|
+
resize: none;
|
|
339
|
+
outline: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.yaml-view .yaml-toolbar {
|
|
343
|
+
background: #334155;
|
|
344
|
+
padding: 6px 12px;
|
|
345
|
+
font-size: 11px;
|
|
346
|
+
display: flex;
|
|
347
|
+
gap: 8px;
|
|
348
|
+
align-items: center;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.yaml-view .yaml-error {
|
|
352
|
+
background: #7f1d1d;
|
|
353
|
+
color: #fee2e2;
|
|
354
|
+
padding: 8px 12px;
|
|
355
|
+
font-size: 12px;
|
|
356
|
+
font-family:
|
|
357
|
+
'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
358
|
+
white-space: pre-wrap;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* 节点角标 */
|
|
362
|
+
.canvas-node .badge {
|
|
363
|
+
display: inline-block;
|
|
364
|
+
font-size: 10px;
|
|
365
|
+
padding: 1px 5px;
|
|
366
|
+
border-radius: 8px;
|
|
367
|
+
background: #e5e7eb;
|
|
368
|
+
color: #374151;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.canvas-node .badge.warn {
|
|
372
|
+
background: #fef3c7;
|
|
373
|
+
color: #92400e;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.canvas-node .badge.err {
|
|
377
|
+
background: #fee2e2;
|
|
378
|
+
color: #991b1b;
|
|
379
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useWorkflowStore } from '../store/workflowStore.ts';
|
|
2
|
+
import { ToolNodeForm } from './ToolNodeForm.tsx';
|
|
3
|
+
import { LlmNodeForm } from './LlmNodeForm.tsx';
|
|
4
|
+
import { SkillNodeForm } from './SkillNodeForm.tsx';
|
|
5
|
+
import type { StepDef } from '../types.ts';
|
|
6
|
+
|
|
7
|
+
export function Inspector() {
|
|
8
|
+
const selectedId = useWorkflowStore((s) => s.selectedNodeId);
|
|
9
|
+
const nodes = useWorkflowStore((s) => s.nodes);
|
|
10
|
+
const updateStep = useWorkflowStore((s) => s.updateStep);
|
|
11
|
+
const removeStep = useWorkflowStore((s) => s.removeStep);
|
|
12
|
+
|
|
13
|
+
const node = nodes.find((n) => n.id === selectedId);
|
|
14
|
+
if (!node) {
|
|
15
|
+
return (
|
|
16
|
+
<aside className="inspector">
|
|
17
|
+
<h2>属性</h2>
|
|
18
|
+
<div className="empty">选中一个节点以编辑参数</div>
|
|
19
|
+
</aside>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const step = node.data.step;
|
|
23
|
+
const kind = node.data.kind;
|
|
24
|
+
const onPatch = (patch: Partial<StepDef>) => updateStep(node.id, patch);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<aside className="inspector">
|
|
28
|
+
<h2>
|
|
29
|
+
{kind} · {step.id}
|
|
30
|
+
</h2>
|
|
31
|
+
|
|
32
|
+
<CommonStepFields step={step} onPatch={onPatch} />
|
|
33
|
+
|
|
34
|
+
{kind === 'tool' && <ToolNodeForm step={step} onPatch={onPatch} />}
|
|
35
|
+
{kind === 'llm' && <LlmNodeForm step={step} onPatch={onPatch} />}
|
|
36
|
+
{kind === 'skill' && <SkillNodeForm step={step} onPatch={onPatch} />}
|
|
37
|
+
{kind === 'assert' && <AssertForm step={step} onPatch={onPatch} />}
|
|
38
|
+
{kind === 'pause' && <PauseForm step={step} onPatch={onPatch} />}
|
|
39
|
+
{kind === 'branch' && <BranchForm step={step} onPatch={onPatch} />}
|
|
40
|
+
{kind === 'loop' && <LoopForm step={step} onPatch={onPatch} />}
|
|
41
|
+
|
|
42
|
+
<CaptureField step={step} onPatch={onPatch} />
|
|
43
|
+
|
|
44
|
+
<div style={{ marginTop: 16 }}>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => {
|
|
47
|
+
if (confirm(`删除节点 ${step.id}?`)) removeStep(node.id);
|
|
48
|
+
}}
|
|
49
|
+
style={{
|
|
50
|
+
background: '#fef2f2',
|
|
51
|
+
color: '#dc2626',
|
|
52
|
+
border: '1px solid #fca5a5',
|
|
53
|
+
borderRadius: 3,
|
|
54
|
+
padding: '4px 10px',
|
|
55
|
+
fontSize: 12,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
删除节点
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</aside>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface FieldProps {
|
|
66
|
+
step: StepDef;
|
|
67
|
+
onPatch: (patch: Partial<StepDef>) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CommonStepFields({ step, onPatch }: FieldProps) {
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<div className="field">
|
|
74
|
+
<label>id</label>
|
|
75
|
+
<input
|
|
76
|
+
value={step.id}
|
|
77
|
+
onChange={(e) => onPatch({ id: e.target.value })}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="field">
|
|
81
|
+
<label>when(可选执行条件)</label>
|
|
82
|
+
<input
|
|
83
|
+
value={step.when ?? ''}
|
|
84
|
+
placeholder="${flag} == true"
|
|
85
|
+
onChange={(e) => onPatch({ when: e.target.value || undefined })}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="field">
|
|
89
|
+
<label>onError</label>
|
|
90
|
+
<select
|
|
91
|
+
value={step.onError ?? 'halt'}
|
|
92
|
+
onChange={(e) =>
|
|
93
|
+
onPatch({ onError: e.target.value as 'halt' | 'continue' })
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
<option value="halt">halt(默认)</option>
|
|
97
|
+
<option value="continue">continue</option>
|
|
98
|
+
</select>
|
|
99
|
+
</div>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function CaptureField({ step, onPatch }: FieldProps) {
|
|
105
|
+
const entries = Object.entries(step.capture ?? {});
|
|
106
|
+
const update = (idx: number, field: 'k' | 'v', val: string) => {
|
|
107
|
+
const next = entries.map((e, i) =>
|
|
108
|
+
i === idx ? ([field === 'k' ? val : e[0], field === 'v' ? val : e[1]] as [
|
|
109
|
+
string,
|
|
110
|
+
string,
|
|
111
|
+
]) : e,
|
|
112
|
+
);
|
|
113
|
+
onPatch({
|
|
114
|
+
capture: Object.fromEntries(next.filter((e) => e[0])),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const add = () =>
|
|
118
|
+
onPatch({ capture: { ...(step.capture ?? {}), '': '' } });
|
|
119
|
+
const remove = (idx: number) => {
|
|
120
|
+
const next = entries.filter((_, i) => i !== idx);
|
|
121
|
+
onPatch({ capture: Object.fromEntries(next) });
|
|
122
|
+
};
|
|
123
|
+
return (
|
|
124
|
+
<div className="field">
|
|
125
|
+
<label>capture(result 字段 → 变量名)</label>
|
|
126
|
+
{entries.map(([k, v], i) => (
|
|
127
|
+
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
|
128
|
+
<input
|
|
129
|
+
value={k}
|
|
130
|
+
placeholder="result"
|
|
131
|
+
onChange={(e) => update(i, 'k', e.target.value)}
|
|
132
|
+
/>
|
|
133
|
+
<span style={{ alignSelf: 'center' }}>→</span>
|
|
134
|
+
<input
|
|
135
|
+
value={v}
|
|
136
|
+
placeholder="var_name"
|
|
137
|
+
onChange={(e) => update(i, 'v', e.target.value)}
|
|
138
|
+
/>
|
|
139
|
+
<button onClick={() => remove(i)} style={{ fontSize: 10 }}>
|
|
140
|
+
×
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
<button onClick={add} style={{ fontSize: 11, padding: '2px 8px' }}>
|
|
145
|
+
+ 添加
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function AssertForm({ step, onPatch }: FieldProps) {
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<div className="field">
|
|
155
|
+
<label>condition</label>
|
|
156
|
+
<input
|
|
157
|
+
value={step.condition ?? ''}
|
|
158
|
+
onChange={(e) => onPatch({ condition: e.target.value })}
|
|
159
|
+
placeholder='fileExists("a.txt")'
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="field">
|
|
163
|
+
<label>onFail</label>
|
|
164
|
+
<input
|
|
165
|
+
value={step.onFail ?? ''}
|
|
166
|
+
onChange={(e) => onPatch({ onFail: e.target.value })}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function PauseForm({ step, onPatch }: FieldProps) {
|
|
174
|
+
return (
|
|
175
|
+
<div className="field">
|
|
176
|
+
<label>prompt(暂停理由,渲染给用户)</label>
|
|
177
|
+
<textarea
|
|
178
|
+
value={step.prompt ?? ''}
|
|
179
|
+
onChange={(e) => onPatch({ prompt: e.target.value })}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function BranchForm({ step, onPatch }: FieldProps) {
|
|
186
|
+
return (
|
|
187
|
+
<div className="field">
|
|
188
|
+
<label>condition</label>
|
|
189
|
+
<input
|
|
190
|
+
value={step.condition ?? ''}
|
|
191
|
+
onChange={(e) => onPatch({ condition: e.target.value })}
|
|
192
|
+
placeholder="${flag}"
|
|
193
|
+
/>
|
|
194
|
+
<small style={{ color: '#6b7280', fontSize: 11 }}>
|
|
195
|
+
then / else 子步骤在 yaml 视图里编辑
|
|
196
|
+
</small>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function LoopForm({ step, onPatch }: FieldProps) {
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
<div className="field">
|
|
205
|
+
<label>over(数组表达式)</label>
|
|
206
|
+
<input
|
|
207
|
+
value={step.over ?? ''}
|
|
208
|
+
onChange={(e) => onPatch({ over: e.target.value })}
|
|
209
|
+
placeholder="${items}"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="field">
|
|
213
|
+
<label>as(迭代变量名)</label>
|
|
214
|
+
<input
|
|
215
|
+
value={step.as ?? 'item'}
|
|
216
|
+
onChange={(e) => onPatch({ as: e.target.value })}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
<small style={{ color: '#6b7280', fontSize: 11 }}>
|
|
220
|
+
do 子步骤在 yaml 视图里编辑
|
|
221
|
+
</small>
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { StepDef } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
step: StepDef;
|
|
5
|
+
onPatch: (patch: Partial<StepDef>) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* llm 字段两种形态:
|
|
10
|
+
* - string:直接是 prompt 文本
|
|
11
|
+
* - object:{ prompt, model?, temperature?, ... } 高级形态
|
|
12
|
+
*
|
|
13
|
+
* MVP 阶段只编辑 prompt 文本;其它高级字段在 yaml 视图里改。
|
|
14
|
+
*/
|
|
15
|
+
export function LlmNodeForm({ step, onPatch }: Props) {
|
|
16
|
+
const prompt = extractPrompt(step.llm);
|
|
17
|
+
const isObjectForm = step.llm !== null && typeof step.llm === 'object';
|
|
18
|
+
|
|
19
|
+
const onPromptChange = (text: string) => {
|
|
20
|
+
if (isObjectForm) {
|
|
21
|
+
onPatch({
|
|
22
|
+
llm: { ...(step.llm as Record<string, unknown>), prompt: text },
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
onPatch({ llm: text });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<div className="field">
|
|
32
|
+
<label>llm prompt</label>
|
|
33
|
+
<textarea
|
|
34
|
+
value={prompt}
|
|
35
|
+
rows={8}
|
|
36
|
+
spellCheck={false}
|
|
37
|
+
placeholder="请用一句话评价 ${book}"
|
|
38
|
+
onChange={(e) => onPromptChange(e.target.value)}
|
|
39
|
+
style={{ fontFamily: 'ui-monospace, monospace', fontSize: 12 }}
|
|
40
|
+
/>
|
|
41
|
+
<small style={{ color: '#6b7280', fontSize: 11 }}>
|
|
42
|
+
支持 ${'{var}'} 模板;
|
|
43
|
+
{isObjectForm
|
|
44
|
+
? '对象形态 — model / temperature 等字段请在 YAML 视图编辑'
|
|
45
|
+
: '默认走主 provider'}
|
|
46
|
+
</small>
|
|
47
|
+
</div>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractPrompt(llm: StepDef['llm']): string {
|
|
53
|
+
if (typeof llm === 'string') return llm;
|
|
54
|
+
if (llm && typeof llm === 'object') {
|
|
55
|
+
const p = (llm as Record<string, unknown>).prompt;
|
|
56
|
+
if (typeof p === 'string') return p;
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useManifestStore } from '../store/manifestStore.ts';
|
|
2
|
+
import type { StepDef } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
step: StepDef;
|
|
6
|
+
onPatch: (patch: Partial<StepDef>) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SkillNodeForm({ step, onPatch }: Props) {
|
|
10
|
+
const manifest = useManifestStore((s) => s.manifest);
|
|
11
|
+
const skills = manifest?.skills ?? [];
|
|
12
|
+
const current = skills.find((s) => s.name === step.skill);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<div className="field">
|
|
17
|
+
<label>skill(技能名)</label>
|
|
18
|
+
<select
|
|
19
|
+
value={step.skill ?? ''}
|
|
20
|
+
onChange={(e) => onPatch({ skill: e.target.value })}
|
|
21
|
+
>
|
|
22
|
+
<option value="">— 选择技能 —</option>
|
|
23
|
+
{skills.map((s) => (
|
|
24
|
+
<option key={s.name} value={s.name}>
|
|
25
|
+
{s.name}
|
|
26
|
+
</option>
|
|
27
|
+
))}
|
|
28
|
+
</select>
|
|
29
|
+
{current && (
|
|
30
|
+
<small style={{ color: '#6b7280', fontSize: 11 }}>
|
|
31
|
+
{current.description}
|
|
32
|
+
</small>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="field">
|
|
37
|
+
<label>input(喂给 skill 的初始消息)</label>
|
|
38
|
+
<textarea
|
|
39
|
+
value={step.input ?? ''}
|
|
40
|
+
rows={4}
|
|
41
|
+
spellCheck={false}
|
|
42
|
+
placeholder="支持 ${var} 模板"
|
|
43
|
+
onChange={(e) => onPatch({ input: e.target.value })}
|
|
44
|
+
style={{ fontFamily: 'ui-monospace, monospace', fontSize: 12 }}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="field">
|
|
49
|
+
<label>maxTurns(最多自主轮数,留空 = 默认)</label>
|
|
50
|
+
<input
|
|
51
|
+
type="number"
|
|
52
|
+
min={1}
|
|
53
|
+
value={step.maxTurns ?? ''}
|
|
54
|
+
onChange={(e) => {
|
|
55
|
+
const v = e.target.value;
|
|
56
|
+
onPatch({ maxTurns: v === '' ? undefined : Number(v) });
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|