@steerable/agent-ui 0.2.3 → 0.2.4
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/dist/cards/ActionSegmentCard.d.ts +18 -0
- package/dist/cards/ActionSegmentCard.d.ts.map +1 -0
- package/dist/cards/ActionSegmentCard.js +26 -0
- package/dist/cards/ActionSegmentCard.js.map +1 -0
- package/dist/cards/AnalysisDocumentCard.d.ts +33 -0
- package/dist/cards/AnalysisDocumentCard.d.ts.map +1 -0
- package/dist/cards/AnalysisDocumentCard.js +11 -0
- package/dist/cards/AnalysisDocumentCard.js.map +1 -0
- package/dist/cards/AskUserQuestionsCard.d.ts +43 -0
- package/dist/cards/AskUserQuestionsCard.d.ts.map +1 -0
- package/dist/cards/AskUserQuestionsCard.js +171 -0
- package/dist/cards/AskUserQuestionsCard.js.map +1 -0
- package/dist/cards/CoverageReportCard.d.ts +41 -0
- package/dist/cards/CoverageReportCard.d.ts.map +1 -0
- package/dist/cards/CoverageReportCard.js +55 -0
- package/dist/cards/CoverageReportCard.js.map +1 -0
- package/dist/cards/OrchestrationPlanCard.d.ts +60 -0
- package/dist/cards/OrchestrationPlanCard.d.ts.map +1 -0
- package/dist/cards/OrchestrationPlanCard.js +103 -0
- package/dist/cards/OrchestrationPlanCard.js.map +1 -0
- package/dist/cards/PlanSelectorCard.d.ts +40 -0
- package/dist/cards/PlanSelectorCard.d.ts.map +1 -0
- package/dist/cards/PlanSelectorCard.js +106 -0
- package/dist/cards/PlanSelectorCard.js.map +1 -0
- package/dist/cards/PlanStepsCard.d.ts +18 -0
- package/dist/cards/PlanStepsCard.d.ts.map +1 -0
- package/dist/cards/PlanStepsCard.js +25 -0
- package/dist/cards/PlanStepsCard.js.map +1 -0
- package/dist/cards/QuizCard.d.ts +44 -0
- package/dist/cards/QuizCard.d.ts.map +1 -0
- package/dist/cards/QuizCard.js +113 -0
- package/dist/cards/QuizCard.js.map +1 -0
- package/dist/cards/ResearchPlanCard.d.ts +23 -0
- package/dist/cards/ResearchPlanCard.d.ts.map +1 -0
- package/dist/cards/ResearchPlanCard.js +50 -0
- package/dist/cards/ResearchPlanCard.js.map +1 -0
- package/dist/cards/SearchSourcesCard.d.ts +29 -0
- package/dist/cards/SearchSourcesCard.d.ts.map +1 -0
- package/dist/cards/SearchSourcesCard.js +83 -0
- package/dist/cards/SearchSourcesCard.js.map +1 -0
- package/dist/cards/SuggestedRepliesCard.d.ts +18 -0
- package/dist/cards/SuggestedRepliesCard.d.ts.map +1 -0
- package/dist/cards/SuggestedRepliesCard.js +14 -0
- package/dist/cards/SuggestedRepliesCard.js.map +1 -0
- package/dist/cards/SummaryMessageCard.d.ts +40 -0
- package/dist/cards/SummaryMessageCard.d.ts.map +1 -0
- package/dist/cards/SummaryMessageCard.js +56 -0
- package/dist/cards/SummaryMessageCard.js.map +1 -0
- package/dist/cards/ThinkingProcessCard.d.ts +46 -0
- package/dist/cards/ThinkingProcessCard.d.ts.map +1 -0
- package/dist/cards/ThinkingProcessCard.js +79 -0
- package/dist/cards/ThinkingProcessCard.js.map +1 -0
- package/dist/cards/ToolExecutionCard.d.ts +21 -0
- package/dist/cards/ToolExecutionCard.d.ts.map +1 -0
- package/dist/cards/ToolExecutionCard.js +65 -0
- package/dist/cards/ToolExecutionCard.js.map +1 -0
- package/dist/cards/icons.d.ts +38 -0
- package/dist/cards/icons.d.ts.map +1 -0
- package/dist/cards/icons.js +87 -0
- package/dist/cards/icons.js.map +1 -0
- package/dist/cards/index.d.ts +38 -0
- package/dist/cards/index.d.ts.map +1 -0
- package/dist/cards/index.js +24 -0
- package/dist/cards/index.js.map +1 -0
- package/dist/cards/types.d.ts +24 -0
- package/dist/cards/types.d.ts.map +1 -0
- package/dist/cards/types.js +7 -0
- package/dist/cards/types.js.map +1 -0
- package/dist/components/AgentSelector.d.ts +23 -0
- package/dist/components/AgentSelector.d.ts.map +1 -0
- package/dist/components/AgentSelector.js +63 -0
- package/dist/components/AgentSelector.js.map +1 -0
- package/dist/components/AgentSelector.stories.d.ts +12 -0
- package/dist/components/AgentSelector.stories.d.ts.map +1 -0
- package/dist/components/AgentSelector.stories.js +95 -0
- package/dist/components/AgentSelector.stories.js.map +1 -0
- package/dist/components/ChatPanel.d.ts +138 -21
- package/dist/components/ChatPanel.d.ts.map +1 -1
- package/dist/components/ChatPanel.js +181 -26
- package/dist/components/ChatPanel.js.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/state/ChatSessionProvider.d.ts +59 -0
- package/dist/state/ChatSessionProvider.d.ts.map +1 -0
- package/dist/state/ChatSessionProvider.js +71 -0
- package/dist/state/ChatSessionProvider.js.map +1 -0
- package/dist/state/MockChatStreamTransport.d.ts +54 -0
- package/dist/state/MockChatStreamTransport.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.js +86 -0
- package/dist/state/MockChatStreamTransport.js.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts +13 -0
- package/dist/state/MockChatStreamTransport.stories.d.ts.map +1 -0
- package/dist/state/MockChatStreamTransport.stories.js +60 -0
- package/dist/state/MockChatStreamTransport.stories.js.map +1 -0
- package/dist/state/bridgeLegacySSE.d.ts +42 -0
- package/dist/state/bridgeLegacySSE.d.ts.map +1 -0
- package/dist/state/bridgeLegacySSE.js +102 -0
- package/dist/state/bridgeLegacySSE.js.map +1 -0
- package/dist/state/index.d.ts +32 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +24 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/parseSSE.d.ts +50 -0
- package/dist/state/parseSSE.d.ts.map +1 -0
- package/dist/state/parseSSE.js +112 -0
- package/dist/state/parseSSE.js.map +1 -0
- package/dist/state/useChatComposer.d.ts +51 -0
- package/dist/state/useChatComposer.d.ts.map +1 -0
- package/dist/state/useChatComposer.js +82 -0
- package/dist/state/useChatComposer.js.map +1 -0
- package/dist/state/useChatList.d.ts +71 -0
- package/dist/state/useChatList.d.ts.map +1 -0
- package/dist/state/useChatList.js +158 -0
- package/dist/state/useChatList.js.map +1 -0
- package/dist/state/useChatSession.d.ts +29 -0
- package/dist/state/useChatSession.d.ts.map +1 -0
- package/dist/state/useChatSession.js +37 -0
- package/dist/state/useChatSession.js.map +1 -0
- package/dist/state/useScrollLock.d.ts +35 -0
- package/dist/state/useScrollLock.d.ts.map +1 -0
- package/dist/state/useScrollLock.js +74 -0
- package/dist/state/useScrollLock.js.map +1 -0
- package/dist/state/useToolCallStream.d.ts +38 -0
- package/dist/state/useToolCallStream.d.ts.map +1 -0
- package/dist/state/useToolCallStream.js +74 -0
- package/dist/state/useToolCallStream.js.map +1 -0
- package/package.json +10 -2
- package/src/cards/ActionSegmentCard.tsx +60 -0
- package/src/cards/AnalysisDocumentCard.tsx +81 -0
- package/src/cards/AskUserQuestionsCard.tsx +354 -0
- package/src/cards/CoverageReportCard.tsx +215 -0
- package/src/cards/OrchestrationPlanCard.tsx +328 -0
- package/src/cards/PlanSelectorCard.tsx +312 -0
- package/src/cards/PlanStepsCard.tsx +52 -0
- package/src/cards/QuizCard.tsx +309 -0
- package/src/cards/ResearchPlanCard.tsx +169 -0
- package/src/cards/SearchSourcesCard.tsx +244 -0
- package/src/cards/SuggestedRepliesCard.tsx +48 -0
- package/src/cards/SummaryMessageCard.tsx +145 -0
- package/src/cards/ThinkingProcessCard.tsx +155 -0
- package/src/cards/ToolExecutionCard.tsx +152 -0
- package/src/cards/cards.test.tsx +492 -0
- package/src/cards/icons.tsx +249 -0
- package/src/cards/index.ts +53 -0
- package/src/cards/types.ts +27 -0
- package/src/components/AgentSelector.stories.tsx +113 -0
- package/src/components/AgentSelector.test.tsx +70 -0
- package/src/components/AgentSelector.tsx +147 -0
- package/src/components/ChatPanel.test.tsx +130 -0
- package/src/components/ChatPanel.tsx +612 -138
- package/src/components/index.ts +13 -1
- package/src/index.ts +43 -0
- package/src/state/ChatSessionProvider.test.tsx +85 -0
- package/src/state/ChatSessionProvider.tsx +98 -0
- package/src/state/MockChatStreamTransport.stories.tsx +95 -0
- package/src/state/MockChatStreamTransport.test.ts +163 -0
- package/src/state/MockChatStreamTransport.ts +123 -0
- package/src/state/bridgeLegacySSE.test.ts +134 -0
- package/src/state/bridgeLegacySSE.ts +134 -0
- package/src/state/index.ts +82 -0
- package/src/state/parseSSE.test.ts +123 -0
- package/src/state/parseSSE.ts +123 -0
- package/src/state/useChatComposer.test.ts +145 -0
- package/src/state/useChatComposer.ts +117 -0
- package/src/state/useChatList.test.ts +135 -0
- package/src/state/useChatList.ts +217 -0
- package/src/state/useChatSession.test.ts +61 -0
- package/src/state/useChatSession.ts +64 -0
- package/src/state/useScrollLock.test.ts +99 -0
- package/src/state/useScrollLock.ts +105 -0
- package/src/state/useToolCallStream.test.ts +86 -0
- package/src/state/useToolCallStream.ts +110 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<OrchestrationPlanCard />`
|
|
3
|
+
*
|
|
4
|
+
* The visible surface of an otherwise invisible Coordinator agent. Renders
|
|
5
|
+
* one row per worker task with a status dot driven by `taskStatuses` (a map
|
|
6
|
+
* the parent computes off sibling assistant bubbles tagged with the same
|
|
7
|
+
* orchestrationGroupId + orchestrationTaskId).
|
|
8
|
+
*
|
|
9
|
+
* Three top-level shells are supported via mutually-exclusive flags so the
|
|
10
|
+
* Coordinator can express its lifecycle without forcing the caller to render
|
|
11
|
+
* three different components:
|
|
12
|
+
* - `loading` + optional `loadingHint` — Coordinator is still planning.
|
|
13
|
+
* - `failed` + optional `failure` — planning errored.
|
|
14
|
+
* - default — `payload.tasks` are rendered.
|
|
15
|
+
*
|
|
16
|
+
* Slots:
|
|
17
|
+
* - `renderAbove` / `renderBelowHeader` — out-of-flow rails for badges and
|
|
18
|
+
* context-load lines that deeppath wants to ride along.
|
|
19
|
+
* - `renderTaskRow(task, status)` — full row override (deeppath uses
|
|
20
|
+
* it to draw the per-agent colored chip + monospaced task id + right-
|
|
21
|
+
* aligned status label).
|
|
22
|
+
* - `headerLabel`, `headerSecondary` — override the default
|
|
23
|
+
* "编排计划 · N 个子任务" header label/subline.
|
|
24
|
+
* - `hideMode`, `hideHeaderCounts` — drop the optional header chips
|
|
25
|
+
* when the host UI does not want them (deeppath hides both).
|
|
26
|
+
* - controlled `expanded` + `onExpandedChange`.
|
|
27
|
+
*/
|
|
28
|
+
import * as React from 'react';
|
|
29
|
+
import type { OrchestrationPlanPayload } from '@steerable/agent-protocol';
|
|
30
|
+
import {
|
|
31
|
+
ChevronDownIcon,
|
|
32
|
+
ChevronRightIcon,
|
|
33
|
+
GitBranchIcon,
|
|
34
|
+
LoaderIcon,
|
|
35
|
+
AlertIcon,
|
|
36
|
+
} from './icons.js';
|
|
37
|
+
import { asReactNode, type RenderSlotResult } from './types.js';
|
|
38
|
+
|
|
39
|
+
export type OrchestrationTaskStatus =
|
|
40
|
+
| 'pending'
|
|
41
|
+
| 'running'
|
|
42
|
+
| 'ok'
|
|
43
|
+
| 'skipped'
|
|
44
|
+
| 'failed'
|
|
45
|
+
| 'ask_user_paused';
|
|
46
|
+
|
|
47
|
+
export type OrchestrationTask = OrchestrationPlanPayload['tasks'][number];
|
|
48
|
+
|
|
49
|
+
export interface OrchestrationPlanCardProps {
|
|
50
|
+
payload: OrchestrationPlanPayload;
|
|
51
|
+
/** Status of each task, keyed by `payload.tasks[i].id`. Missing → pending. */
|
|
52
|
+
taskStatuses?: Record<string, OrchestrationTaskStatus>;
|
|
53
|
+
/** Lookup of agentId → display name; falls back to the raw id. */
|
|
54
|
+
agentNameFor?: (agentId: string) => string;
|
|
55
|
+
className?: string;
|
|
56
|
+
defaultExpanded?: boolean;
|
|
57
|
+
expanded?: boolean;
|
|
58
|
+
onExpandedChange?: (next: boolean) => void;
|
|
59
|
+
|
|
60
|
+
// Lifecycle shells -- mutually exclusive with each other & the default body
|
|
61
|
+
loading?: boolean;
|
|
62
|
+
loadingHint?: React.ReactNode;
|
|
63
|
+
failed?: boolean;
|
|
64
|
+
failure?: React.ReactNode;
|
|
65
|
+
|
|
66
|
+
// Header customisation
|
|
67
|
+
headerLabel?: React.ReactNode;
|
|
68
|
+
headerSecondary?: React.ReactNode;
|
|
69
|
+
hideMode?: boolean;
|
|
70
|
+
hideHeaderCounts?: boolean;
|
|
71
|
+
|
|
72
|
+
// Out-of-flow rails
|
|
73
|
+
renderAbove?: () => RenderSlotResult;
|
|
74
|
+
renderBelowHeader?: () => RenderSlotResult;
|
|
75
|
+
|
|
76
|
+
// Per-row override
|
|
77
|
+
renderTaskRow?: (task: OrchestrationTask, status: OrchestrationTaskStatus) => RenderSlotResult;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const STATUS_LABEL: Record<OrchestrationTaskStatus, string> = {
|
|
81
|
+
pending: '排队中',
|
|
82
|
+
running: '执行中',
|
|
83
|
+
ok: '已完成',
|
|
84
|
+
skipped: '已跳过',
|
|
85
|
+
failed: '失败',
|
|
86
|
+
ask_user_paused: '等待用户',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const DOT_CLASS: Record<OrchestrationTaskStatus, string> = {
|
|
90
|
+
pending: 'bg-[var(--agent-muted-foreground,#9ca3af)]',
|
|
91
|
+
running: 'bg-amber-500 animate-pulse',
|
|
92
|
+
ok: 'bg-emerald-500',
|
|
93
|
+
skipped: 'bg-[var(--agent-muted-foreground,#d1d5db)]',
|
|
94
|
+
failed: 'bg-rose-500',
|
|
95
|
+
ask_user_paused: 'bg-sky-500',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const OrchestrationPlanCard: React.FC<OrchestrationPlanCardProps> = ({
|
|
99
|
+
payload,
|
|
100
|
+
taskStatuses,
|
|
101
|
+
agentNameFor,
|
|
102
|
+
className,
|
|
103
|
+
defaultExpanded = true,
|
|
104
|
+
expanded: expandedProp,
|
|
105
|
+
onExpandedChange,
|
|
106
|
+
loading,
|
|
107
|
+
loadingHint,
|
|
108
|
+
failed,
|
|
109
|
+
failure,
|
|
110
|
+
headerLabel,
|
|
111
|
+
headerSecondary,
|
|
112
|
+
hideMode,
|
|
113
|
+
hideHeaderCounts,
|
|
114
|
+
renderAbove,
|
|
115
|
+
renderBelowHeader,
|
|
116
|
+
renderTaskRow,
|
|
117
|
+
}) => {
|
|
118
|
+
const [expandedState, setExpandedState] = React.useState(defaultExpanded);
|
|
119
|
+
const isControlled = expandedProp !== undefined;
|
|
120
|
+
const expanded = isControlled ? Boolean(expandedProp) : expandedState;
|
|
121
|
+
const setExpanded = (next: boolean) => {
|
|
122
|
+
if (!isControlled) setExpandedState(next);
|
|
123
|
+
onExpandedChange?.(next);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const above = renderAbove ? asReactNode(renderAbove()) : null;
|
|
127
|
+
const belowHeader = renderBelowHeader ? asReactNode(renderBelowHeader()) : null;
|
|
128
|
+
|
|
129
|
+
// ---------------- loading shell ----------------
|
|
130
|
+
if (loading) {
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
className={[
|
|
134
|
+
'steerable-orchestration-plan',
|
|
135
|
+
className,
|
|
136
|
+
].filter(Boolean).join(' ')}
|
|
137
|
+
>
|
|
138
|
+
{above}
|
|
139
|
+
<div className="rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] text-sm shadow-sm">
|
|
140
|
+
<div className="flex w-full items-center gap-2 px-3 py-2 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
141
|
+
<LoaderIcon size={14} className="shrink-0 animate-spin" />
|
|
142
|
+
<GitBranchIcon size={14} className="shrink-0" />
|
|
143
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap">
|
|
144
|
+
{headerLabel ?? (
|
|
145
|
+
<span className="font-medium text-[var(--agent-foreground,#111827)]">协调员</span>
|
|
146
|
+
)}
|
|
147
|
+
<span className="text-[var(--agent-muted-foreground,#9ca3af)]">·</span>
|
|
148
|
+
<span>{loadingHint ?? '正在规划'}</span>
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
{belowHeader}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------- failed shell ----------------
|
|
158
|
+
if (failed) {
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
className={[
|
|
162
|
+
'steerable-orchestration-plan',
|
|
163
|
+
className,
|
|
164
|
+
].filter(Boolean).join(' ')}
|
|
165
|
+
>
|
|
166
|
+
{above}
|
|
167
|
+
<div className="rounded-lg border border-rose-500/30 bg-rose-500/5 text-sm shadow-sm">
|
|
168
|
+
<div className="flex w-full items-start gap-2 px-3 py-2 text-xs">
|
|
169
|
+
<AlertIcon size={14} className="mt-0.5 shrink-0 text-rose-500" />
|
|
170
|
+
<GitBranchIcon size={14} className="mt-0.5 shrink-0 text-[var(--agent-muted-foreground,#6b7280)]" />
|
|
171
|
+
<div className="min-w-0 flex-1">
|
|
172
|
+
<div className="inline-flex items-center gap-1.5 whitespace-nowrap">
|
|
173
|
+
{headerLabel ?? (
|
|
174
|
+
<span className="font-medium text-[var(--agent-foreground,#111827)]">协调员</span>
|
|
175
|
+
)}
|
|
176
|
+
<span className="text-[var(--agent-muted-foreground,#9ca3af)]">·</span>
|
|
177
|
+
<span className="text-rose-500">规划失败</span>
|
|
178
|
+
</div>
|
|
179
|
+
{failure && (
|
|
180
|
+
<div className="mt-1 break-words leading-relaxed text-[var(--agent-muted-foreground,#6b7280)]">
|
|
181
|
+
{failure}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
{belowHeader}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------- default ready shell ----------------
|
|
193
|
+
const tasks = payload.tasks ?? [];
|
|
194
|
+
|
|
195
|
+
const counts = React.useMemo(() => {
|
|
196
|
+
const c = { running: 0, ok: 0, failed: 0, paused: 0, total: tasks.length };
|
|
197
|
+
for (const t of tasks) {
|
|
198
|
+
const s = (taskStatuses?.[t.id] ?? 'pending') as OrchestrationTaskStatus;
|
|
199
|
+
if (s === 'running') c.running += 1;
|
|
200
|
+
else if (s === 'ok') c.ok += 1;
|
|
201
|
+
else if (s === 'failed') c.failed += 1;
|
|
202
|
+
else if (s === 'ask_user_paused') c.paused += 1;
|
|
203
|
+
}
|
|
204
|
+
return c;
|
|
205
|
+
}, [tasks, taskStatuses]);
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
className={[
|
|
210
|
+
'steerable-orchestration-plan',
|
|
211
|
+
className,
|
|
212
|
+
].filter(Boolean).join(' ')}
|
|
213
|
+
>
|
|
214
|
+
{above}
|
|
215
|
+
<div className="rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] text-sm shadow-sm">
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={() => setExpanded(!expanded)}
|
|
219
|
+
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-xs text-[var(--agent-muted-foreground,#6b7280)] transition-all duration-200 hover:text-[var(--agent-foreground,#111827)]"
|
|
220
|
+
>
|
|
221
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
222
|
+
{expanded ? <ChevronDownIcon size={14} /> : <ChevronRightIcon size={14} />}
|
|
223
|
+
<GitBranchIcon size={14} className="shrink-0" />
|
|
224
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap">
|
|
225
|
+
{headerLabel ?? (
|
|
226
|
+
<span className="font-medium text-[var(--agent-foreground,#111827)]">编排计划</span>
|
|
227
|
+
)}
|
|
228
|
+
{headerSecondary !== undefined ? (
|
|
229
|
+
<>
|
|
230
|
+
<span className="text-[var(--agent-muted-foreground,#9ca3af)]">·</span>
|
|
231
|
+
<span>{headerSecondary}</span>
|
|
232
|
+
</>
|
|
233
|
+
) : (
|
|
234
|
+
<>
|
|
235
|
+
<span className="text-[var(--agent-muted-foreground,#9ca3af)]">·</span>
|
|
236
|
+
<span>{tasks.length} 个子任务</span>
|
|
237
|
+
</>
|
|
238
|
+
)}
|
|
239
|
+
{!hideMode && payload.mode && (
|
|
240
|
+
<span className="ml-1 rounded-full bg-[var(--agent-muted,#f3f4f6)] px-2 py-0.5 text-[10px] uppercase tracking-wider text-[var(--agent-muted-foreground,#6b7280)]">
|
|
241
|
+
{payload.mode}
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
</span>
|
|
245
|
+
{payload.rationale && !expanded && (
|
|
246
|
+
<span className="min-w-0 flex-1 truncate text-[var(--agent-muted-foreground,#9ca3af)]">
|
|
247
|
+
{payload.rationale}
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
250
|
+
</span>
|
|
251
|
+
{!hideHeaderCounts && (
|
|
252
|
+
<span className="flex shrink-0 items-center gap-2">
|
|
253
|
+
{counts.running > 0 && (
|
|
254
|
+
<span className="flex items-center gap-1">
|
|
255
|
+
<LoaderIcon size={12} className="text-amber-500" /> {counts.running}
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
{counts.failed > 0 && (
|
|
259
|
+
<span className="flex items-center gap-1 text-rose-600">
|
|
260
|
+
<AlertIcon size={12} /> {counts.failed}
|
|
261
|
+
</span>
|
|
262
|
+
)}
|
|
263
|
+
<span>
|
|
264
|
+
{counts.ok}/{counts.total}
|
|
265
|
+
</span>
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
|
|
270
|
+
{belowHeader}
|
|
271
|
+
|
|
272
|
+
{expanded && (
|
|
273
|
+
<div className="px-3 pb-3">
|
|
274
|
+
{payload.rationale && (
|
|
275
|
+
<p className="pb-2 text-xs leading-relaxed text-[var(--agent-muted-foreground,#6b7280)]">
|
|
276
|
+
{payload.rationale}
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
279
|
+
<ul className="space-y-1.5">
|
|
280
|
+
{tasks.map((t) => {
|
|
281
|
+
const status = (taskStatuses?.[t.id] ?? 'pending') as OrchestrationTaskStatus;
|
|
282
|
+
if (renderTaskRow) {
|
|
283
|
+
return (
|
|
284
|
+
<React.Fragment key={t.id}>
|
|
285
|
+
{asReactNode(renderTaskRow(t, status))}
|
|
286
|
+
</React.Fragment>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return (
|
|
290
|
+
<li key={t.id} className="flex items-start gap-2 text-sm">
|
|
291
|
+
<span
|
|
292
|
+
className={`mt-1.5 h-2 w-2 flex-shrink-0 rounded-full ${DOT_CLASS[status]}`}
|
|
293
|
+
aria-hidden
|
|
294
|
+
/>
|
|
295
|
+
<div className="min-w-0 flex-1">
|
|
296
|
+
<div className="flex items-center gap-2">
|
|
297
|
+
<span className="font-medium text-[var(--agent-foreground,#111827)]">
|
|
298
|
+
{agentNameFor ? agentNameFor(t.agentId) : t.agentId}
|
|
299
|
+
</span>
|
|
300
|
+
<span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
301
|
+
{STATUS_LABEL[status]}
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
{t.prompt && (
|
|
305
|
+
<p className="mt-0.5 line-clamp-2 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
306
|
+
{t.prompt}
|
|
307
|
+
</p>
|
|
308
|
+
)}
|
|
309
|
+
{(t.dependsOn?.length ?? 0) > 0 && (
|
|
310
|
+
<p className="mt-0.5 text-[10px] uppercase tracking-wider text-[var(--agent-muted-foreground,#9ca3af)]">
|
|
311
|
+
依赖:{t.dependsOn!.join(', ')}
|
|
312
|
+
</p>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
</li>
|
|
316
|
+
);
|
|
317
|
+
})}
|
|
318
|
+
</ul>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export { STATUS_LABEL as ORCHESTRATION_STATUS_LABEL, DOT_CLASS as ORCHESTRATION_DOT_CLASS };
|
|
327
|
+
|
|
328
|
+
export default OrchestrationPlanCard;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<PlanSelectorCard />`
|
|
3
|
+
*
|
|
4
|
+
* Two-to-four candidate plans with effort / risk metrics and pros/cons.
|
|
5
|
+
*
|
|
6
|
+
* Calls `onSelect(planId)` once the user picks one. If `payload.selectedPlan`
|
|
7
|
+
* is filled in by the backend the card switches to read-only mode and
|
|
8
|
+
* highlights the chosen plan.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour switches:
|
|
11
|
+
* - `expandableDetails` (default `false`): when on, only the plan summary +
|
|
12
|
+
* metrics + select button are visible by default; approach / pros / cons
|
|
13
|
+
* sit behind a "查看详情" toggle.
|
|
14
|
+
*
|
|
15
|
+
* Slots:
|
|
16
|
+
* - `renderHeader(plansCount)` -- replace the "候选方案 / 为你准备了 N 个方案"
|
|
17
|
+
* label area.
|
|
18
|
+
* - `renderBottomHint({ submitted })` -- swap the hint shown below the list.
|
|
19
|
+
* - `existingGoalPrefix`, `newGoalPrefix` -- localise goal-attribution
|
|
20
|
+
* phrasing.
|
|
21
|
+
*/
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
import type { PlanSelectorPayload } from '@steerable/agent-protocol';
|
|
24
|
+
import {
|
|
25
|
+
ClockIcon,
|
|
26
|
+
FlameIcon,
|
|
27
|
+
ShieldIcon,
|
|
28
|
+
CheckIcon,
|
|
29
|
+
TargetIcon,
|
|
30
|
+
AlertIcon,
|
|
31
|
+
ThumbsUpIcon,
|
|
32
|
+
LoaderIcon,
|
|
33
|
+
ChevronDownIcon,
|
|
34
|
+
ChevronUpIcon,
|
|
35
|
+
} from './icons.js';
|
|
36
|
+
import { asReactNode, type RenderSlotResult } from './types.js';
|
|
37
|
+
|
|
38
|
+
type Level = 'low' | 'medium' | 'high';
|
|
39
|
+
|
|
40
|
+
const EFFORT_CONFIG: Record<Level, { label: string; text: string; bg: string }> = {
|
|
41
|
+
low: {
|
|
42
|
+
label: '低投入',
|
|
43
|
+
text: 'text-green-600 dark:text-green-400',
|
|
44
|
+
bg: 'bg-green-50 dark:bg-green-950/30',
|
|
45
|
+
},
|
|
46
|
+
medium: {
|
|
47
|
+
label: '中投入',
|
|
48
|
+
text: 'text-amber-600 dark:text-amber-400',
|
|
49
|
+
bg: 'bg-amber-50 dark:bg-amber-950/30',
|
|
50
|
+
},
|
|
51
|
+
high: {
|
|
52
|
+
label: '高投入',
|
|
53
|
+
text: 'text-red-600 dark:text-red-400',
|
|
54
|
+
bg: 'bg-red-50 dark:bg-red-950/30',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const RISK_CONFIG: Record<Level, { label: string; text: string; bg: string }> = {
|
|
58
|
+
low: {
|
|
59
|
+
label: '低风险',
|
|
60
|
+
text: 'text-green-600 dark:text-green-400',
|
|
61
|
+
bg: 'bg-green-50 dark:bg-green-950/30',
|
|
62
|
+
},
|
|
63
|
+
medium: {
|
|
64
|
+
label: '中风险',
|
|
65
|
+
text: 'text-amber-600 dark:text-amber-400',
|
|
66
|
+
bg: 'bg-amber-50 dark:bg-amber-950/30',
|
|
67
|
+
},
|
|
68
|
+
high: {
|
|
69
|
+
label: '高风险',
|
|
70
|
+
text: 'text-red-600 dark:text-red-400',
|
|
71
|
+
bg: 'bg-red-50 dark:bg-red-950/30',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export interface PlanSelectorCardProps {
|
|
76
|
+
payload: PlanSelectorPayload;
|
|
77
|
+
isComplete?: boolean;
|
|
78
|
+
onSelect?: (planId: string) => void;
|
|
79
|
+
className?: string;
|
|
80
|
+
expandableDetails?: boolean;
|
|
81
|
+
existingGoalPrefix?: string;
|
|
82
|
+
newGoalPrefix?: string;
|
|
83
|
+
renderHeader?: (plansCount: number) => RenderSlotResult;
|
|
84
|
+
renderBottomHint?: (args: { submitted: boolean }) => RenderSlotResult;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const PlanSelectorCard: React.FC<PlanSelectorCardProps> = ({
|
|
88
|
+
payload,
|
|
89
|
+
isComplete,
|
|
90
|
+
onSelect,
|
|
91
|
+
className,
|
|
92
|
+
expandableDetails = false,
|
|
93
|
+
existingGoalPrefix = '归属目标:',
|
|
94
|
+
newGoalPrefix = '新目标:',
|
|
95
|
+
renderHeader,
|
|
96
|
+
renderBottomHint,
|
|
97
|
+
}) => {
|
|
98
|
+
const isReadOnly = Boolean(isComplete || payload.selectedPlan);
|
|
99
|
+
const plans = payload.plans ?? [];
|
|
100
|
+
const [submittingFor, setSubmittingFor] = React.useState<string | null>(null);
|
|
101
|
+
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
|
102
|
+
|
|
103
|
+
const goalLabel = (() => {
|
|
104
|
+
const g = payload.goalAttribution;
|
|
105
|
+
if (!g) return null;
|
|
106
|
+
if (g.type === 'existing') return `${existingGoalPrefix}${g.existingGoalTitle ?? ''}`;
|
|
107
|
+
return `${newGoalPrefix}${g.newGoalTitle ?? ''}`;
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
const handleSelect = (planId: string) => {
|
|
111
|
+
if (!onSelect || isReadOnly) return;
|
|
112
|
+
setSubmittingFor(planId);
|
|
113
|
+
try {
|
|
114
|
+
onSelect(planId);
|
|
115
|
+
} finally {
|
|
116
|
+
setSubmittingFor(null);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className={[
|
|
123
|
+
'steerable-plan-selector rounded-lg border border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#fff)] text-sm text-[var(--agent-foreground,#111827)]',
|
|
124
|
+
className,
|
|
125
|
+
].filter(Boolean).join(' ')}
|
|
126
|
+
>
|
|
127
|
+
<div className="space-y-3 p-3">
|
|
128
|
+
{renderHeader ? (
|
|
129
|
+
<div>{asReactNode(renderHeader(plans.length))}</div>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<TargetIcon size={14} className="text-blue-500" />
|
|
133
|
+
<span className="text-sm font-medium">候选方案</span>
|
|
134
|
+
{goalLabel && (
|
|
135
|
+
<span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
136
|
+
· {goalLabel}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
{renderHeader && goalLabel && (
|
|
142
|
+
<p className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">{goalLabel}</p>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<div className="space-y-2">
|
|
146
|
+
{plans.map((p) => {
|
|
147
|
+
const isSelected = payload.selectedPlan === p.id;
|
|
148
|
+
const effort = EFFORT_CONFIG[p.metrics?.effortLevel || 'medium'];
|
|
149
|
+
const risk = RISK_CONFIG[p.metrics?.riskLevel || 'medium'];
|
|
150
|
+
const isExpanded = expandableDetails ? expandedId === p.id : true;
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
key={p.id}
|
|
155
|
+
className={[
|
|
156
|
+
'rounded-lg border transition-all duration-200',
|
|
157
|
+
isSelected
|
|
158
|
+
? 'border-blue-400 dark:border-blue-600 bg-blue-50/50 dark:bg-blue-950/20 ring-1 ring-blue-200 dark:ring-blue-800'
|
|
159
|
+
: 'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-background,#fff)] hover:border-[var(--agent-border,#e5e7eb)]/80',
|
|
160
|
+
isReadOnly && !isSelected ? 'opacity-50' : '',
|
|
161
|
+
].filter(Boolean).join(' ')}
|
|
162
|
+
>
|
|
163
|
+
<div className="p-3">
|
|
164
|
+
<div className="flex items-start justify-between gap-2">
|
|
165
|
+
<div className="min-w-0 flex-1">
|
|
166
|
+
<div className="flex items-center gap-2">
|
|
167
|
+
<h4 className="truncate text-sm font-medium">{p.name}</h4>
|
|
168
|
+
{isSelected && (
|
|
169
|
+
<span className="inline-flex items-center gap-0.5 rounded-full bg-blue-100 px-1.5 py-0.5 text-[10px] font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
|
|
170
|
+
<CheckIcon size={10} /> 已选择
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
<p className="mt-1 text-xs leading-relaxed text-[var(--agent-muted-foreground,#6b7280)]">
|
|
175
|
+
{p.summary}
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
181
|
+
{p.metrics?.duration && (
|
|
182
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-[var(--agent-muted,#f3f4f6)] px-2 py-0.5 text-[10px] font-medium text-[var(--agent-muted-foreground,#6b7280)]">
|
|
183
|
+
<ClockIcon size={11} />
|
|
184
|
+
{p.metrics.duration}
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
<span
|
|
188
|
+
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium ${effort.bg} ${effort.text}`}
|
|
189
|
+
>
|
|
190
|
+
<FlameIcon size={11} />
|
|
191
|
+
{effort.label}
|
|
192
|
+
</span>
|
|
193
|
+
<span
|
|
194
|
+
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium ${risk.bg} ${risk.text}`}
|
|
195
|
+
>
|
|
196
|
+
<ShieldIcon size={11} />
|
|
197
|
+
{risk.label}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div className="mt-2 flex items-center justify-between">
|
|
202
|
+
{expandableDetails ? (
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={() => setExpandedId(isExpanded ? null : p.id)}
|
|
206
|
+
className="inline-flex items-center gap-1 text-[11px] text-[var(--agent-muted-foreground,#6b7280)] transition-colors duration-200 hover:text-[var(--agent-foreground,#111827)]"
|
|
207
|
+
>
|
|
208
|
+
{isExpanded ? (
|
|
209
|
+
<>
|
|
210
|
+
<ChevronUpIcon size={11} /> 收起详情
|
|
211
|
+
</>
|
|
212
|
+
) : (
|
|
213
|
+
<>
|
|
214
|
+
<ChevronDownIcon size={11} /> 查看详情
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
</button>
|
|
218
|
+
) : (
|
|
219
|
+
<span />
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{!isReadOnly && onSelect && (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={() => handleSelect(p.id)}
|
|
226
|
+
disabled={submittingFor !== null}
|
|
227
|
+
className="inline-flex items-center gap-1 rounded-full bg-[var(--agent-primary,#18181b)] px-3 py-1 text-xs font-medium text-[var(--agent-primary-foreground,#fff)] shadow-sm transition-all duration-200 hover:opacity-90 disabled:opacity-50"
|
|
228
|
+
>
|
|
229
|
+
{submittingFor === p.id && <LoaderIcon size={10} />}
|
|
230
|
+
选择此方案
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{isExpanded && (
|
|
237
|
+
<div className="space-y-2 border-t border-[var(--agent-border,#e5e7eb)]/50 px-3 pb-3 pt-2">
|
|
238
|
+
{p.approach && (
|
|
239
|
+
<p className="text-xs leading-relaxed">{p.approach}</p>
|
|
240
|
+
)}
|
|
241
|
+
<div className="grid grid-cols-2 gap-2">
|
|
242
|
+
{(p.pros?.length ?? 0) > 0 && (
|
|
243
|
+
<div className="space-y-1">
|
|
244
|
+
<div className="flex items-center gap-1 text-[10px] font-medium text-green-600 dark:text-green-400">
|
|
245
|
+
<ThumbsUpIcon size={11} />
|
|
246
|
+
优势
|
|
247
|
+
</div>
|
|
248
|
+
{p.pros!.map((pro, i) => (
|
|
249
|
+
<p
|
|
250
|
+
key={`pro-${i}`}
|
|
251
|
+
className="pl-4 text-[11px] text-[var(--agent-muted-foreground,#6b7280)]"
|
|
252
|
+
>
|
|
253
|
+
{pro}
|
|
254
|
+
</p>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
{(p.cons?.length ?? 0) > 0 && (
|
|
259
|
+
<div className="space-y-1">
|
|
260
|
+
<div className="flex items-center gap-1 text-[10px] font-medium text-amber-600 dark:text-amber-400">
|
|
261
|
+
<AlertIcon size={11} />
|
|
262
|
+
注意
|
|
263
|
+
</div>
|
|
264
|
+
{p.cons!.map((con, i) => (
|
|
265
|
+
<p
|
|
266
|
+
key={`con-${i}`}
|
|
267
|
+
className="pl-4 text-[11px] text-[var(--agent-muted-foreground,#6b7280)]"
|
|
268
|
+
>
|
|
269
|
+
{con}
|
|
270
|
+
</p>
|
|
271
|
+
))}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
{p.bestFor && (
|
|
276
|
+
<p className="text-[11px] italic text-[var(--agent-muted-foreground,#6b7280)]">
|
|
277
|
+
适合:{p.bestFor}
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{payload.comparison && (
|
|
288
|
+
<p className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
289
|
+
{payload.comparison}
|
|
290
|
+
</p>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
<div className="flex items-center justify-between pt-1">
|
|
294
|
+
{renderBottomHint
|
|
295
|
+
? asReactNode(renderBottomHint({ submitted: isReadOnly }))
|
|
296
|
+
: !isReadOnly
|
|
297
|
+
? <span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
298
|
+
点击选择方案
|
|
299
|
+
</span>
|
|
300
|
+
: <div className="flex items-center gap-1.5">
|
|
301
|
+
<CheckIcon size={12} className="text-blue-500" />
|
|
302
|
+
<span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
303
|
+
方案已选择
|
|
304
|
+
</span>
|
|
305
|
+
</div>}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default PlanSelectorCard;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<PlanStepsCard />`
|
|
3
|
+
*
|
|
4
|
+
* Compact horizontal checklist of skill names: `step1 / step2 / step3`. Used
|
|
5
|
+
* to show what the agent intends to do this turn. Pure render off
|
|
6
|
+
* `PlanStepsPayload`; the parent passes `isStreaming` if it wants the pulsing
|
|
7
|
+
* cursor dot at the end.
|
|
8
|
+
*/
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import type { PlanStepsPayload } from '@steerable/agent-protocol';
|
|
11
|
+
import { ListChecksIcon } from './icons.js';
|
|
12
|
+
|
|
13
|
+
export interface PlanStepsCardProps {
|
|
14
|
+
payload: PlanStepsPayload;
|
|
15
|
+
isStreaming?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PlanStepsCard: React.FC<PlanStepsCardProps> = ({ payload, isStreaming, className }) => {
|
|
20
|
+
const steps = payload.steps ?? [];
|
|
21
|
+
if (steps.length === 0 && !isStreaming) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={[
|
|
26
|
+
'steerable-plan-steps flex items-start gap-2 rounded-lg border px-3 py-2 transition-all duration-200',
|
|
27
|
+
isStreaming
|
|
28
|
+
? 'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-card-bg,#f9fafb)] shadow-sm'
|
|
29
|
+
: 'border-[var(--agent-border,#e5e7eb)] bg-[var(--agent-muted,#f3f4f6)]',
|
|
30
|
+
className,
|
|
31
|
+
].filter(Boolean).join(' ')}
|
|
32
|
+
>
|
|
33
|
+
<ListChecksIcon size={14} className="mt-0.5 flex-shrink-0 text-[var(--agent-muted-foreground,#6b7280)]" />
|
|
34
|
+
<div className="flex flex-wrap items-center gap-1 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
35
|
+
{steps.map((step, idx) => (
|
|
36
|
+
<React.Fragment key={`${idx}-${step}`}>
|
|
37
|
+
{idx > 0 && <span aria-hidden className="select-none opacity-40">/</span>}
|
|
38
|
+
<span className="whitespace-nowrap">{step}</span>
|
|
39
|
+
</React.Fragment>
|
|
40
|
+
))}
|
|
41
|
+
{isStreaming && (
|
|
42
|
+
<span
|
|
43
|
+
aria-hidden
|
|
44
|
+
className="ml-1 inline-block h-1.5 w-1.5 translate-y-[1px] animate-pulse rounded-full bg-current"
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default PlanStepsCard;
|