@springbrand/message-panel 0.1.2 → 0.1.3-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/demo/camel-chat-showcase.tsx +1265 -0
- package/demo/fixtures.ts +292 -0
- package/demo/message-panel-chat-showcase.tsx +250 -0
- package/demo/message-panel-gallery.tsx +389 -0
- package/package.json +6 -16
- package/src/adapters/ai-sdk.ts +32 -0
- package/src/camel/camel-chat-messages.tsx +1435 -0
- package/src/camel/camel-model-select.tsx +88 -0
- package/src/camel/camel-plan-bar.tsx +115 -0
- package/src/camel/camel-prompt-input.tsx +547 -0
- package/src/camel/camel-tool-presentation.tsx +1270 -0
- package/src/camel/camel-turn.ts +334 -0
- package/src/camel/turn-summary-bar.tsx +81 -0
- package/src/chat-summary-panel.tsx +229 -0
- package/src/composer/chat-composer.tsx +216 -0
- package/src/composer/composer-attachments.tsx +138 -0
- package/src/composer/composer-trigger-popover.tsx +236 -0
- package/src/composer/composer.tsx +425 -0
- package/src/composer/drop-controller.ts +80 -0
- package/src/composer/types.ts +79 -0
- package/src/contracts.ts +138 -0
- package/src/conversation.tsx +76 -0
- package/src/internal/cn.ts +3 -0
- package/src/message-panel.tsx +368 -0
- package/src/message.tsx +254 -0
- package/src/parts/index.tsx +1149 -0
- package/src/parts/kind.ts +67 -0
- package/src/parts/plan.ts +64 -0
- package/src/parts/primitives.tsx +439 -0
- package/src/registry.ts +44 -0
- package/src/status.tsx +132 -0
- package/dist/demo/camel-chat-showcase.d.ts +0 -1
- package/dist/demo/camel-chat-showcase.js +0 -1007
- package/dist/demo/fixtures.d.ts +0 -32
- package/dist/demo/fixtures.js +0 -250
- package/dist/demo/index.js +0 -4
- package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
- package/dist/demo/message-panel-chat-showcase.js +0 -114
- package/dist/demo/message-panel-gallery.d.ts +0 -1
- package/dist/demo/message-panel-gallery.js +0 -130
- package/dist/src/adapters/ai-sdk.d.ts +0 -4
- package/dist/src/adapters/ai-sdk.js +0 -25
- package/dist/src/camel/camel-chat-messages.d.ts +0 -44
- package/dist/src/camel/camel-chat-messages.js +0 -421
- package/dist/src/camel/camel-model-select.d.ts +0 -13
- package/dist/src/camel/camel-model-select.js +0 -7
- package/dist/src/camel/camel-plan-bar.d.ts +0 -5
- package/dist/src/camel/camel-plan-bar.js +0 -24
- package/dist/src/camel/camel-prompt-input.d.ts +0 -32
- package/dist/src/camel/camel-prompt-input.js +0 -182
- package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
- package/dist/src/camel/camel-tool-presentation.js +0 -784
- package/dist/src/camel/camel-turn.d.ts +0 -57
- package/dist/src/camel/camel-turn.js +0 -203
- package/dist/src/camel/index.js +0 -7
- package/dist/src/camel/turn-summary-bar.d.ts +0 -8
- package/dist/src/camel/turn-summary-bar.js +0 -12
- package/dist/src/chat-summary-panel.d.ts +0 -22
- package/dist/src/chat-summary-panel.js +0 -26
- package/dist/src/composer/chat-composer.d.ts +0 -4
- package/dist/src/composer/chat-composer.js +0 -90
- package/dist/src/composer/composer-attachments.d.ts +0 -13
- package/dist/src/composer/composer-attachments.js +0 -47
- package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
- package/dist/src/composer/composer-trigger-popover.js +0 -29
- package/dist/src/composer/composer.d.ts +0 -15
- package/dist/src/composer/composer.js +0 -180
- package/dist/src/composer/drop-controller.d.ts +0 -8
- package/dist/src/composer/drop-controller.js +0 -69
- package/dist/src/composer/index.js +0 -6
- package/dist/src/composer/types.d.ts +0 -62
- package/dist/src/composer/types.js +0 -1
- package/dist/src/contracts.d.ts +0 -94
- package/dist/src/contracts.js +0 -1
- package/dist/src/conversation.d.ts +0 -13
- package/dist/src/conversation.js +0 -18
- package/dist/src/index.js +0 -12
- package/dist/src/internal/cn.d.ts +0 -1
- package/dist/src/internal/cn.js +0 -3
- package/dist/src/message-panel.d.ts +0 -4
- package/dist/src/message-panel.js +0 -144
- package/dist/src/message.d.ts +0 -36
- package/dist/src/message.js +0 -83
- package/dist/src/parts/index.d.ts +0 -187
- package/dist/src/parts/index.js +0 -332
- package/dist/src/parts/kind.d.ts +0 -3
- package/dist/src/parts/kind.js +0 -69
- package/dist/src/parts/plan.d.ts +0 -10
- package/dist/src/parts/plan.js +0 -40
- package/dist/src/parts/primitives.d.ts +0 -70
- package/dist/src/parts/primitives.js +0 -153
- package/dist/src/registry.d.ts +0 -4
- package/dist/src/registry.js +0 -31
- package/dist/src/status.d.ts +0 -26
- package/dist/src/status.js +0 -74
- /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
- /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
- /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
- /package/{dist/src/index.d.ts → src/index.ts} +0 -0
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertCircleIcon,
|
|
3
|
+
ArrowRightIcon,
|
|
4
|
+
AtomIcon,
|
|
5
|
+
BarChart3Icon,
|
|
6
|
+
BellIcon,
|
|
7
|
+
BlocksIcon,
|
|
8
|
+
BotIcon,
|
|
9
|
+
BrainCircuitIcon,
|
|
10
|
+
CheckCircle2Icon,
|
|
11
|
+
CheckIcon,
|
|
12
|
+
ChevronDownIcon,
|
|
13
|
+
ChevronRightIcon,
|
|
14
|
+
CircleDotIcon,
|
|
15
|
+
CircleIcon,
|
|
16
|
+
Code2Icon,
|
|
17
|
+
DownloadIcon,
|
|
18
|
+
ExternalLinkIcon,
|
|
19
|
+
EyeIcon,
|
|
20
|
+
FileAudioIcon,
|
|
21
|
+
FileCodeIcon,
|
|
22
|
+
FileImageIcon,
|
|
23
|
+
FileSpreadsheetIcon,
|
|
24
|
+
FileTextIcon,
|
|
25
|
+
GlobeIcon,
|
|
26
|
+
ImageIcon,
|
|
27
|
+
Loader2Icon,
|
|
28
|
+
ListTodoIcon,
|
|
29
|
+
Maximize2Icon,
|
|
30
|
+
NewspaperIcon,
|
|
31
|
+
OrbitIcon,
|
|
32
|
+
RefreshCcwIcon,
|
|
33
|
+
SparklesIcon,
|
|
34
|
+
TerminalIcon,
|
|
35
|
+
TriangleAlertIcon,
|
|
36
|
+
WrenchIcon,
|
|
37
|
+
XIcon,
|
|
38
|
+
} from "lucide-react";
|
|
39
|
+
import {
|
|
40
|
+
useMemo,
|
|
41
|
+
useState,
|
|
42
|
+
type CSSProperties,
|
|
43
|
+
type HTMLAttributes,
|
|
44
|
+
type ReactNode,
|
|
45
|
+
} from "react";
|
|
46
|
+
import type {
|
|
47
|
+
MessagePanelActions,
|
|
48
|
+
PartRendererContext,
|
|
49
|
+
RichPartKind,
|
|
50
|
+
AIToolPart,
|
|
51
|
+
} from "../contracts";
|
|
52
|
+
import { cn } from "../internal/cn";
|
|
53
|
+
import { MessageResponse } from "../message";
|
|
54
|
+
import { ThinkingIndicator } from "../status";
|
|
55
|
+
import { resolvePartKind, toolNameOfPart } from "./kind";
|
|
56
|
+
import {
|
|
57
|
+
planStepsOfPart,
|
|
58
|
+
type PlanStep,
|
|
59
|
+
type PlanStepStatus,
|
|
60
|
+
} from "./plan";
|
|
61
|
+
import {
|
|
62
|
+
Confirmation,
|
|
63
|
+
ConfirmationAccepted,
|
|
64
|
+
ConfirmationAction,
|
|
65
|
+
ConfirmationActions,
|
|
66
|
+
ConfirmationRejected,
|
|
67
|
+
ConfirmationRequest,
|
|
68
|
+
ConfirmationTitle,
|
|
69
|
+
Plan,
|
|
70
|
+
PlanContent,
|
|
71
|
+
PlanHeader,
|
|
72
|
+
PlanTitle,
|
|
73
|
+
Reasoning,
|
|
74
|
+
ReasoningContent,
|
|
75
|
+
ReasoningTrigger,
|
|
76
|
+
} from "./primitives";
|
|
77
|
+
|
|
78
|
+
export { resolvePartKind, toolNameOfPart } from "./kind";
|
|
79
|
+
export {
|
|
80
|
+
planStepsOfPart,
|
|
81
|
+
projectLatestPlan,
|
|
82
|
+
type PlanStep,
|
|
83
|
+
type PlanStepStatus,
|
|
84
|
+
} from "./plan";
|
|
85
|
+
|
|
86
|
+
export interface TextPartProps {
|
|
87
|
+
text: string;
|
|
88
|
+
streaming?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const OUT_OF_BAND_PREFIX = "[Delivered to the user out of band]";
|
|
92
|
+
|
|
93
|
+
export function stripNoticePrefix(text: string): string {
|
|
94
|
+
const trimmed = text.trimStart();
|
|
95
|
+
return trimmed.startsWith(OUT_OF_BAND_PREFIX)
|
|
96
|
+
? trimmed.slice(OUT_OF_BAND_PREFIX.length).trim()
|
|
97
|
+
: text.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function TextPart({ text, streaming = false }: TextPartProps) {
|
|
101
|
+
if (!text.trim()) return null;
|
|
102
|
+
if (text.trimStart().startsWith(OUT_OF_BAND_PREFIX)) {
|
|
103
|
+
return <div className="sb-notice-line">{stripNoticePrefix(text)}</div>;
|
|
104
|
+
}
|
|
105
|
+
return (
|
|
106
|
+
<div data-part-type="text">
|
|
107
|
+
<MessageResponse streaming={streaming}>{text}</MessageResponse>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ReasoningPartProps {
|
|
113
|
+
text: string;
|
|
114
|
+
label?: string;
|
|
115
|
+
streaming?: boolean;
|
|
116
|
+
duration?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function ReasoningPart({
|
|
120
|
+
text,
|
|
121
|
+
label,
|
|
122
|
+
streaming = false,
|
|
123
|
+
duration,
|
|
124
|
+
}: ReasoningPartProps) {
|
|
125
|
+
if (!text.trim()) return null;
|
|
126
|
+
return (
|
|
127
|
+
<div data-part-type="reasoning">
|
|
128
|
+
<Reasoning isStreaming={streaming} defaultOpen={streaming} duration={duration}>
|
|
129
|
+
<ReasoningTrigger>{label}</ReasoningTrigger>
|
|
130
|
+
<ReasoningContent>{text}</ReasoningContent>
|
|
131
|
+
</Reasoning>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type ToolIcon = "web" | "news" | "code" | "image" | "file" | "terminal";
|
|
137
|
+
const toolIcons = {
|
|
138
|
+
web: GlobeIcon,
|
|
139
|
+
news: NewspaperIcon,
|
|
140
|
+
code: Code2Icon,
|
|
141
|
+
image: ImageIcon,
|
|
142
|
+
file: FileTextIcon,
|
|
143
|
+
terminal: TerminalIcon,
|
|
144
|
+
} as const;
|
|
145
|
+
|
|
146
|
+
function humanizeToolName(name: string): string {
|
|
147
|
+
return name
|
|
148
|
+
.replace(/^tool-/, "")
|
|
149
|
+
.replace(/[_-]+/g, " ")
|
|
150
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ToolPartProps {
|
|
154
|
+
name: string;
|
|
155
|
+
state: AIToolPart["state"] | "interrupted";
|
|
156
|
+
input?: unknown;
|
|
157
|
+
output?: unknown;
|
|
158
|
+
errorText?: string;
|
|
159
|
+
icon?: ToolIcon;
|
|
160
|
+
label?: string;
|
|
161
|
+
meta?: string;
|
|
162
|
+
results?: Array<{ title: string; domain: string }>;
|
|
163
|
+
command?: string;
|
|
164
|
+
isActive?: boolean;
|
|
165
|
+
activityId?: string;
|
|
166
|
+
onNavigate?: (target: string) => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ToolPart = AIToolPart;
|
|
170
|
+
|
|
171
|
+
export function ToolPart({
|
|
172
|
+
name,
|
|
173
|
+
state,
|
|
174
|
+
input,
|
|
175
|
+
output,
|
|
176
|
+
errorText,
|
|
177
|
+
icon,
|
|
178
|
+
label,
|
|
179
|
+
meta,
|
|
180
|
+
results,
|
|
181
|
+
command,
|
|
182
|
+
activityId,
|
|
183
|
+
onNavigate,
|
|
184
|
+
}: ToolPartProps) {
|
|
185
|
+
const Icon = icon ? toolIcons[icon] : WrenchIcon;
|
|
186
|
+
const [open, setOpen] = useState(false);
|
|
187
|
+
const title = label ?? humanizeToolName(name);
|
|
188
|
+
const running = state === "input-streaming" || state === "input-available";
|
|
189
|
+
const hasDetails =
|
|
190
|
+
input != null ||
|
|
191
|
+
output != null ||
|
|
192
|
+
Boolean(errorText || command || results?.length);
|
|
193
|
+
|
|
194
|
+
if (state === "interrupted") {
|
|
195
|
+
return (
|
|
196
|
+
<div className="sb-tool-failed" data-part-type="tool">
|
|
197
|
+
<TriangleAlertIcon size={16} />
|
|
198
|
+
<span>Interrupted</span>
|
|
199
|
+
<small>{title}</small>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (state === "output-error") {
|
|
205
|
+
return (
|
|
206
|
+
<div className="sb-tool-failed" data-part-type="tool">
|
|
207
|
+
<TriangleAlertIcon size={16} />
|
|
208
|
+
<span>Failed</span>
|
|
209
|
+
<small>{errorText || title}</small>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="sb-tool-line" data-part-type="tool">
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
className="sb-tool-line__trigger"
|
|
219
|
+
aria-expanded={open}
|
|
220
|
+
onClick={() => {
|
|
221
|
+
if (hasDetails) setOpen((current) => !current);
|
|
222
|
+
if (activityId) onNavigate?.(activityId);
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<Icon size={16} />
|
|
226
|
+
<span>{running ? `${title}…` : title}</span>
|
|
227
|
+
{meta && <small>{meta}</small>}
|
|
228
|
+
{running ? (
|
|
229
|
+
<Loader2Icon size={14} className="sb-spin" />
|
|
230
|
+
) : (
|
|
231
|
+
hasDetails && (
|
|
232
|
+
<ChevronRightIcon
|
|
233
|
+
size={15}
|
|
234
|
+
className={cn("sb-tool-line__chevron", open && "is-open")}
|
|
235
|
+
/>
|
|
236
|
+
)
|
|
237
|
+
)}
|
|
238
|
+
</button>
|
|
239
|
+
{open && (
|
|
240
|
+
<div className="sb-tool-line__details">
|
|
241
|
+
{results?.length ? (
|
|
242
|
+
<ul className="sb-tool-results">
|
|
243
|
+
{results.map((result, index) => (
|
|
244
|
+
<li key={`${result.domain}:${result.title}`}>
|
|
245
|
+
<i data-index={index % 2} />
|
|
246
|
+
<span>{result.title}</span>
|
|
247
|
+
<small>{result.domain}</small>
|
|
248
|
+
</li>
|
|
249
|
+
))}
|
|
250
|
+
</ul>
|
|
251
|
+
) : null}
|
|
252
|
+
{command && (
|
|
253
|
+
<div className="sb-terminal">
|
|
254
|
+
<code>$ {command}</code>
|
|
255
|
+
{typeof output === "string" && <pre>{output}</pre>}
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
{!command && input != null && (
|
|
259
|
+
<pre>{JSON.stringify(input, null, 2)}</pre>
|
|
260
|
+
)}
|
|
261
|
+
{!command && output != null && (
|
|
262
|
+
<pre>
|
|
263
|
+
{typeof output === "string"
|
|
264
|
+
? output
|
|
265
|
+
: JSON.stringify(output, null, 2)}
|
|
266
|
+
</pre>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function ErrorPart({
|
|
275
|
+
title = "Error",
|
|
276
|
+
body,
|
|
277
|
+
onRetry,
|
|
278
|
+
}: {
|
|
279
|
+
title?: string;
|
|
280
|
+
body: string;
|
|
281
|
+
onRetry?: () => void;
|
|
282
|
+
}) {
|
|
283
|
+
return (
|
|
284
|
+
<div className="sb-error-part" data-part-type="error" role="alert">
|
|
285
|
+
<AlertCircleIcon size={18} />
|
|
286
|
+
<div>
|
|
287
|
+
<strong>{title}</strong>
|
|
288
|
+
<p>{body}</p>
|
|
289
|
+
{onRetry && (
|
|
290
|
+
<button type="button" className="sb-link-button" onClick={onRetry}>
|
|
291
|
+
<RefreshCcwIcon size={14} />
|
|
292
|
+
重试
|
|
293
|
+
</button>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function ImagePart({
|
|
301
|
+
src,
|
|
302
|
+
alt,
|
|
303
|
+
onPreview,
|
|
304
|
+
}: {
|
|
305
|
+
src: string;
|
|
306
|
+
alt: string;
|
|
307
|
+
onPreview?: () => void;
|
|
308
|
+
}) {
|
|
309
|
+
return (
|
|
310
|
+
<figure className="sb-image-part" data-part-type="image">
|
|
311
|
+
<button type="button" onClick={onPreview} aria-label={`预览图片:${alt}`}>
|
|
312
|
+
<img src={src} alt={alt} />
|
|
313
|
+
{onPreview && <Maximize2Icon size={17} />}
|
|
314
|
+
</button>
|
|
315
|
+
<figcaption>{alt}</figcaption>
|
|
316
|
+
</figure>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface ChartBar {
|
|
321
|
+
label: string;
|
|
322
|
+
value: number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function ChartPart({
|
|
326
|
+
title,
|
|
327
|
+
unit,
|
|
328
|
+
bars,
|
|
329
|
+
expandable,
|
|
330
|
+
onPreview,
|
|
331
|
+
}: {
|
|
332
|
+
title: string;
|
|
333
|
+
unit?: string;
|
|
334
|
+
bars: readonly ChartBar[];
|
|
335
|
+
expandable?: boolean;
|
|
336
|
+
onPreview?: () => void;
|
|
337
|
+
}) {
|
|
338
|
+
const max = Math.max(...bars.map((bar) => bar.value), 1);
|
|
339
|
+
return (
|
|
340
|
+
<section className="sb-chart" data-part-type="chart" aria-label={title}>
|
|
341
|
+
<header>
|
|
342
|
+
<span>
|
|
343
|
+
<BarChart3Icon size={17} />
|
|
344
|
+
<strong>{title}</strong>
|
|
345
|
+
</span>
|
|
346
|
+
{expandable && (
|
|
347
|
+
<button type="button" onClick={onPreview} aria-label="展开图表">
|
|
348
|
+
<Maximize2Icon size={16} />
|
|
349
|
+
</button>
|
|
350
|
+
)}
|
|
351
|
+
</header>
|
|
352
|
+
<div className="sb-chart__plot">
|
|
353
|
+
{bars.map((bar) => (
|
|
354
|
+
<div key={bar.label} className="sb-chart__bar-group">
|
|
355
|
+
<span>{bar.value}{unit}</span>
|
|
356
|
+
<i style={{ "--sb-bar-height": `${(bar.value / max) * 100}%` } as CSSProperties} />
|
|
357
|
+
<small>{bar.label}</small>
|
|
358
|
+
</div>
|
|
359
|
+
))}
|
|
360
|
+
</div>
|
|
361
|
+
</section>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function AgentCardPart({
|
|
366
|
+
title,
|
|
367
|
+
agentName,
|
|
368
|
+
agentDesc,
|
|
369
|
+
onNavigate,
|
|
370
|
+
}: {
|
|
371
|
+
title: string;
|
|
372
|
+
agentName: string;
|
|
373
|
+
agentDesc: string;
|
|
374
|
+
onNavigate?: () => void;
|
|
375
|
+
}) {
|
|
376
|
+
return (
|
|
377
|
+
<section className="sb-agent-card" data-part-type="agent_card">
|
|
378
|
+
<small>{title}</small>
|
|
379
|
+
<div>
|
|
380
|
+
<span className="sb-agent-card__icon"><BotIcon size={20} /></span>
|
|
381
|
+
<div>
|
|
382
|
+
<strong>{agentName}</strong>
|
|
383
|
+
<p>{agentDesc}</p>
|
|
384
|
+
</div>
|
|
385
|
+
{onNavigate && (
|
|
386
|
+
<button type="button" onClick={onNavigate} aria-label={`打开 ${agentName}`}>
|
|
387
|
+
<ArrowRightIcon size={17} />
|
|
388
|
+
</button>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
</section>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function StepStatusIcon({
|
|
396
|
+
status,
|
|
397
|
+
animated = false,
|
|
398
|
+
}: {
|
|
399
|
+
status: PlanStepStatus;
|
|
400
|
+
animated?: boolean;
|
|
401
|
+
}) {
|
|
402
|
+
if (status === "done") return <CheckCircle2Icon size={16} data-tone="success" />;
|
|
403
|
+
if (status === "in_progress") {
|
|
404
|
+
return animated ? (
|
|
405
|
+
<Loader2Icon size={16} className="sb-spin" data-tone="active" />
|
|
406
|
+
) : (
|
|
407
|
+
<CircleDotIcon size={16} data-tone="muted" />
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return <CircleIcon size={16} data-tone="muted" />;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function PlanPart({
|
|
414
|
+
title,
|
|
415
|
+
steps,
|
|
416
|
+
streaming = false,
|
|
417
|
+
}: {
|
|
418
|
+
title: string;
|
|
419
|
+
steps: readonly PlanStep[];
|
|
420
|
+
streaming?: boolean;
|
|
421
|
+
}) {
|
|
422
|
+
const done = steps.filter((step) => step.status === "done").length;
|
|
423
|
+
const allDone = steps.length > 0 && done === steps.length;
|
|
424
|
+
const animated = streaming && !allDone;
|
|
425
|
+
return (
|
|
426
|
+
<Plan
|
|
427
|
+
className="sb-plan-card"
|
|
428
|
+
data-part-type="plan"
|
|
429
|
+
isStreaming={animated}
|
|
430
|
+
>
|
|
431
|
+
<PlanHeader>
|
|
432
|
+
<div className="sb-plan__heading">
|
|
433
|
+
<ListTodoIcon size={18} />
|
|
434
|
+
<PlanTitle>{title}</PlanTitle>
|
|
435
|
+
</div>
|
|
436
|
+
{steps.length > 0 && (
|
|
437
|
+
<span
|
|
438
|
+
className="sb-plan__badge"
|
|
439
|
+
data-state={allDone ? "done" : animated ? "running" : "paused"}
|
|
440
|
+
>
|
|
441
|
+
{allDone && <CheckCircle2Icon size={13} />}
|
|
442
|
+
{allDone
|
|
443
|
+
? "Done"
|
|
444
|
+
: `${animated ? "Running · " : ""}${done}/${steps.length}`}
|
|
445
|
+
</span>
|
|
446
|
+
)}
|
|
447
|
+
</PlanHeader>
|
|
448
|
+
<PlanContent>
|
|
449
|
+
{steps.length > 0 ? (
|
|
450
|
+
<ol className="sb-plan__steps">
|
|
451
|
+
{steps.map((step, index) => {
|
|
452
|
+
const status =
|
|
453
|
+
step.status ??
|
|
454
|
+
(animated && index === done ? "in_progress" : "pending");
|
|
455
|
+
return (
|
|
456
|
+
<li key={`${index}:${step.text}`} data-status={status}>
|
|
457
|
+
<StepStatusIcon
|
|
458
|
+
status={status}
|
|
459
|
+
animated={animated && status === "in_progress"}
|
|
460
|
+
/>
|
|
461
|
+
<span>{step.text}</span>
|
|
462
|
+
</li>
|
|
463
|
+
);
|
|
464
|
+
})}
|
|
465
|
+
</ol>
|
|
466
|
+
) : animated ? (
|
|
467
|
+
<div className="sb-plan__skeleton" role="status" aria-label="正在生成计划">
|
|
468
|
+
{[72, 56, 40].map((width) => (
|
|
469
|
+
<span key={width}>
|
|
470
|
+
<i />
|
|
471
|
+
<b style={{ width: `${width}%` }} />
|
|
472
|
+
</span>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
) : (
|
|
476
|
+
<p className="sb-plan__empty">(空计划)</p>
|
|
477
|
+
)}
|
|
478
|
+
</PlanContent>
|
|
479
|
+
</Plan>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function SchedulePart({
|
|
484
|
+
title,
|
|
485
|
+
cadence,
|
|
486
|
+
nextRun,
|
|
487
|
+
scheduled = false,
|
|
488
|
+
onSchedule,
|
|
489
|
+
}: {
|
|
490
|
+
title: string;
|
|
491
|
+
cadence: string;
|
|
492
|
+
nextRun: string;
|
|
493
|
+
scheduled?: boolean;
|
|
494
|
+
onSchedule?: () => void;
|
|
495
|
+
}) {
|
|
496
|
+
return (
|
|
497
|
+
<section className="sb-schedule" data-part-type="schedule">
|
|
498
|
+
<BellIcon size={18} />
|
|
499
|
+
<div>
|
|
500
|
+
<strong>{title}</strong>
|
|
501
|
+
<p>{cadence}</p>
|
|
502
|
+
<small>{nextRun}</small>
|
|
503
|
+
</div>
|
|
504
|
+
<button type="button" className="sb-button" onClick={onSchedule} disabled={scheduled}>
|
|
505
|
+
{scheduled ? "已安排" : "安排"}
|
|
506
|
+
</button>
|
|
507
|
+
</section>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface ParallelTool {
|
|
512
|
+
icon?: Exclude<ToolIcon, "image" | "terminal">;
|
|
513
|
+
label: string;
|
|
514
|
+
meta?: string;
|
|
515
|
+
status?: "running" | "done" | "failed";
|
|
516
|
+
results?: Array<{ title: string; domain: string }>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function RunningRow({ tool }: { tool: ParallelTool }) {
|
|
520
|
+
const Icon = tool.icon ? toolIcons[tool.icon] : WrenchIcon;
|
|
521
|
+
return (
|
|
522
|
+
<div className="sb-running-row">
|
|
523
|
+
<Icon size={15} />
|
|
524
|
+
<span>{tool.label}</span>
|
|
525
|
+
<Loader2Icon size={14} className="sb-spin" />
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function CollapsedRow({ tool }: { tool: ParallelTool }) {
|
|
531
|
+
const Icon = tool.icon ? toolIcons[tool.icon] : WrenchIcon;
|
|
532
|
+
return (
|
|
533
|
+
<div className="sb-collapsed-row">
|
|
534
|
+
<Icon size={15} />
|
|
535
|
+
<span>{tool.label}</span>
|
|
536
|
+
<small>{tool.meta}</small>
|
|
537
|
+
{tool.status === "failed" ? <XIcon size={14} /> : <CheckIcon size={14} />}
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function ParallelPart({
|
|
543
|
+
label = "Parallel tasks",
|
|
544
|
+
tools,
|
|
545
|
+
isActive = true,
|
|
546
|
+
}: {
|
|
547
|
+
label?: string;
|
|
548
|
+
tools: readonly ParallelTool[];
|
|
549
|
+
isActive?: boolean;
|
|
550
|
+
}) {
|
|
551
|
+
const active = isActive && tools.some((tool) => tool.status === "running");
|
|
552
|
+
return (
|
|
553
|
+
<details className="sb-parallel" data-part-type="parallel" open={active}>
|
|
554
|
+
<summary>
|
|
555
|
+
<strong>{label}</strong>
|
|
556
|
+
<span>{tools.filter((tool) => tool.status === "done").length}/{tools.length}</span>
|
|
557
|
+
<ChevronDownIcon size={16} />
|
|
558
|
+
</summary>
|
|
559
|
+
<div>
|
|
560
|
+
{tools.map((tool, index) =>
|
|
561
|
+
tool.status === "running" && isActive ? (
|
|
562
|
+
<RunningRow key={`${index}:${tool.label}`} tool={tool} />
|
|
563
|
+
) : (
|
|
564
|
+
<CollapsedRow
|
|
565
|
+
key={`${index}:${tool.label}`}
|
|
566
|
+
tool={
|
|
567
|
+
tool.status === "running"
|
|
568
|
+
? { ...tool, status: "failed" }
|
|
569
|
+
: tool
|
|
570
|
+
}
|
|
571
|
+
/>
|
|
572
|
+
),
|
|
573
|
+
)}
|
|
574
|
+
</div>
|
|
575
|
+
</details>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export interface SubAgent {
|
|
580
|
+
name: string;
|
|
581
|
+
tint?: string;
|
|
582
|
+
summary: string;
|
|
583
|
+
status?: "running" | "done" | "failed";
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const SUBAGENT_VISUALS = [
|
|
587
|
+
{ icon: SparklesIcon, name: "sparkles", color: "#e59a45" },
|
|
588
|
+
{ icon: AtomIcon, name: "atom", color: "#9b7bd1" },
|
|
589
|
+
{ icon: BrainCircuitIcon, name: "brain", color: "#9b7bd1" },
|
|
590
|
+
{ icon: OrbitIcon, name: "orbit", color: "#e59a45" },
|
|
591
|
+
{ icon: BlocksIcon, name: "blocks", color: "#e59a45" },
|
|
592
|
+
] as const;
|
|
593
|
+
|
|
594
|
+
function subAgentVisual(name: string) {
|
|
595
|
+
let hash = 0;
|
|
596
|
+
for (const character of name) {
|
|
597
|
+
hash = (hash * 31 + (character.codePointAt(0) ?? 0)) >>> 0;
|
|
598
|
+
}
|
|
599
|
+
return SUBAGENT_VISUALS[hash % SUBAGENT_VISUALS.length] ?? SUBAGENT_VISUALS[0];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function SubAgentsPart({
|
|
603
|
+
agents,
|
|
604
|
+
isActive = true,
|
|
605
|
+
}: {
|
|
606
|
+
agents: readonly SubAgent[];
|
|
607
|
+
isActive?: boolean;
|
|
608
|
+
}) {
|
|
609
|
+
return (
|
|
610
|
+
<div className="sb-subagents" data-part-type="subagents">
|
|
611
|
+
{agents.map((agent, index) => {
|
|
612
|
+
const visual = subAgentVisual(agent.name);
|
|
613
|
+
const AgentIcon = visual.icon;
|
|
614
|
+
return (
|
|
615
|
+
<article key={`${index}:${agent.name}`} data-state={agent.status ?? "done"}>
|
|
616
|
+
<span
|
|
617
|
+
data-subagent-icon={visual.name}
|
|
618
|
+
style={{ color: agent.tint ?? visual.color }}
|
|
619
|
+
aria-hidden="true"
|
|
620
|
+
>
|
|
621
|
+
<AgentIcon size={14} strokeWidth={2.25} />
|
|
622
|
+
</span>
|
|
623
|
+
<div>
|
|
624
|
+
<strong>{agent.name}</strong>
|
|
625
|
+
{agent.status === "running" && isActive ? (
|
|
626
|
+
<small><Loader2Icon size={12} className="sb-spin" />Running…</small>
|
|
627
|
+
) : (
|
|
628
|
+
<p>{agent.summary || (agent.status === "running" ? "Interrupted" : "")}</p>
|
|
629
|
+
)}
|
|
630
|
+
</div>
|
|
631
|
+
</article>
|
|
632
|
+
);
|
|
633
|
+
})}
|
|
634
|
+
</div>
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function ConfirmationPart({
|
|
639
|
+
id = "confirmation",
|
|
640
|
+
title,
|
|
641
|
+
body,
|
|
642
|
+
confirmLabel = "确认",
|
|
643
|
+
cancelLabel = "取消",
|
|
644
|
+
state,
|
|
645
|
+
onConfirm,
|
|
646
|
+
}: {
|
|
647
|
+
id?: string;
|
|
648
|
+
title: string;
|
|
649
|
+
body?: string;
|
|
650
|
+
confirmLabel?: string;
|
|
651
|
+
cancelLabel?: string;
|
|
652
|
+
state?: "pending" | "confirmed" | "cancelled";
|
|
653
|
+
onConfirm?: (confirmed: boolean) => void | Promise<void>;
|
|
654
|
+
}) {
|
|
655
|
+
const [internalState, setInternalState] = useState(state ?? "pending");
|
|
656
|
+
const resolvedState = state ?? internalState;
|
|
657
|
+
const decide = async (confirmed: boolean) => {
|
|
658
|
+
await onConfirm?.(confirmed);
|
|
659
|
+
if (state === undefined) setInternalState(confirmed ? "confirmed" : "cancelled");
|
|
660
|
+
};
|
|
661
|
+
return (
|
|
662
|
+
<div data-part-type="confirmation">
|
|
663
|
+
<Confirmation state={resolvedState} approval={{ id, approved: resolvedState === "pending" ? undefined : resolvedState === "confirmed" }}>
|
|
664
|
+
<ConfirmationTitle>
|
|
665
|
+
<AlertCircleIcon size={18} />
|
|
666
|
+
<span>
|
|
667
|
+
<ConfirmationRequest>{title}</ConfirmationRequest>
|
|
668
|
+
<ConfirmationAccepted>已确认:{title}</ConfirmationAccepted>
|
|
669
|
+
<ConfirmationRejected>已取消:{title}</ConfirmationRejected>
|
|
670
|
+
</span>
|
|
671
|
+
</ConfirmationTitle>
|
|
672
|
+
{body && <p>{body}</p>}
|
|
673
|
+
<ConfirmationActions>
|
|
674
|
+
<ConfirmationAction className="sb-button--secondary" onClick={() => void decide(false)}>
|
|
675
|
+
{cancelLabel}
|
|
676
|
+
</ConfirmationAction>
|
|
677
|
+
<ConfirmationAction onClick={() => void decide(true)}>
|
|
678
|
+
{confirmLabel}
|
|
679
|
+
</ConfirmationAction>
|
|
680
|
+
</ConfirmationActions>
|
|
681
|
+
</Confirmation>
|
|
682
|
+
</div>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function ToolApprovalPresentation({
|
|
687
|
+
children,
|
|
688
|
+
toolName,
|
|
689
|
+
state,
|
|
690
|
+
context,
|
|
691
|
+
}: {
|
|
692
|
+
children: ReactNode;
|
|
693
|
+
toolName: string;
|
|
694
|
+
state: AIToolPart["state"];
|
|
695
|
+
context: PartRendererContext;
|
|
696
|
+
}) {
|
|
697
|
+
const approval = context.approval;
|
|
698
|
+
if (!approval) return <>{children}</>;
|
|
699
|
+
return (
|
|
700
|
+
<div className="sb-tool-stack">
|
|
701
|
+
{children}
|
|
702
|
+
<Confirmation approval={approval} state={state}>
|
|
703
|
+
<ConfirmationTitle>
|
|
704
|
+
<ConfirmationRequest>
|
|
705
|
+
<strong>{humanizeToolName(toolName)}</strong> 请求执行,需要你的批准。
|
|
706
|
+
</ConfirmationRequest>
|
|
707
|
+
<ConfirmationAccepted>
|
|
708
|
+
已批准 {humanizeToolName(toolName)},继续执行。
|
|
709
|
+
</ConfirmationAccepted>
|
|
710
|
+
<ConfirmationRejected>
|
|
711
|
+
已拒绝 {humanizeToolName(toolName)}。
|
|
712
|
+
</ConfirmationRejected>
|
|
713
|
+
</ConfirmationTitle>
|
|
714
|
+
<ConfirmationActions>
|
|
715
|
+
<ConfirmationAction
|
|
716
|
+
className="sb-button--secondary"
|
|
717
|
+
onClick={() => void context.actions.onApprove?.(approval.id, "deny")}
|
|
718
|
+
>
|
|
719
|
+
拒绝
|
|
720
|
+
</ConfirmationAction>
|
|
721
|
+
<ConfirmationAction
|
|
722
|
+
className="sb-button--secondary"
|
|
723
|
+
onClick={() => void context.actions.onApprove?.(approval.id, "allow_once")}
|
|
724
|
+
>
|
|
725
|
+
仅本次允许
|
|
726
|
+
</ConfirmationAction>
|
|
727
|
+
<ConfirmationAction
|
|
728
|
+
onClick={() => void context.actions.onApprove?.(approval.id, "allow_level")}
|
|
729
|
+
>
|
|
730
|
+
允许该档位
|
|
731
|
+
</ConfirmationAction>
|
|
732
|
+
</ConfirmationActions>
|
|
733
|
+
</Confirmation>
|
|
734
|
+
</div>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export type FileKind = "doc" | "image" | "html" | "audio" | "csv" | "code";
|
|
739
|
+
const fileIcons = {
|
|
740
|
+
doc: FileTextIcon,
|
|
741
|
+
image: FileImageIcon,
|
|
742
|
+
html: Code2Icon,
|
|
743
|
+
audio: FileAudioIcon,
|
|
744
|
+
csv: FileSpreadsheetIcon,
|
|
745
|
+
code: FileCodeIcon,
|
|
746
|
+
} as const;
|
|
747
|
+
|
|
748
|
+
export function FilePart({
|
|
749
|
+
name,
|
|
750
|
+
kind = "doc",
|
|
751
|
+
note,
|
|
752
|
+
url,
|
|
753
|
+
artifact,
|
|
754
|
+
onPreview,
|
|
755
|
+
onDownload,
|
|
756
|
+
}: {
|
|
757
|
+
name: string;
|
|
758
|
+
kind?: FileKind;
|
|
759
|
+
note?: string;
|
|
760
|
+
url?: string;
|
|
761
|
+
artifact?: unknown;
|
|
762
|
+
onPreview?: (artifact: unknown) => void;
|
|
763
|
+
onDownload?: (artifact: unknown) => void;
|
|
764
|
+
}) {
|
|
765
|
+
const Icon = fileIcons[kind];
|
|
766
|
+
const payload = artifact ?? { name, kind, note, url };
|
|
767
|
+
return (
|
|
768
|
+
<div className="sb-file-part" data-part-type="file">
|
|
769
|
+
<span><Icon size={19} /></span>
|
|
770
|
+
<div>
|
|
771
|
+
<strong>{name}</strong>
|
|
772
|
+
{note && <small>{note}</small>}
|
|
773
|
+
</div>
|
|
774
|
+
<div>
|
|
775
|
+
{onPreview && (
|
|
776
|
+
<button type="button" onClick={() => onPreview(payload)} aria-label={`预览 ${name}`}>
|
|
777
|
+
<EyeIcon size={16} />
|
|
778
|
+
</button>
|
|
779
|
+
)}
|
|
780
|
+
{onDownload ? (
|
|
781
|
+
<button type="button" onClick={() => onDownload(payload)} aria-label={`下载 ${name}`}>
|
|
782
|
+
<DownloadIcon size={16} />
|
|
783
|
+
</button>
|
|
784
|
+
) : url ? (
|
|
785
|
+
<a href={url} target="_blank" rel="noreferrer" download={name} aria-label={`打开 ${name}`}>
|
|
786
|
+
<ExternalLinkIcon size={16} />
|
|
787
|
+
</a>
|
|
788
|
+
) : null}
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export interface DashboardData {
|
|
795
|
+
title: string;
|
|
796
|
+
cards?: Array<{ icon?: string; title: string; body: string }>;
|
|
797
|
+
tiles?: Array<{ label: string; value: string }>;
|
|
798
|
+
badges?: string[];
|
|
799
|
+
fileNote?: { title: string; body: string };
|
|
800
|
+
table?: { columns: string[]; rows: string[][] };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function DashboardPart(data: DashboardData) {
|
|
804
|
+
return (
|
|
805
|
+
<section className="sb-dashboard" data-part-type="dashboard">
|
|
806
|
+
<h3>{data.title}</h3>
|
|
807
|
+
{data.tiles?.length ? (
|
|
808
|
+
<div className="sb-dashboard__tiles">
|
|
809
|
+
{data.tiles.map((tile) => (
|
|
810
|
+
<article key={tile.label}><strong>{tile.value}</strong><small>{tile.label}</small></article>
|
|
811
|
+
))}
|
|
812
|
+
</div>
|
|
813
|
+
) : null}
|
|
814
|
+
{data.cards?.length ? (
|
|
815
|
+
<div className="sb-dashboard__cards">
|
|
816
|
+
{data.cards.map((card) => (
|
|
817
|
+
<article key={card.title}><span>{card.icon}</span><strong>{card.title}</strong><p>{card.body}</p></article>
|
|
818
|
+
))}
|
|
819
|
+
</div>
|
|
820
|
+
) : null}
|
|
821
|
+
{data.badges?.length ? <div className="sb-dashboard__badges">{data.badges.map((badge) => <span key={badge}>{badge}</span>)}</div> : null}
|
|
822
|
+
{data.fileNote && <aside><strong>{data.fileNote.title}</strong><p>{data.fileNote.body}</p></aside>}
|
|
823
|
+
{data.table && (
|
|
824
|
+
<div className="sb-table-scroll">
|
|
825
|
+
<table>
|
|
826
|
+
<thead><tr>{data.table.columns.map((column) => <th key={column}>{column}</th>)}</tr></thead>
|
|
827
|
+
<tbody>{data.table.rows.map((row, index) => <tr key={index}>{row.map((cell, cellIndex) => <td key={cellIndex}>{cell}</td>)}</tr>)}</tbody>
|
|
828
|
+
</table>
|
|
829
|
+
</div>
|
|
830
|
+
)}
|
|
831
|
+
</section>
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export interface AskQuestion {
|
|
836
|
+
prompt: string;
|
|
837
|
+
kind: "single" | "multi";
|
|
838
|
+
options: string[];
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export function AskUserPart({
|
|
842
|
+
questions,
|
|
843
|
+
freeTextPlaceholder,
|
|
844
|
+
submittedAnswer,
|
|
845
|
+
onSubmit,
|
|
846
|
+
}: {
|
|
847
|
+
questions: readonly AskQuestion[];
|
|
848
|
+
freeTextPlaceholder?: string;
|
|
849
|
+
submittedAnswer?: string;
|
|
850
|
+
onSubmit?: (payload: { selections: string[][]; text: string }) => void | Promise<void>;
|
|
851
|
+
}) {
|
|
852
|
+
const [selections, setSelections] = useState<string[][]>(() => questions.map(() => []));
|
|
853
|
+
const [text, setText] = useState("");
|
|
854
|
+
const [submitted, setSubmitted] = useState(false);
|
|
855
|
+
const externalAnswer = submittedAnswer?.trim() ?? "";
|
|
856
|
+
const externalSelections = useMemo(
|
|
857
|
+
() =>
|
|
858
|
+
new Set(
|
|
859
|
+
externalAnswer
|
|
860
|
+
? externalAnswer.split("、").map((item) => item.trim()).filter(Boolean)
|
|
861
|
+
: [],
|
|
862
|
+
),
|
|
863
|
+
[externalAnswer],
|
|
864
|
+
);
|
|
865
|
+
const resolvedSubmitted = submitted || Boolean(externalAnswer);
|
|
866
|
+
const valid = questions.every(
|
|
867
|
+
(question, index) => question.kind !== "single" || selections[index].length === 1,
|
|
868
|
+
);
|
|
869
|
+
const toggle = (questionIndex: number, option: string) => {
|
|
870
|
+
if (resolvedSubmitted) return;
|
|
871
|
+
setSelections((current) =>
|
|
872
|
+
current.map((selected, index) => {
|
|
873
|
+
if (index !== questionIndex) return selected;
|
|
874
|
+
if (questions[index].kind === "single") return [option];
|
|
875
|
+
return selected.includes(option)
|
|
876
|
+
? selected.filter((item) => item !== option)
|
|
877
|
+
: [...selected, option];
|
|
878
|
+
}),
|
|
879
|
+
);
|
|
880
|
+
};
|
|
881
|
+
const submit = async () => {
|
|
882
|
+
if (!valid || resolvedSubmitted) return;
|
|
883
|
+
await onSubmit?.({ selections, text: text.trim() });
|
|
884
|
+
setSubmitted(true);
|
|
885
|
+
};
|
|
886
|
+
const resolvedAnswer =
|
|
887
|
+
externalAnswer ||
|
|
888
|
+
[...selections.flat(), text.trim()].filter(Boolean).join("、");
|
|
889
|
+
return (
|
|
890
|
+
<section
|
|
891
|
+
className="sb-ask-user"
|
|
892
|
+
data-part-type="ask_user"
|
|
893
|
+
data-state={resolvedSubmitted ? "submitted" : "pending"}
|
|
894
|
+
>
|
|
895
|
+
{questions.map((question, questionIndex) => (
|
|
896
|
+
<fieldset
|
|
897
|
+
key={`${questionIndex}:${question.prompt}`}
|
|
898
|
+
disabled={resolvedSubmitted}
|
|
899
|
+
>
|
|
900
|
+
<legend>{question.prompt}</legend>
|
|
901
|
+
<div>
|
|
902
|
+
{question.options.map((option) => {
|
|
903
|
+
const selected = externalAnswer
|
|
904
|
+
? externalSelections.has(option)
|
|
905
|
+
: selections[questionIndex].includes(option);
|
|
906
|
+
return (
|
|
907
|
+
<button
|
|
908
|
+
key={option}
|
|
909
|
+
type="button"
|
|
910
|
+
aria-pressed={selected}
|
|
911
|
+
onClick={() => toggle(questionIndex, option)}
|
|
912
|
+
>
|
|
913
|
+
{question.kind === "multi" && <span>{selected && <CheckIcon size={12} />}</span>}
|
|
914
|
+
{selected && question.kind === "single" && <CheckIcon size={14} />}
|
|
915
|
+
{option}
|
|
916
|
+
</button>
|
|
917
|
+
);
|
|
918
|
+
})}
|
|
919
|
+
</div>
|
|
920
|
+
</fieldset>
|
|
921
|
+
))}
|
|
922
|
+
{freeTextPlaceholder && !resolvedSubmitted && (
|
|
923
|
+
<textarea value={text} onChange={(event) => setText(event.currentTarget.value)} placeholder={freeTextPlaceholder} />
|
|
924
|
+
)}
|
|
925
|
+
{resolvedSubmitted ? (
|
|
926
|
+
<div className="sb-ask-user__submitted">
|
|
927
|
+
<CheckIcon size={15} />
|
|
928
|
+
<span>Submitted</span>
|
|
929
|
+
{resolvedAnswer && <small>{resolvedAnswer}</small>}
|
|
930
|
+
</div>
|
|
931
|
+
) : (
|
|
932
|
+
<button type="button" className="sb-button" disabled={!valid} onClick={() => void submit()}>
|
|
933
|
+
提交<ArrowRightIcon size={15} />
|
|
934
|
+
</button>
|
|
935
|
+
)}
|
|
936
|
+
</section>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export function Suggestions({
|
|
941
|
+
items,
|
|
942
|
+
onPick,
|
|
943
|
+
}: {
|
|
944
|
+
items: readonly string[];
|
|
945
|
+
onPick?: (text: string) => void | Promise<void>;
|
|
946
|
+
}) {
|
|
947
|
+
return (
|
|
948
|
+
<div className="sb-suggestions" data-part-type="suggestions">
|
|
949
|
+
{items.map((item) => (
|
|
950
|
+
<button type="button" key={item} onClick={() => void onPick?.(item)}>
|
|
951
|
+
<ArrowRightIcon size={15} />
|
|
952
|
+
<span>{item}</span>
|
|
953
|
+
</button>
|
|
954
|
+
))}
|
|
955
|
+
</div>
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export function InlineText({ children }: { children: ReactNode }) {
|
|
960
|
+
return <span className="sb-inline-text">{children}</span>;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export function PartLoading({
|
|
964
|
+
kind,
|
|
965
|
+
label,
|
|
966
|
+
}: {
|
|
967
|
+
kind: RichPartKind;
|
|
968
|
+
label?: string;
|
|
969
|
+
}) {
|
|
970
|
+
if (kind === "reasoning") return <ThinkingIndicator />;
|
|
971
|
+
if (kind === "tool") return <RunningRow tool={{ label: label ?? "Running tool", status: "running" }} />;
|
|
972
|
+
return (
|
|
973
|
+
<div className="sb-part-loading" data-kind={kind} role="status">
|
|
974
|
+
<span />
|
|
975
|
+
<span />
|
|
976
|
+
<span />
|
|
977
|
+
</div>
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export function UnknownPart({ part }: { part: unknown }) {
|
|
982
|
+
const type =
|
|
983
|
+
part && typeof part === "object" && typeof (part as { type?: unknown }).type === "string"
|
|
984
|
+
? (part as { type: string }).type
|
|
985
|
+
: "unknown";
|
|
986
|
+
return (
|
|
987
|
+
<div className="sb-unknown-part" data-part-type="unknown" data-unknown-type={type}>
|
|
988
|
+
<AlertCircleIcon size={15} />
|
|
989
|
+
<span>暂不支持的消息组件:{type}</span>
|
|
990
|
+
</div>
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function objectOf(value: unknown): Record<string, unknown> {
|
|
995
|
+
return value != null && typeof value === "object"
|
|
996
|
+
? (value as Record<string, unknown>)
|
|
997
|
+
: {};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function strings(value: unknown): string[] {
|
|
1001
|
+
return Array.isArray(value)
|
|
1002
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
1003
|
+
: [];
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function dataOf(part: unknown): Record<string, unknown> {
|
|
1007
|
+
const record = objectOf(part);
|
|
1008
|
+
return objectOf(record.data ?? record.input ?? record.arguments ?? record);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function fileKind(mediaType: unknown, filename: unknown): FileKind {
|
|
1012
|
+
const value = `${String(mediaType ?? "")} ${String(filename ?? "")}`;
|
|
1013
|
+
if (/image/i.test(value)) return "image";
|
|
1014
|
+
if (/audio/i.test(value)) return "audio";
|
|
1015
|
+
if (/csv|sheet|excel/i.test(value)) return "csv";
|
|
1016
|
+
if (/html/i.test(value)) return "html";
|
|
1017
|
+
if (/code|javascript|typescript|json|xml/i.test(value)) return "code";
|
|
1018
|
+
return "doc";
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
export function DefaultPartRenderer({
|
|
1022
|
+
part,
|
|
1023
|
+
context,
|
|
1024
|
+
}: {
|
|
1025
|
+
part: unknown;
|
|
1026
|
+
context: PartRendererContext;
|
|
1027
|
+
}) {
|
|
1028
|
+
const record = objectOf(part);
|
|
1029
|
+
if (record.type === "step-start") return null;
|
|
1030
|
+
const data = dataOf(part);
|
|
1031
|
+
const kind = resolvePartKind(part);
|
|
1032
|
+
const toolName = toolNameOfPart(part);
|
|
1033
|
+
const toolState = String(
|
|
1034
|
+
record.state ?? data.state ?? "output-available",
|
|
1035
|
+
) as AIToolPart["state"];
|
|
1036
|
+
const withToolApproval = (children: ReactNode) =>
|
|
1037
|
+
toolName ? (
|
|
1038
|
+
<ToolApprovalPresentation
|
|
1039
|
+
toolName={toolName}
|
|
1040
|
+
state={toolState}
|
|
1041
|
+
context={context}
|
|
1042
|
+
>
|
|
1043
|
+
{children}
|
|
1044
|
+
</ToolApprovalPresentation>
|
|
1045
|
+
) : (
|
|
1046
|
+
children
|
|
1047
|
+
);
|
|
1048
|
+
if (kind === "text") {
|
|
1049
|
+
return <TextPart text={String(record.text ?? data.text ?? "")} streaming={context.isActive} />;
|
|
1050
|
+
}
|
|
1051
|
+
if (kind === "reasoning") {
|
|
1052
|
+
return (
|
|
1053
|
+
<ReasoningPart
|
|
1054
|
+
text={String(
|
|
1055
|
+
record.thinking ?? record.text ?? data.detail ?? data.text ?? "",
|
|
1056
|
+
)}
|
|
1057
|
+
label={typeof data.label === "string" ? data.label : undefined}
|
|
1058
|
+
streaming={context.isActive && record.state === "streaming"}
|
|
1059
|
+
/>
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (kind === "tool") {
|
|
1063
|
+
const name = toolName ?? String(data.name ?? "tool");
|
|
1064
|
+
const state = toolState;
|
|
1065
|
+
const pending = state === "input-available" || state === "input-streaming";
|
|
1066
|
+
return withToolApproval(
|
|
1067
|
+
<ToolPart
|
|
1068
|
+
name={name}
|
|
1069
|
+
label={typeof data.label === "string" ? data.label : undefined}
|
|
1070
|
+
icon={typeof data.icon === "string" && data.icon in toolIcons ? data.icon as ToolIcon : undefined}
|
|
1071
|
+
state={pending && !context.isActive ? "interrupted" : state}
|
|
1072
|
+
input={record.input ?? record.arguments}
|
|
1073
|
+
output={record.output ?? data.output}
|
|
1074
|
+
errorText={typeof record.errorText === "string" ? record.errorText : undefined}
|
|
1075
|
+
command={typeof data.command === "string" ? data.command : undefined}
|
|
1076
|
+
meta={typeof data.meta === "string" ? data.meta : undefined}
|
|
1077
|
+
results={Array.isArray(data.results) ? data.results as Array<{ title: string; domain: string }> : undefined}
|
|
1078
|
+
activityId={context.activityId}
|
|
1079
|
+
onNavigate={context.actions.onNavigate}
|
|
1080
|
+
/>
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
if (kind === "error") {
|
|
1084
|
+
return <ErrorPart title={String(data.title ?? "Error")} body={String(data.body ?? data.message ?? "请求失败")} />;
|
|
1085
|
+
}
|
|
1086
|
+
if (kind === "image") {
|
|
1087
|
+
return <ImagePart src={String(record.src ?? record.url ?? data.src ?? data.url ?? "")} alt={String(record.alt ?? record.filename ?? data.alt ?? data.filename ?? "图片")} onPreview={() => context.actions.onPreview?.(part)} />;
|
|
1088
|
+
}
|
|
1089
|
+
if (kind === "chart") {
|
|
1090
|
+
return <ChartPart title={String(data.title ?? "Chart")} unit={typeof data.unit === "string" ? data.unit : undefined} bars={Array.isArray(data.bars) ? data.bars as ChartBar[] : []} expandable={Boolean(data.expandable)} onPreview={() => context.actions.onPreview?.(part)} />;
|
|
1091
|
+
}
|
|
1092
|
+
if (kind === "agent_card") {
|
|
1093
|
+
return <AgentCardPart title={String(data.title ?? "Agent")} agentName={String(data.agentName ?? data.name ?? "Agent")} agentDesc={String(data.agentDesc ?? data.description ?? "")} onNavigate={() => context.actions.onNavigate?.(String(data.target ?? "agents"))} />;
|
|
1094
|
+
}
|
|
1095
|
+
if (kind === "plan") {
|
|
1096
|
+
if (!context.isLatestOfKind) return null;
|
|
1097
|
+
const steps = planStepsOfPart(part);
|
|
1098
|
+
return withToolApproval(
|
|
1099
|
+
<PlanPart
|
|
1100
|
+
title={String(data.title ?? "任务计划")}
|
|
1101
|
+
steps={steps}
|
|
1102
|
+
streaming={context.isActive && context.status === "streaming"}
|
|
1103
|
+
/>,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
if (kind === "schedule") {
|
|
1107
|
+
return withToolApproval(<SchedulePart title={String(data.title ?? "安排任务")} cadence={String(data.cadence ?? data.schedule ?? "")} nextRun={String(data.nextRun ?? data.next_run ?? "")} scheduled={Boolean(data.scheduled)} onSchedule={() => context.actions.onAnswer?.({ type: "schedule", data })} />);
|
|
1108
|
+
}
|
|
1109
|
+
if (kind === "parallel") {
|
|
1110
|
+
return withToolApproval(<ParallelPart label={typeof data.label === "string" ? data.label : undefined} tools={Array.isArray(data.tools) ? data.tools as ParallelTool[] : []} isActive={context.isActive} />);
|
|
1111
|
+
}
|
|
1112
|
+
if (kind === "subagents") {
|
|
1113
|
+
const agents = Array.isArray(data.agents)
|
|
1114
|
+
? data.agents as SubAgent[]
|
|
1115
|
+
: [{ name: String(data.name ?? "Sub-agent"), summary: String(data.summary ?? ""), status: context.isActive ? "running" as const : "done" as const }];
|
|
1116
|
+
return withToolApproval(<SubAgentsPart agents={agents} isActive={context.isActive} />);
|
|
1117
|
+
}
|
|
1118
|
+
if (kind === "confirmation") {
|
|
1119
|
+
return <ConfirmationPart id={String(data.id ?? context.activityId)} title={String(data.title ?? "确认操作?")} body={typeof data.body === "string" ? data.body : undefined} confirmLabel={String(data.confirmLabel ?? "确认")} cancelLabel={String(data.cancelLabel ?? "取消")} onConfirm={(confirmed) => context.actions.onApprove?.(String(data.id ?? context.activityId), confirmed ? "allow_once" : "deny")} />;
|
|
1120
|
+
}
|
|
1121
|
+
if (kind === "file") {
|
|
1122
|
+
const filename = String(record.filename ?? data.name ?? data.filename ?? record.mediaType ?? "附件");
|
|
1123
|
+
return <FilePart name={filename} kind={fileKind(record.mediaType ?? data.mediaType, filename)} note={typeof data.note === "string" ? data.note : undefined} url={typeof record.url === "string" ? record.url : typeof data.url === "string" ? data.url : undefined} artifact={part} onPreview={context.actions.onPreview} onDownload={context.actions.onDownload} />;
|
|
1124
|
+
}
|
|
1125
|
+
if (kind === "dashboard") {
|
|
1126
|
+
return <DashboardPart {...data as unknown as DashboardData} title={String(data.title ?? "Dashboard")} />;
|
|
1127
|
+
}
|
|
1128
|
+
if (kind === "ask_user") {
|
|
1129
|
+
const questions: AskQuestion[] = Array.isArray(data.questions)
|
|
1130
|
+
? data.questions as AskQuestion[]
|
|
1131
|
+
: [{
|
|
1132
|
+
prompt: String(data.question ?? "请选择"),
|
|
1133
|
+
kind: data.multiSelect === true ? "multi" : "single",
|
|
1134
|
+
options: strings(data.options),
|
|
1135
|
+
}];
|
|
1136
|
+
return withToolApproval(
|
|
1137
|
+
<AskUserPart
|
|
1138
|
+
questions={questions}
|
|
1139
|
+
freeTextPlaceholder={typeof data.freeTextPlaceholder === "string" ? data.freeTextPlaceholder : undefined}
|
|
1140
|
+
submittedAnswer={context.replyText}
|
|
1141
|
+
onSubmit={(payload) => context.actions.onAnswer?.(payload)}
|
|
1142
|
+
/>,
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
if (kind === "suggestions") {
|
|
1146
|
+
return withToolApproval(<Suggestions items={strings(data.items)} onPick={context.actions.onSuggestion} />);
|
|
1147
|
+
}
|
|
1148
|
+
return <UnknownPart part={part} />;
|
|
1149
|
+
}
|