@steerable/agent-shell-web 0.6.49 → 0.6.51
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/package.json +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +1 -1
- package/src/components/chat/LocalChatPanel.tsx +38 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +474 -0
- package/src/components/chat/SessionOrchestrationFlow.tsx +526 -0
- package/src/components/chat/orchestration-flow-model.ts +336 -0
- package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
- package/src/components/settings/SettingsNavMenu.tsx +55 -0
- package/src/lib/orchestration-settings.ts +58 -0
- package/src/pages/AgentPage.tsx +2 -0
- package/src/pages/SettingsPage.tsx +208 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"react-router-dom": "^6.30.0",
|
|
46
46
|
"rehype-raw": "^7.0.0",
|
|
47
47
|
"remark-gfm": "^4.0.1",
|
|
48
|
-
"@steerable/agent-protocol": "0.6.
|
|
49
|
-
"@steerable/agent-ui": "0.6.
|
|
48
|
+
"@steerable/agent-protocol": "0.6.51",
|
|
49
|
+
"@steerable/agent-ui": "0.6.51"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"typescript": "^5.8.3",
|
|
60
60
|
"vite": "^5.4.21",
|
|
61
61
|
"vitest": "^3.2.4",
|
|
62
|
-
"@steerable/pack-sdk": "0.6.
|
|
62
|
+
"@steerable/pack-sdk": "0.6.51"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"gen": "echo agent-shell-web",
|
|
@@ -61,7 +61,7 @@ export interface ExecutedAction {
|
|
|
61
61
|
sandbox?: { backend?: string; enforcement: string };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
interface ExecutedActionsCardProps {
|
|
64
|
+
export interface ExecutedActionsCardProps {
|
|
65
65
|
actions: ExecutedAction[];
|
|
66
66
|
/** Drop the outer card chrome when nested in a turn-process group. */
|
|
67
67
|
compact?: boolean;
|
|
@@ -20,6 +20,8 @@ import type { TurnFile } from './turn-files';
|
|
|
20
20
|
import type { LlmSpeedSnapshot } from './process-status';
|
|
21
21
|
import { SessionTodoList } from './SessionTodoList';
|
|
22
22
|
import { resolveLatestSessionTodos } from './todo-list-model';
|
|
23
|
+
import { SessionOrchestrationFlow } from './SessionOrchestrationFlow';
|
|
24
|
+
import { resolveLatestSessionOrchestrationFlow } from './orchestration-flow-model';
|
|
23
25
|
import {
|
|
24
26
|
appendAttachmentRefs,
|
|
25
27
|
collectImageAttachments,
|
|
@@ -327,6 +329,28 @@ export function LocalChatPanel({
|
|
|
327
329
|
],
|
|
328
330
|
);
|
|
329
331
|
|
|
332
|
+
const sessionOrchestrationFlow = useMemo(
|
|
333
|
+
() =>
|
|
334
|
+
resolveLatestSessionOrchestrationFlow({
|
|
335
|
+
messages,
|
|
336
|
+
executedActionsByMessageId,
|
|
337
|
+
currentTurnActions,
|
|
338
|
+
currentTurnChildren,
|
|
339
|
+
orchestrationChildrenByMessageId,
|
|
340
|
+
timelineByMessageId,
|
|
341
|
+
currentTurnTimeline,
|
|
342
|
+
}),
|
|
343
|
+
[
|
|
344
|
+
messages,
|
|
345
|
+
executedActionsByMessageId,
|
|
346
|
+
currentTurnActions,
|
|
347
|
+
currentTurnChildren,
|
|
348
|
+
orchestrationChildrenByMessageId,
|
|
349
|
+
timelineByMessageId,
|
|
350
|
+
currentTurnTimeline,
|
|
351
|
+
],
|
|
352
|
+
);
|
|
353
|
+
|
|
330
354
|
// Sync `chat-input-box-width` CSS var so bubbles match the input width
|
|
331
355
|
// (mirrors the rendered input box width so message bubbles + input visually align in a column).
|
|
332
356
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -390,7 +414,20 @@ export function LocalChatPanel({
|
|
|
390
414
|
onOpenSettings={onOpenSettings}
|
|
391
415
|
toolbarExtras={inputToolbarExtras}
|
|
392
416
|
leadingChrome={inputLeadingChrome}
|
|
393
|
-
trailingChrome={
|
|
417
|
+
trailingChrome={
|
|
418
|
+
sessionOrchestrationFlow || sessionTodos ? (
|
|
419
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
420
|
+
{sessionOrchestrationFlow ? (
|
|
421
|
+
<SessionOrchestrationFlow
|
|
422
|
+
flow={sessionOrchestrationFlow}
|
|
423
|
+
chatId={chatId}
|
|
424
|
+
onInspectTask={onInspectTask}
|
|
425
|
+
/>
|
|
426
|
+
) : null}
|
|
427
|
+
{sessionTodos ? <SessionTodoList todos={sessionTodos} /> : null}
|
|
428
|
+
</div>
|
|
429
|
+
) : undefined
|
|
430
|
+
}
|
|
394
431
|
mode={mode}
|
|
395
432
|
onModeChange={onModeChange}
|
|
396
433
|
execPolicy={execPolicy}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LuCircle,
|
|
4
|
+
LuCircleCheck,
|
|
5
|
+
LuCircleDot,
|
|
6
|
+
LuCirclePause,
|
|
7
|
+
LuCircleX,
|
|
8
|
+
LuChevronDown,
|
|
9
|
+
LuChevronRight,
|
|
10
|
+
LuGitFork,
|
|
11
|
+
LuLoaderCircle,
|
|
12
|
+
LuBot,
|
|
13
|
+
LuCheck,
|
|
14
|
+
LuMessageSquare,
|
|
15
|
+
LuExternalLink,
|
|
16
|
+
} from 'react-icons/lu';
|
|
17
|
+
import {
|
|
18
|
+
type OrchestrationFlowData,
|
|
19
|
+
type OrchestrationNodeStatus,
|
|
20
|
+
} from './orchestration-flow-model';
|
|
21
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
22
|
+
|
|
23
|
+
function NodeStatusIcon({
|
|
24
|
+
status,
|
|
25
|
+
className = '',
|
|
26
|
+
}: {
|
|
27
|
+
status: OrchestrationNodeStatus;
|
|
28
|
+
className?: string;
|
|
29
|
+
}) {
|
|
30
|
+
const cls = `h-3.5 w-3.5 shrink-0 ${className}`.trim();
|
|
31
|
+
switch (status) {
|
|
32
|
+
case 'completed':
|
|
33
|
+
return <LuCircleCheck className={`${cls} text-emerald-600 dark:text-emerald-400`} />;
|
|
34
|
+
case 'running':
|
|
35
|
+
return <LuCircleDot className={`${cls} animate-pulse text-blue-600 dark:text-blue-400`} />;
|
|
36
|
+
case 'interrupted':
|
|
37
|
+
return <LuCirclePause className={`${cls} text-amber-600 dark:text-amber-400`} />;
|
|
38
|
+
case 'closed':
|
|
39
|
+
return <LuCircleX className={`${cls} text-agent-muted-foreground`} />;
|
|
40
|
+
case 'failed':
|
|
41
|
+
return <LuCircleX className={`${cls} text-agent-destructive`} />;
|
|
42
|
+
case 'pending':
|
|
43
|
+
default:
|
|
44
|
+
return <LuCircle className={`${cls} text-agent-muted-foreground/60`} />;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function statusBadgeText(status: OrchestrationNodeStatus): string {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 'completed':
|
|
51
|
+
return '已完成';
|
|
52
|
+
case 'running':
|
|
53
|
+
return '执行中';
|
|
54
|
+
case 'interrupted':
|
|
55
|
+
return '已暂停';
|
|
56
|
+
case 'closed':
|
|
57
|
+
return '已关闭';
|
|
58
|
+
case 'failed':
|
|
59
|
+
return '失败';
|
|
60
|
+
case 'pending':
|
|
61
|
+
default:
|
|
62
|
+
return '等待中';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function statusBadgeStyle(status: OrchestrationNodeStatus): string {
|
|
67
|
+
switch (status) {
|
|
68
|
+
case 'completed':
|
|
69
|
+
return 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border-emerald-500/20';
|
|
70
|
+
case 'running':
|
|
71
|
+
return 'bg-blue-500/10 text-blue-700 dark:text-blue-300 border-blue-500/20';
|
|
72
|
+
case 'interrupted':
|
|
73
|
+
return 'bg-amber-500/10 text-amber-700 dark:text-amber-300 border-amber-500/20';
|
|
74
|
+
case 'closed':
|
|
75
|
+
return 'bg-agent-muted text-agent-muted-foreground border-agent-border';
|
|
76
|
+
case 'failed':
|
|
77
|
+
return 'bg-agent-destructive/10 text-agent-destructive border-agent-destructive/20';
|
|
78
|
+
case 'pending':
|
|
79
|
+
default:
|
|
80
|
+
return 'bg-agent-muted text-agent-muted-foreground border-agent-border';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ForkLines({ count }: { count: number }) {
|
|
85
|
+
if (count <= 1) {
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex h-5 w-full items-center justify-center">
|
|
88
|
+
<svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
|
|
89
|
+
<line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
|
|
90
|
+
<polygon points="5,13 8,19 11,13" fill="currentColor" />
|
|
91
|
+
</svg>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (count === 2) {
|
|
97
|
+
return (
|
|
98
|
+
<div className="relative h-6 w-full">
|
|
99
|
+
<svg
|
|
100
|
+
className="absolute inset-0 h-full w-full text-violet-500/70"
|
|
101
|
+
viewBox="0 0 100 24"
|
|
102
|
+
preserveAspectRatio="none"
|
|
103
|
+
fill="none"
|
|
104
|
+
>
|
|
105
|
+
<path
|
|
106
|
+
d="M 50 0 L 50 12 M 25 12 L 75 12 M 25 12 L 25 18 M 75 12 L 75 18"
|
|
107
|
+
stroke="currentColor"
|
|
108
|
+
strokeWidth="1.5"
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
vectorEffect="non-scaling-stroke"
|
|
112
|
+
/>
|
|
113
|
+
</svg>
|
|
114
|
+
<div className="absolute top-[17px] left-[25%] -translate-x-1/2">
|
|
115
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
116
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
117
|
+
</svg>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="absolute top-[17px] left-[75%] -translate-x-1/2">
|
|
120
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
121
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (count === 3) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="relative h-6 w-full">
|
|
131
|
+
<svg
|
|
132
|
+
className="absolute inset-0 h-full w-full text-violet-500/70"
|
|
133
|
+
viewBox="0 0 100 24"
|
|
134
|
+
preserveAspectRatio="none"
|
|
135
|
+
fill="none"
|
|
136
|
+
>
|
|
137
|
+
<path
|
|
138
|
+
d="M 50 0 L 50 18 M 17 12 L 83 12 M 17 12 L 17 18 M 83 12 L 83 18"
|
|
139
|
+
stroke="currentColor"
|
|
140
|
+
strokeWidth="1.5"
|
|
141
|
+
strokeLinecap="round"
|
|
142
|
+
strokeLinejoin="round"
|
|
143
|
+
vectorEffect="non-scaling-stroke"
|
|
144
|
+
/>
|
|
145
|
+
</svg>
|
|
146
|
+
<div className="absolute top-[17px] left-[17%] -translate-x-1/2">
|
|
147
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
148
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
149
|
+
</svg>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="absolute top-[17px] left-[50%] -translate-x-1/2">
|
|
152
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
153
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
154
|
+
</svg>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="absolute top-[17px] left-[83%] -translate-x-1/2">
|
|
157
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
158
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
159
|
+
</svg>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex h-5 w-full items-center justify-center">
|
|
167
|
+
<svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
|
|
168
|
+
<line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
|
|
169
|
+
<polygon points="5,13 8,19 11,13" fill="currentColor" />
|
|
170
|
+
</svg>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function JoinLines({ count }: { count: number }) {
|
|
176
|
+
if (count <= 1) {
|
|
177
|
+
return (
|
|
178
|
+
<div className="flex h-5 w-full items-center justify-center">
|
|
179
|
+
<svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
|
|
180
|
+
<line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
|
|
181
|
+
<polygon points="5,13 8,19 11,13" fill="currentColor" />
|
|
182
|
+
</svg>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (count === 2) {
|
|
188
|
+
return (
|
|
189
|
+
<div className="relative h-6 w-full">
|
|
190
|
+
<svg
|
|
191
|
+
className="absolute inset-0 h-full w-full text-violet-500/70"
|
|
192
|
+
viewBox="0 0 100 24"
|
|
193
|
+
preserveAspectRatio="none"
|
|
194
|
+
fill="none"
|
|
195
|
+
>
|
|
196
|
+
<path
|
|
197
|
+
d="M 25 0 L 25 12 M 75 0 L 75 12 M 25 12 L 75 12 M 50 12 L 50 18"
|
|
198
|
+
stroke="currentColor"
|
|
199
|
+
strokeWidth="1.5"
|
|
200
|
+
strokeLinecap="round"
|
|
201
|
+
strokeLinejoin="round"
|
|
202
|
+
vectorEffect="non-scaling-stroke"
|
|
203
|
+
/>
|
|
204
|
+
</svg>
|
|
205
|
+
<div className="absolute top-[17px] left-[50%] -translate-x-1/2">
|
|
206
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
207
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
208
|
+
</svg>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (count === 3) {
|
|
215
|
+
return (
|
|
216
|
+
<div className="relative h-6 w-full">
|
|
217
|
+
<svg
|
|
218
|
+
className="absolute inset-0 h-full w-full text-violet-500/70"
|
|
219
|
+
viewBox="0 0 100 24"
|
|
220
|
+
preserveAspectRatio="none"
|
|
221
|
+
fill="none"
|
|
222
|
+
>
|
|
223
|
+
<path
|
|
224
|
+
d="M 17 0 L 17 12 M 50 0 L 50 18 M 83 0 L 83 12 M 17 12 L 83 12 M 50 12 L 50 18"
|
|
225
|
+
stroke="currentColor"
|
|
226
|
+
strokeWidth="1.5"
|
|
227
|
+
strokeLinecap="round"
|
|
228
|
+
strokeLinejoin="round"
|
|
229
|
+
vectorEffect="non-scaling-stroke"
|
|
230
|
+
/>
|
|
231
|
+
</svg>
|
|
232
|
+
<div className="absolute top-[17px] left-[50%] -translate-x-1/2">
|
|
233
|
+
<svg className="h-2 w-2.5 text-violet-500/70" viewBox="0 0 10 8" fill="none">
|
|
234
|
+
<polygon points="1,1 5,7 9,1" fill="currentColor" />
|
|
235
|
+
</svg>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className="flex h-5 w-full items-center justify-center">
|
|
243
|
+
<svg className="h-5 w-4 text-violet-500/70" viewBox="0 0 16 20" fill="none">
|
|
244
|
+
<line x1="8" y1="0" x2="8" y2="14" stroke="currentColor" strokeWidth="1.5" />
|
|
245
|
+
<polygon points="5,13 8,19 11,13" fill="currentColor" />
|
|
246
|
+
</svg>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function OrchestrationFlowCard({
|
|
252
|
+
flow,
|
|
253
|
+
chatId,
|
|
254
|
+
onInspectTask,
|
|
255
|
+
className = '',
|
|
256
|
+
defaultExpanded = false,
|
|
257
|
+
}: {
|
|
258
|
+
flow: OrchestrationFlowData;
|
|
259
|
+
chatId?: string | null;
|
|
260
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
261
|
+
className?: string;
|
|
262
|
+
defaultExpanded?: boolean;
|
|
263
|
+
}) {
|
|
264
|
+
const { nodes, isAllCompleted, hasActive, summaryCopy } = flow;
|
|
265
|
+
|
|
266
|
+
const [userToggled, setUserToggled] = useState<boolean | null>(defaultExpanded ? true : null);
|
|
267
|
+
const expanded = userToggled ?? defaultExpanded;
|
|
268
|
+
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (isAllCompleted && defaultExpanded) {
|
|
271
|
+
setUserToggled(false);
|
|
272
|
+
}
|
|
273
|
+
}, [isAllCompleted, defaultExpanded]);
|
|
274
|
+
|
|
275
|
+
const gridColsClass =
|
|
276
|
+
nodes.length === 2
|
|
277
|
+
? 'grid-cols-2'
|
|
278
|
+
: nodes.length === 3
|
|
279
|
+
? 'grid-cols-3'
|
|
280
|
+
: 'grid-cols-1 sm:grid-cols-2';
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div
|
|
284
|
+
className={`my-2 overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xs ${className}`}
|
|
285
|
+
data-testid="orchestration-flow-card"
|
|
286
|
+
>
|
|
287
|
+
{/* 顶部 Todo 风格状态胶囊 Header */}
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={() => setUserToggled((prev) => (prev === null ? !expanded : !prev))}
|
|
291
|
+
className="flex w-full items-center justify-between border-b border-agent-border bg-agent-muted/30 px-3 py-2 text-left transition-colors hover:bg-agent-muted/50"
|
|
292
|
+
aria-expanded={expanded}
|
|
293
|
+
>
|
|
294
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
295
|
+
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-violet-500/10 text-violet-600 dark:text-violet-400">
|
|
296
|
+
<LuGitFork className="h-3 w-3" />
|
|
297
|
+
</div>
|
|
298
|
+
<div className="flex min-w-0 items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
299
|
+
<span>多智能体协同编排</span>
|
|
300
|
+
<span className="rounded-full bg-violet-500/10 px-1.5 py-0.2 text-[10px] font-medium text-violet-700 dark:text-violet-300">
|
|
301
|
+
Fork-Join 流程
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div className="flex items-center gap-2 text-xs">
|
|
307
|
+
<span
|
|
308
|
+
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors ${
|
|
309
|
+
isAllCompleted
|
|
310
|
+
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
|
311
|
+
: hasActive
|
|
312
|
+
? 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-300'
|
|
313
|
+
: 'border-agent-border bg-agent-muted text-agent-muted-foreground'
|
|
314
|
+
}`}
|
|
315
|
+
>
|
|
316
|
+
{isAllCompleted ? (
|
|
317
|
+
<LuCircleCheck className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
|
|
318
|
+
) : hasActive ? (
|
|
319
|
+
<LuLoaderCircle className="h-3 w-3 animate-spin text-blue-600 dark:text-blue-400" />
|
|
320
|
+
) : (
|
|
321
|
+
<LuCheck className="h-3 w-3" />
|
|
322
|
+
)}
|
|
323
|
+
<span>{summaryCopy}</span>
|
|
324
|
+
</span>
|
|
325
|
+
{expanded ? (
|
|
326
|
+
<LuChevronDown className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
327
|
+
) : (
|
|
328
|
+
<LuChevronRight className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</button>
|
|
332
|
+
|
|
333
|
+
{/* 展开后的流程图 (真正的 Fork-Join DAG 图形) */}
|
|
334
|
+
{expanded ? (
|
|
335
|
+
<div className="p-3">
|
|
336
|
+
<div className="flex flex-col items-center">
|
|
337
|
+
{/* 1. 起点:目标分发 (Fork) 居中胶囊节点 */}
|
|
338
|
+
<div className="inline-flex items-center gap-1.5 rounded-full border border-violet-500/30 bg-violet-500/10 px-3 py-1 text-xs font-semibold text-violet-800 dark:text-violet-200 shadow-2xs">
|
|
339
|
+
<LuBot className="h-3.5 w-3.5 text-violet-600 dark:text-violet-400" />
|
|
340
|
+
<span>主智能体目标分发 (Fork)</span>
|
|
341
|
+
<span className="rounded-full bg-violet-500/20 px-1.5 py-0.2 text-[10px] font-medium text-violet-700 dark:text-violet-300">
|
|
342
|
+
并发 {nodes.length} 分支
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* 2. 向下的分流连线 */}
|
|
347
|
+
<ForkLines count={nodes.length} />
|
|
348
|
+
|
|
349
|
+
{/* 3. 并行分支节点网格 */}
|
|
350
|
+
<div className={`grid w-full gap-2.5 ${gridColsClass}`}>
|
|
351
|
+
{nodes.map((node, index) => {
|
|
352
|
+
const done = node.status === 'completed';
|
|
353
|
+
const active = node.status === 'running' || node.status === 'pending';
|
|
354
|
+
const canInspect = Boolean(chatId && onInspectTask && node.recordId);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div
|
|
358
|
+
key={node.childId || `node-${index}`}
|
|
359
|
+
className="flex flex-col justify-between rounded-agent-md border border-agent-border/80 bg-agent-muted/20 p-2.5 text-xs shadow-2xs transition-all hover:border-agent-border hover:bg-agent-muted/30"
|
|
360
|
+
>
|
|
361
|
+
<div>
|
|
362
|
+
{/* 节点顶栏:状态与编号 */}
|
|
363
|
+
<div className="flex items-center justify-between gap-1 border-b border-agent-border/40 pb-1.5">
|
|
364
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
365
|
+
<NodeStatusIcon status={node.status} />
|
|
366
|
+
<span className="font-semibold text-agent-foreground text-[11px] truncate">
|
|
367
|
+
分支 #{index + 1}
|
|
368
|
+
</span>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="flex items-center gap-1">
|
|
371
|
+
<span
|
|
372
|
+
className={`rounded-full border px-1.5 py-0.2 text-[10px] font-medium ${statusBadgeStyle(
|
|
373
|
+
node.status,
|
|
374
|
+
)}`}
|
|
375
|
+
>
|
|
376
|
+
{statusBadgeText(node.status)}
|
|
377
|
+
</span>
|
|
378
|
+
{canInspect && chatId ? (
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
onClick={() =>
|
|
382
|
+
onInspectTask?.({
|
|
383
|
+
id: node.childId,
|
|
384
|
+
chatId,
|
|
385
|
+
recordId: node.recordId,
|
|
386
|
+
live: node.status === 'running',
|
|
387
|
+
title: node.task || '子代理执行过程',
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
className="inline-flex shrink-0 items-center gap-0.5 rounded px-1 text-[10px] text-agent-muted-foreground hover:bg-agent-muted hover:text-agent-foreground"
|
|
391
|
+
title="查看推理过程"
|
|
392
|
+
>
|
|
393
|
+
<LuExternalLink className="h-2.5 w-2.5" />
|
|
394
|
+
<span>过程</span>
|
|
395
|
+
</button>
|
|
396
|
+
) : null}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{/* 任务描述 */}
|
|
401
|
+
<div className="mt-1.5 leading-relaxed">
|
|
402
|
+
<p
|
|
403
|
+
className={`line-clamp-3 text-[11px] ${
|
|
404
|
+
done
|
|
405
|
+
? 'text-agent-foreground/90 font-medium'
|
|
406
|
+
: active
|
|
407
|
+
? 'text-agent-foreground font-semibold'
|
|
408
|
+
: 'text-agent-muted-foreground'
|
|
409
|
+
}`}
|
|
410
|
+
title={node.task}
|
|
411
|
+
>
|
|
412
|
+
{node.task || `子任务 (${node.childId})`}
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{/* 追加指令 */}
|
|
417
|
+
{node.steers.length > 0 ? (
|
|
418
|
+
<div className="mt-1.5 space-y-0.5 rounded bg-amber-500/5 p-1 text-[10px] text-amber-800 dark:text-amber-200">
|
|
419
|
+
{node.steers.map((s, sIdx) => (
|
|
420
|
+
<div key={sIdx} className="flex items-start gap-1">
|
|
421
|
+
<LuMessageSquare className="mt-0.5 h-2.5 w-2.5 shrink-0 text-amber-600" />
|
|
422
|
+
<span className="truncate">{s.message}</span>
|
|
423
|
+
</div>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
) : null}
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
{/* 结论预览 */}
|
|
430
|
+
{node.answer ? (
|
|
431
|
+
<div className="mt-2 rounded bg-emerald-500/10 border border-emerald-500/20 px-2 py-1 text-[11px]">
|
|
432
|
+
<div className="flex items-center gap-1 font-semibold text-emerald-800 dark:text-emerald-200 text-[10px]">
|
|
433
|
+
<LuCheck className="h-3 w-3" />
|
|
434
|
+
<span>结论:</span>
|
|
435
|
+
<span className="font-mono text-emerald-950 dark:text-emerald-50 truncate">
|
|
436
|
+
{node.answer}
|
|
437
|
+
</span>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
) : node.error ? (
|
|
441
|
+
<div className="mt-2 rounded bg-agent-destructive/10 px-1.5 py-1 text-[10px] text-agent-destructive truncate">
|
|
442
|
+
失败: {node.error}
|
|
443
|
+
</div>
|
|
444
|
+
) : null}
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
})}
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
{/* 4. 向下的汇聚连线 */}
|
|
451
|
+
<JoinLines count={nodes.length} />
|
|
452
|
+
|
|
453
|
+
{/* 5. 终点:结果汇聚 (Join) 居中胶囊节点 */}
|
|
454
|
+
<div
|
|
455
|
+
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold shadow-2xs ${
|
|
456
|
+
isAllCompleted
|
|
457
|
+
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200'
|
|
458
|
+
: 'border-agent-border bg-agent-muted text-agent-muted-foreground'
|
|
459
|
+
}`}
|
|
460
|
+
>
|
|
461
|
+
<LuCircleCheck className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
|
|
462
|
+
<span>结果汇聚 (Join)</span>
|
|
463
|
+
<span className="text-[10px] opacity-80">
|
|
464
|
+
{isAllCompleted ? '所有分支已汇聚并完成回答' : '等待分支就绪...'}
|
|
465
|
+
</span>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
) : null}
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export default OrchestrationFlowCard;
|