@springbrand/message-panel 0.1.3-alpha.1 → 0.1.3-alpha.2
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/cloud-os/README.md +71 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +606 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +640 -0
- package/cloud-os/chat/tool-presentation.ts +542 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +599 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +532 -0
- package/cloud-os/index.ts +103 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +202 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +559 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +13 -917
- package/demo/chat-scenarios.ts +926 -0
- package/demo/cloud-os-chat-showcase.tsx +470 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +36 -18
- package/src/message.tsx +0 -8
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowsOutSimple,
|
|
3
|
+
Blueprint,
|
|
4
|
+
Info,
|
|
5
|
+
Moon,
|
|
6
|
+
Pulse,
|
|
7
|
+
Sun,
|
|
8
|
+
} from "@phosphor-icons/react";
|
|
9
|
+
import { useCallback, useMemo, useState } from "react";
|
|
10
|
+
import {
|
|
11
|
+
CloudOsChatInput,
|
|
12
|
+
CloudOsChatMessages,
|
|
13
|
+
CloudOsRoot,
|
|
14
|
+
CloudOsWorkspacePanel,
|
|
15
|
+
CloudOsWorkspaceSplit,
|
|
16
|
+
WorkshopIconButton,
|
|
17
|
+
buildCloudOsEntries,
|
|
18
|
+
type CloudOsAttachmentView,
|
|
19
|
+
type CloudOsMode,
|
|
20
|
+
} from "../cloud-os";
|
|
21
|
+
import {
|
|
22
|
+
defaultPrompt,
|
|
23
|
+
demoModels,
|
|
24
|
+
flowSteps,
|
|
25
|
+
messagesForScenario,
|
|
26
|
+
scenarios,
|
|
27
|
+
type DemoScenario,
|
|
28
|
+
type DemoScenarioMeta,
|
|
29
|
+
} from "./chat-scenarios";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* cloud-os 版快速预览。场景表和 camel-chat-showcase 共用 `chat-scenarios.ts`,
|
|
33
|
+
* 所以两边的状态逐条对齐(同样 22 个场景、同样的 7 步流程条、同样的初始 URL 参数)。
|
|
34
|
+
* 差别只在外壳:这里是 cloudflare-os 的左聊天 / 右工作区双栏。
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const cloudOsModels = demoModels.map((model) => ({
|
|
38
|
+
id: model.value,
|
|
39
|
+
name: model.label,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
function initialScenario(): DemoScenario {
|
|
43
|
+
if (typeof window === "undefined") return "complete";
|
|
44
|
+
const value = new URLSearchParams(window.location.search).get("scenario");
|
|
45
|
+
return value && Object.hasOwn(scenarios, value)
|
|
46
|
+
? (value as DemoScenario)
|
|
47
|
+
: "complete";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 右栏「Activity」页:把同一份消息流投影成一条工作流水,验证面板确实吃 UIMessage。 */
|
|
51
|
+
function ActivityTab({
|
|
52
|
+
entries,
|
|
53
|
+
}: {
|
|
54
|
+
entries: ReturnType<typeof buildCloudOsEntries>;
|
|
55
|
+
}) {
|
|
56
|
+
const rows = entries.flatMap((entry) =>
|
|
57
|
+
entry.type === "assistant"
|
|
58
|
+
? entry.blocks.flatMap((block) =>
|
|
59
|
+
block.kind === "toolGroup"
|
|
60
|
+
? block.group.calls.map((call) => ({
|
|
61
|
+
key: `${block.key}-${call.toolCallId}`,
|
|
62
|
+
name: call.toolName,
|
|
63
|
+
state: call.failed
|
|
64
|
+
? "failed"
|
|
65
|
+
: call.running
|
|
66
|
+
? "running"
|
|
67
|
+
: "done",
|
|
68
|
+
}))
|
|
69
|
+
: [],
|
|
70
|
+
)
|
|
71
|
+
: [],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (rows.length === 0) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex h-full items-center justify-center px-6 text-center">
|
|
77
|
+
<p className="m-0 max-w-[320px] text-[13px] leading-[19px] text-kumo-subtle">
|
|
78
|
+
这个场景还没有工具调用。切到「工具执行中」「并行与子 Agent」看活动流水。
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-1 p-3">
|
|
86
|
+
{rows.map((row) => (
|
|
87
|
+
<div
|
|
88
|
+
key={row.key}
|
|
89
|
+
className="flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-[13px] leading-[19px] tracking-[-0.25px] text-kumo-subtle hover:bg-kumo-tint/50"
|
|
90
|
+
>
|
|
91
|
+
<span
|
|
92
|
+
className={`h-1.5 w-1.5 flex-shrink-0 rounded-full ${
|
|
93
|
+
row.state === "failed"
|
|
94
|
+
? "bg-kumo-danger"
|
|
95
|
+
: row.state === "running"
|
|
96
|
+
? "animate-pulse bg-kumo-brand motion-reduce:animate-none"
|
|
97
|
+
: "bg-kumo-success"
|
|
98
|
+
}`}
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
/>
|
|
101
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[12px]">
|
|
102
|
+
{row.name}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="flex-shrink-0 text-[11px] uppercase tracking-[0.06em] text-kumo-inactive">
|
|
105
|
+
{row.state}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function PartsTab({ messagesJson }: { messagesJson: string }) {
|
|
114
|
+
return (
|
|
115
|
+
<pre className="m-0 h-full overflow-auto bg-kumo-base p-3 font-mono text-[11px] leading-[17px] text-kumo-subtle whitespace-pre-wrap">
|
|
116
|
+
{messagesJson}
|
|
117
|
+
</pre>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function CloudOsChatShowcase() {
|
|
122
|
+
const [mode, setMode] = useState<CloudOsMode>("light");
|
|
123
|
+
const [scenario, setScenario] = useState<DemoScenario>(initialScenario);
|
|
124
|
+
const [prompt, setPrompt] = useState(defaultPrompt);
|
|
125
|
+
const [humanAnswer, setHumanAnswer] = useState("管理层");
|
|
126
|
+
const [model, setModel] = useState<string | null>(cloudOsModels[0].id);
|
|
127
|
+
const [value, setValue] = useState("");
|
|
128
|
+
const [attachments, setAttachments] = useState<CloudOsAttachmentView[]>([]);
|
|
129
|
+
const [approval, setApproval] = useState<boolean | undefined>();
|
|
130
|
+
const [showThinkingTraces, setShowThinkingTraces] = useState(true);
|
|
131
|
+
const [workspaceOpen, setWorkspaceOpen] = useState(true);
|
|
132
|
+
const [activeTab, setActiveTab] = useState("activity");
|
|
133
|
+
const [lastEvent, setLastEvent] = useState("等待交互");
|
|
134
|
+
|
|
135
|
+
const selectScenario = useCallback((next: DemoScenario) => {
|
|
136
|
+
setScenario(next);
|
|
137
|
+
const url = new URL(window.location.href);
|
|
138
|
+
url.searchParams.set("scenario", next);
|
|
139
|
+
window.history.replaceState(window.history.state, "", url);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const scenarioMeta: DemoScenarioMeta = scenarios[scenario];
|
|
143
|
+
const status = scenarioMeta.status;
|
|
144
|
+
const messages = useMemo(
|
|
145
|
+
() => messagesForScenario(scenario, prompt, humanAnswer, approval),
|
|
146
|
+
[approval, humanAnswer, prompt, scenario],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// 待授权的 part → 授权 id。和 camel showcase 的 approvalView 判据一致。
|
|
150
|
+
const approvalIdOf = useCallback((part: unknown): string | undefined => {
|
|
151
|
+
const record =
|
|
152
|
+
part && typeof part === "object"
|
|
153
|
+
? (part as { type?: unknown; state?: unknown })
|
|
154
|
+
: {};
|
|
155
|
+
return (record.type === "tool-write_file" ||
|
|
156
|
+
record.type === "tool-cancel_schedule") &&
|
|
157
|
+
record.state === "approval-requested"
|
|
158
|
+
? "cloud-os-demo-approval"
|
|
159
|
+
: undefined;
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const entries = useMemo(
|
|
163
|
+
() =>
|
|
164
|
+
buildCloudOsEntries({
|
|
165
|
+
messages,
|
|
166
|
+
isActive: status === "submitted" || status === "streaming",
|
|
167
|
+
showThinkingTraces,
|
|
168
|
+
approvalIdOf,
|
|
169
|
+
}),
|
|
170
|
+
[approvalIdOf, messages, showThinkingTraces, status],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const messagesJson = useMemo(
|
|
174
|
+
() => JSON.stringify(messages, null, 2),
|
|
175
|
+
[messages],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const statusError = useMemo(
|
|
179
|
+
() =>
|
|
180
|
+
scenario === "error"
|
|
181
|
+
? new Error("数据源连接中断。已完成步骤仍然保留,请点击 Retry 继续。")
|
|
182
|
+
: undefined,
|
|
183
|
+
[scenario],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const addFiles = useCallback((files: readonly File[]) => {
|
|
187
|
+
setAttachments((current) => [
|
|
188
|
+
...current,
|
|
189
|
+
...files.map((file, index) => ({
|
|
190
|
+
id: `${file.name}-${file.lastModified}-${index}`,
|
|
191
|
+
filename: file.name,
|
|
192
|
+
mediaType: file.type,
|
|
193
|
+
size: file.size,
|
|
194
|
+
previewUrl: file.type.startsWith("image/")
|
|
195
|
+
? URL.createObjectURL(file)
|
|
196
|
+
: undefined,
|
|
197
|
+
status: "ready" as const,
|
|
198
|
+
})),
|
|
199
|
+
]);
|
|
200
|
+
setLastEvent(`添加了 ${files.length} 个附件`);
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
const removeAttachment = useCallback((id: string) => {
|
|
204
|
+
setAttachments((current) => {
|
|
205
|
+
const removed = current.find((attachment) => attachment.id === id);
|
|
206
|
+
if (removed?.previewUrl?.startsWith("blob:")) {
|
|
207
|
+
URL.revokeObjectURL(removed.previewUrl);
|
|
208
|
+
}
|
|
209
|
+
return current.filter((attachment) => attachment.id !== id);
|
|
210
|
+
});
|
|
211
|
+
setLastEvent("移除了附件");
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<CloudOsRoot mode={mode} className="flex h-dvh min-h-0 w-full flex-col overflow-hidden">
|
|
216
|
+
{/* 顶栏:高度对齐 cloudflare-os 的 h-14 */}
|
|
217
|
+
<header className="relative z-30 flex flex-shrink-0 flex-wrap items-center gap-2 border-b border-kumo-line bg-kumo-base px-4 py-2">
|
|
218
|
+
<a
|
|
219
|
+
href="/"
|
|
220
|
+
className="rounded-md px-2 py-1 text-[12px] leading-4 text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
221
|
+
>
|
|
222
|
+
← 返回系统
|
|
223
|
+
</a>
|
|
224
|
+
<div className="min-w-48 flex-1">
|
|
225
|
+
<h1 className="m-0 text-[14px] leading-5 font-semibold tracking-[-0.25px] text-kumo-default">
|
|
226
|
+
cloud-os Chat · 真实回合验收
|
|
227
|
+
</h1>
|
|
228
|
+
<p className="m-0 text-[11px] leading-4 text-kumo-inactive">
|
|
229
|
+
与 camel-chat-showcase 同一份场景表,外壳换成 cloudflare-os 的左聊天 / 右工作区
|
|
230
|
+
</p>
|
|
231
|
+
</div>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-kumo-line bg-kumo-base px-2.5 text-[12px] leading-4 text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
235
|
+
onClick={() => setMode((current) => (current === "dark" ? "light" : "dark"))}
|
|
236
|
+
aria-label="切换主题"
|
|
237
|
+
>
|
|
238
|
+
{mode === "dark" ? <Sun size={13} /> : <Moon size={13} />}
|
|
239
|
+
{mode === "dark" ? "浅色" : "深色"}
|
|
240
|
+
</button>
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-kumo-line bg-kumo-base px-2.5 text-[12px] leading-4 text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
|
|
244
|
+
onClick={() => setWorkspaceOpen((open) => !open)}
|
|
245
|
+
>
|
|
246
|
+
<ArrowsOutSimple size={13} />
|
|
247
|
+
{workspaceOpen ? "收起工作区" : "展开工作区"}
|
|
248
|
+
</button>
|
|
249
|
+
<div
|
|
250
|
+
className="order-last flex basis-full flex-wrap items-center gap-1 border-t border-kumo-line/60 pt-2"
|
|
251
|
+
role="group"
|
|
252
|
+
aria-label="消息场景"
|
|
253
|
+
>
|
|
254
|
+
<span className="mr-1 text-[12px] leading-4 text-kumo-inactive">消息场景</span>
|
|
255
|
+
{(Object.entries(scenarios) as [DemoScenario, DemoScenarioMeta][]).map(
|
|
256
|
+
([option, meta]) => {
|
|
257
|
+
const selected = scenario === option;
|
|
258
|
+
return (
|
|
259
|
+
<button
|
|
260
|
+
key={option}
|
|
261
|
+
type="button"
|
|
262
|
+
data-scenario={option}
|
|
263
|
+
aria-pressed={selected}
|
|
264
|
+
className={[
|
|
265
|
+
"rounded-md border px-2 py-1 text-[11px] leading-4 transition-colors focus-visible:outline-none",
|
|
266
|
+
selected
|
|
267
|
+
? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
|
|
268
|
+
: "border-kumo-line text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default",
|
|
269
|
+
].join(" ")}
|
|
270
|
+
onClick={() => {
|
|
271
|
+
selectScenario(option);
|
|
272
|
+
setApproval(undefined);
|
|
273
|
+
setLastEvent(`切换到:${meta.label}`);
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
{meta.label}
|
|
277
|
+
</button>
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</header>
|
|
283
|
+
|
|
284
|
+
<CloudOsWorkspaceSplit
|
|
285
|
+
workspaceOpen={workspaceOpen}
|
|
286
|
+
isAgentActive={status === "submitted" || status === "streaming"}
|
|
287
|
+
chat={
|
|
288
|
+
<div className="flex h-full min-h-0 flex-col bg-kumo-base">
|
|
289
|
+
<aside
|
|
290
|
+
className="flex-shrink-0 border-b border-kumo-line bg-kumo-elevated/60 px-4 py-3"
|
|
291
|
+
aria-label="消息 UI 验收说明"
|
|
292
|
+
>
|
|
293
|
+
<div className="flex items-start gap-2.5">
|
|
294
|
+
<Info size={15} className="mt-0.5 flex-shrink-0 text-kumo-inactive" />
|
|
295
|
+
<div className="min-w-0">
|
|
296
|
+
<h2 className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
|
|
297
|
+
消息体说明
|
|
298
|
+
</h2>
|
|
299
|
+
<p className="m-0 mt-0.5 text-[12px] leading-[18px] text-kumo-subtle">
|
|
300
|
+
{scenarioMeta.description}
|
|
301
|
+
{scenario !== "empty" &&
|
|
302
|
+
scenario !== "commands" &&
|
|
303
|
+
" 下方始终使用同一个用户问题,便于直接比较状态变化。"}
|
|
304
|
+
</p>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<ol className="mt-2.5 flex flex-wrap gap-1.5" aria-label="工具收齐流程">
|
|
308
|
+
{flowSteps.map((step, index) => {
|
|
309
|
+
const reached = index <= scenarioMeta.flowStep;
|
|
310
|
+
return (
|
|
311
|
+
<li
|
|
312
|
+
key={`${step.label}-${index}`}
|
|
313
|
+
className="min-w-0"
|
|
314
|
+
style={{ flex: "1 1 8rem" }}
|
|
315
|
+
>
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
className={[
|
|
319
|
+
"flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-[11px] leading-4 transition-colors",
|
|
320
|
+
reached
|
|
321
|
+
? "border-kumo-line bg-kumo-tint text-kumo-default"
|
|
322
|
+
: "border-kumo-line/60 text-kumo-inactive hover:bg-kumo-tint/50",
|
|
323
|
+
].join(" ")}
|
|
324
|
+
onClick={() => {
|
|
325
|
+
selectScenario(step.scenario);
|
|
326
|
+
setApproval(undefined);
|
|
327
|
+
setLastEvent(`查看流程:${step.label}`);
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<span
|
|
331
|
+
className={[
|
|
332
|
+
"grid size-4 flex-shrink-0 place-items-center rounded-full font-mono text-[9px]",
|
|
333
|
+
reached
|
|
334
|
+
? "bg-kumo-contrast text-kumo-inverse"
|
|
335
|
+
: "bg-kumo-fill text-kumo-inactive",
|
|
336
|
+
].join(" ")}
|
|
337
|
+
>
|
|
338
|
+
{index + 1}
|
|
339
|
+
</span>
|
|
340
|
+
<span className="truncate">{step.label}</span>
|
|
341
|
+
</button>
|
|
342
|
+
</li>
|
|
343
|
+
);
|
|
344
|
+
})}
|
|
345
|
+
</ol>
|
|
346
|
+
</aside>
|
|
347
|
+
|
|
348
|
+
<CloudOsChatMessages
|
|
349
|
+
messages={messages}
|
|
350
|
+
status={status}
|
|
351
|
+
isHydrating={scenario === "hydrating"}
|
|
352
|
+
isRecovering={scenario === "recovering"}
|
|
353
|
+
recoveryStatusLabel="模型响应暂时中断,正在重试…"
|
|
354
|
+
showThinkingTraces={showThinkingTraces}
|
|
355
|
+
approvalIdOf={approvalIdOf}
|
|
356
|
+
error={statusError}
|
|
357
|
+
constrainWidth={!workspaceOpen}
|
|
358
|
+
onRetry={() => {
|
|
359
|
+
selectScenario("recovering");
|
|
360
|
+
setLastEvent("已重试,从失败步骤继续执行");
|
|
361
|
+
}}
|
|
362
|
+
onApprove={(_approvalId, decision) => {
|
|
363
|
+
const approved = decision !== "deny";
|
|
364
|
+
if (scenario === "schedule_cancel") {
|
|
365
|
+
setApproval(approved);
|
|
366
|
+
setLastEvent(approved ? "已允许取消定时任务" : "已拒绝取消定时任务");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
setApproval(approved);
|
|
370
|
+
selectScenario(approved ? "collected" : "error");
|
|
371
|
+
setLastEvent(approved ? "已允许写入报告" : "已拒绝写入报告");
|
|
372
|
+
}}
|
|
373
|
+
onSuggestion={(text) => {
|
|
374
|
+
setPrompt(text);
|
|
375
|
+
selectScenario("submitted");
|
|
376
|
+
setValue("");
|
|
377
|
+
setLastEvent(`已发送后续问题:${text}`);
|
|
378
|
+
}}
|
|
379
|
+
onAnswer={(payload) => {
|
|
380
|
+
const data =
|
|
381
|
+
payload && typeof payload === "object"
|
|
382
|
+
? (payload as { selections?: string[][]; text?: string })
|
|
383
|
+
: {};
|
|
384
|
+
const answer = [
|
|
385
|
+
...(data.selections?.flat() ?? []),
|
|
386
|
+
data.text?.trim() ?? "",
|
|
387
|
+
]
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
.join("、");
|
|
390
|
+
if (answer) setHumanAnswer(answer);
|
|
391
|
+
selectScenario("planning");
|
|
392
|
+
setLastEvent(`已回答并继续:${answer || humanAnswer}`);
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
<div className="flex-shrink-0 border-t border-kumo-line bg-kumo-base">
|
|
397
|
+
<CloudOsChatInput
|
|
398
|
+
value={value}
|
|
399
|
+
onChange={setValue}
|
|
400
|
+
status={status}
|
|
401
|
+
models={cloudOsModels}
|
|
402
|
+
selectedModel={model}
|
|
403
|
+
onModelChange={setModel}
|
|
404
|
+
attachments={attachments}
|
|
405
|
+
onFilesSelected={addFiles}
|
|
406
|
+
onAttachmentRemove={removeAttachment}
|
|
407
|
+
showThinkingTraces={showThinkingTraces}
|
|
408
|
+
onToggleThinkingTraces={() => {
|
|
409
|
+
setShowThinkingTraces((current) => !current);
|
|
410
|
+
setLastEvent(
|
|
411
|
+
showThinkingTraces ? "已隐藏思考过程" : "已显示思考过程",
|
|
412
|
+
);
|
|
413
|
+
}}
|
|
414
|
+
placeholder="Ask a follow-up…"
|
|
415
|
+
onSubmit={() => {
|
|
416
|
+
if (value.trim()) setPrompt(value.trim());
|
|
417
|
+
selectScenario("submitted");
|
|
418
|
+
setApproval(undefined);
|
|
419
|
+
setLastEvent(`已提交:${value.trim() || "附件"}`);
|
|
420
|
+
setValue("");
|
|
421
|
+
setAttachments([]);
|
|
422
|
+
}}
|
|
423
|
+
onStop={() => {
|
|
424
|
+
selectScenario("interrupted");
|
|
425
|
+
setLastEvent("已停止生成,保留当前回合");
|
|
426
|
+
}}
|
|
427
|
+
footnote={
|
|
428
|
+
<output aria-live="polite" className="truncate">
|
|
429
|
+
最近交互:{lastEvent}
|
|
430
|
+
</output>
|
|
431
|
+
}
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
}
|
|
436
|
+
workspace={
|
|
437
|
+
<CloudOsWorkspacePanel
|
|
438
|
+
title="Agent 工作台"
|
|
439
|
+
titleIcon={Pulse}
|
|
440
|
+
activeTab={activeTab}
|
|
441
|
+
onTabChange={setActiveTab}
|
|
442
|
+
onClose={() => setWorkspaceOpen(false)}
|
|
443
|
+
actions={
|
|
444
|
+
<WorkshopIconButton
|
|
445
|
+
aria-label="Blueprints"
|
|
446
|
+
title="Blueprints"
|
|
447
|
+
onClick={() => setLastEvent("打开 Blueprints(演示占位)")}
|
|
448
|
+
>
|
|
449
|
+
<Blueprint size={16} />
|
|
450
|
+
</WorkshopIconButton>
|
|
451
|
+
}
|
|
452
|
+
tabs={[
|
|
453
|
+
{
|
|
454
|
+
value: "activity",
|
|
455
|
+
label: "Activity",
|
|
456
|
+
count: entries.filter((entry) => entry.type === "assistant").length,
|
|
457
|
+
content: <ActivityTab entries={entries} />,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
value: "parts",
|
|
461
|
+
label: "Parts",
|
|
462
|
+
content: <PartsTab messagesJson={messagesJson} />,
|
|
463
|
+
},
|
|
464
|
+
]}
|
|
465
|
+
/>
|
|
466
|
+
}
|
|
467
|
+
/>
|
|
468
|
+
</CloudOsRoot>
|
|
469
|
+
);
|
|
470
|
+
}
|
package/demo/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/message-panel",
|
|
3
|
-
"version": "0.1.3-alpha.
|
|
3
|
+
"version": "0.1.3-alpha.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
7
|
-
"demo"
|
|
7
|
+
"demo",
|
|
8
|
+
"cloud-os"
|
|
8
9
|
],
|
|
9
10
|
"publishConfig": {
|
|
10
11
|
"access": "public"
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
".": "./src/index.ts",
|
|
14
15
|
"./composer": "./src/composer/index.ts",
|
|
15
16
|
"./styles.css": "./src/styles/index.css",
|
|
17
|
+
"./cloud-os": "./cloud-os/index.ts",
|
|
18
|
+
"./cloud-os.css": "./cloud-os/styles/cloud-os.css",
|
|
16
19
|
"./demo": "./demo/index.ts"
|
|
17
20
|
},
|
|
18
21
|
"peerDependencies": {
|
|
@@ -22,22 +25,31 @@
|
|
|
22
25
|
},
|
|
23
26
|
"dependencies": {
|
|
24
27
|
"@assistant-ui/react": "^0.15.1",
|
|
28
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
25
29
|
"@streamdown/cjk": "^1.0.3",
|
|
26
30
|
"@streamdown/code": "^1.1.1",
|
|
27
31
|
"@streamdown/math": "^1.0.2",
|
|
28
32
|
"@streamdown/mermaid": "^1.0.2",
|
|
29
33
|
"lucide-react": "^1.24.0",
|
|
30
34
|
"radix-ui": "^1.6.2",
|
|
35
|
+
"react-markdown": "^10.1.0",
|
|
36
|
+
"remark-gfm": "^4.0.1",
|
|
31
37
|
"streamdown": "^2.5.0",
|
|
32
38
|
"thinking-orbs": "0.1.1",
|
|
33
39
|
"use-stick-to-bottom": "^1.1.6"
|
|
34
40
|
},
|
|
35
41
|
"devDependencies": {
|
|
42
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
36
43
|
"@types/react": "^19.2.17",
|
|
37
44
|
"@types/react-dom": "^19.2.3",
|
|
38
|
-
"
|
|
45
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
46
|
+
"tailwindcss": "^4.3.2",
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"vite": "^8.1.4"
|
|
39
49
|
},
|
|
40
50
|
"scripts": {
|
|
41
|
-
"typecheck": "tsc --noEmit"
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"dev:cloud-os": "vite",
|
|
53
|
+
"build:cloud-os": "vite build"
|
|
42
54
|
}
|
|
43
55
|
}
|
|
@@ -71,10 +71,7 @@ export interface CamelChatMessagesProps {
|
|
|
71
71
|
finalToolNames?: readonly string[];
|
|
72
72
|
isHydrating?: boolean;
|
|
73
73
|
isRecovering?: boolean;
|
|
74
|
-
|
|
75
|
-
/** Show the opt-in activity bar for recovering or server-streaming states. */
|
|
76
|
-
showActivityStatus?: boolean;
|
|
77
|
-
activityStatusLabel?: string;
|
|
74
|
+
recoveryStatusLabel?: string;
|
|
78
75
|
hasPendingSteer?: boolean;
|
|
79
76
|
startedWithPending?: boolean;
|
|
80
77
|
error?: unknown;
|
|
@@ -164,6 +161,23 @@ export function CamelChatError({
|
|
|
164
161
|
);
|
|
165
162
|
}
|
|
166
163
|
|
|
164
|
+
export function CamelChatRecovery({
|
|
165
|
+
label = "模型响应暂时中断,正在重试…",
|
|
166
|
+
}: {
|
|
167
|
+
label?: string;
|
|
168
|
+
}) {
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className="mt-4 flex items-center gap-2 rounded-lg border border-border/70 bg-muted/40 px-3 py-2 text-sm text-muted-foreground"
|
|
172
|
+
data-camel-chat-recovery=""
|
|
173
|
+
role="status"
|
|
174
|
+
>
|
|
175
|
+
<Loader2Icon className="size-4 shrink-0 animate-spin" aria-hidden="true" />
|
|
176
|
+
<span>{label}</span>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
167
181
|
export function CamelApprovalPrompts({
|
|
168
182
|
approvals,
|
|
169
183
|
error,
|
|
@@ -1151,6 +1165,7 @@ function RenderAssistantPart({
|
|
|
1151
1165
|
function AssistantTurn({
|
|
1152
1166
|
turn,
|
|
1153
1167
|
isActive,
|
|
1168
|
+
keepTraceExpanded,
|
|
1154
1169
|
hasPendingSteer,
|
|
1155
1170
|
hasPendingApproval,
|
|
1156
1171
|
onFork,
|
|
@@ -1162,6 +1177,7 @@ function AssistantTurn({
|
|
|
1162
1177
|
}: {
|
|
1163
1178
|
turn: CamelAssistantTurn;
|
|
1164
1179
|
isActive: boolean;
|
|
1180
|
+
keepTraceExpanded: boolean;
|
|
1165
1181
|
hasPendingSteer: boolean;
|
|
1166
1182
|
hasPendingApproval: boolean;
|
|
1167
1183
|
onFork?: (messageId: string) => void | Promise<void>;
|
|
@@ -1185,7 +1201,10 @@ function AssistantTurn({
|
|
|
1185
1201
|
/>
|
|
1186
1202
|
);
|
|
1187
1203
|
const showWorkInPlace =
|
|
1188
|
-
isActive ||
|
|
1204
|
+
isActive ||
|
|
1205
|
+
keepTraceExpanded ||
|
|
1206
|
+
hasPendingApproval ||
|
|
1207
|
+
turn.terminalState !== undefined;
|
|
1189
1208
|
const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
|
|
1190
1209
|
const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
|
|
1191
1210
|
const messageTime =
|
|
@@ -1294,9 +1313,7 @@ export function CamelChatMessages({
|
|
|
1294
1313
|
finalToolNames,
|
|
1295
1314
|
isHydrating = false,
|
|
1296
1315
|
isRecovering = false,
|
|
1297
|
-
|
|
1298
|
-
showActivityStatus = false,
|
|
1299
|
-
activityStatusLabel,
|
|
1316
|
+
recoveryStatusLabel,
|
|
1300
1317
|
hasPendingSteer = false,
|
|
1301
1318
|
startedWithPending = false,
|
|
1302
1319
|
error,
|
|
@@ -1328,6 +1345,10 @@ export function CamelChatMessages({
|
|
|
1328
1345
|
isActive && lastItem?.kind === "assistant"
|
|
1329
1346
|
? lastItem.actionMessageId
|
|
1330
1347
|
: null;
|
|
1348
|
+
const recoveringAssistantMessageId =
|
|
1349
|
+
isRecovering && lastItem?.kind === "assistant"
|
|
1350
|
+
? lastItem.actionMessageId
|
|
1351
|
+
: null;
|
|
1331
1352
|
|
|
1332
1353
|
const syncScrollButton = useCallback(() => {
|
|
1333
1354
|
const element = scrollRef.current;
|
|
@@ -1341,7 +1362,7 @@ export function CamelChatMessages({
|
|
|
1341
1362
|
const element = scrollRef.current;
|
|
1342
1363
|
if (!element || showScrollButton) return;
|
|
1343
1364
|
element.scrollTo({ top: element.scrollHeight });
|
|
1344
|
-
}, [messages, status, showScrollButton]);
|
|
1365
|
+
}, [isRecovering, messages, status, showScrollButton]);
|
|
1345
1366
|
|
|
1346
1367
|
return (
|
|
1347
1368
|
<div
|
|
@@ -1351,15 +1372,6 @@ export function CamelChatMessages({
|
|
|
1351
1372
|
data-status={status}
|
|
1352
1373
|
aria-busy={isActive || isHydrating}
|
|
1353
1374
|
>
|
|
1354
|
-
{showActivityStatus && (isRecovering || isServerStreaming) && (
|
|
1355
|
-
<div
|
|
1356
|
-
className="absolute inset-x-0 top-0 z-10 flex h-7 items-center justify-center gap-2 bg-background/85 text-xs text-muted-foreground backdrop-blur-sm"
|
|
1357
|
-
role="status"
|
|
1358
|
-
>
|
|
1359
|
-
<span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
|
|
1360
|
-
{activityStatusLabel ?? (isRecovering ? "Recovering…" : "Running…")}
|
|
1361
|
-
</div>
|
|
1362
|
-
)}
|
|
1363
1375
|
<div
|
|
1364
1376
|
ref={scrollRef}
|
|
1365
1377
|
onScroll={syncScrollButton}
|
|
@@ -1380,6 +1392,9 @@ export function CamelChatMessages({
|
|
|
1380
1392
|
key={item.key}
|
|
1381
1393
|
turn={item}
|
|
1382
1394
|
isActive={item.actionMessageId === activeAssistantMessageId}
|
|
1395
|
+
keepTraceExpanded={
|
|
1396
|
+
item.actionMessageId === recoveringAssistantMessageId
|
|
1397
|
+
}
|
|
1383
1398
|
hasPendingSteer={hasPendingSteer}
|
|
1384
1399
|
hasPendingApproval={item.parts.some((partRef) => {
|
|
1385
1400
|
const approval =
|
|
@@ -1398,6 +1413,9 @@ export function CamelChatMessages({
|
|
|
1398
1413
|
<StandaloneMessage key={item.key} item={item} />
|
|
1399
1414
|
),
|
|
1400
1415
|
)}
|
|
1416
|
+
{isRecovering && (
|
|
1417
|
+
<CamelChatRecovery label={recoveryStatusLabel} />
|
|
1418
|
+
)}
|
|
1401
1419
|
{error != null && <CamelChatError error={error} onRetry={onRetry} />}
|
|
1402
1420
|
{isActive && activeAssistantMessageId === null && (
|
|
1403
1421
|
<ThinkingIndicator
|
package/src/message.tsx
CHANGED
|
@@ -85,13 +85,6 @@ export const MessageAction = forwardRef<HTMLButtonElement, MessageActionProps>(
|
|
|
85
85
|
MessageAction.displayName = "MessageAction";
|
|
86
86
|
|
|
87
87
|
const streamdownPlugins = { cjk, code, math, mermaid };
|
|
88
|
-
const streamAnimation = {
|
|
89
|
-
animation: "fadeIn",
|
|
90
|
-
sep: "word",
|
|
91
|
-
duration: 260,
|
|
92
|
-
stagger: 14,
|
|
93
|
-
} as const;
|
|
94
|
-
|
|
95
88
|
export type MessageResponseProps = StreamdownProps & {
|
|
96
89
|
streaming?: boolean;
|
|
97
90
|
resolveUrl?: MessageUrlResolver;
|
|
@@ -146,7 +139,6 @@ export const MessageResponse = memo(
|
|
|
146
139
|
? {
|
|
147
140
|
mode: "streaming" as const,
|
|
148
141
|
isAnimating: true,
|
|
149
|
-
animated: streamAnimation,
|
|
150
142
|
}
|
|
151
143
|
: { mode: "static" as const })}
|
|
152
144
|
{...props}
|