@springbrand/message-panel 0.1.3-alpha.1 → 0.1.3-alpha.3
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/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +632 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +672 -0
- package/cloud-os/chat/tool-presentation.ts +584 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +672 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +541 -0
- package/cloud-os/index.ts +107 -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 +242 -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 +573 -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 +471 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +41 -18
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/message.tsx +0 -8
- package/src/styles/index.css +4 -1
|
@@ -0,0 +1,471 @@
|
|
|
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
|
+
onRespond={async (_toolCallId, response) => {
|
|
380
|
+
const data =
|
|
381
|
+
response && typeof response === "object"
|
|
382
|
+
? (response as { selections?: string[]; text?: string })
|
|
383
|
+
: {};
|
|
384
|
+
const answer = [
|
|
385
|
+
...(data.selections ?? []),
|
|
386
|
+
data.text?.trim() ?? "",
|
|
387
|
+
]
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
.join(" / ");
|
|
390
|
+
if (answer) setHumanAnswer(answer);
|
|
391
|
+
selectScenario("planning");
|
|
392
|
+
setLastEvent(`已回答并继续:${answer || humanAnswer}`);
|
|
393
|
+
return true;
|
|
394
|
+
}}
|
|
395
|
+
/>
|
|
396
|
+
|
|
397
|
+
<div className="flex-shrink-0 border-t border-kumo-line bg-kumo-base">
|
|
398
|
+
<CloudOsChatInput
|
|
399
|
+
value={value}
|
|
400
|
+
onChange={setValue}
|
|
401
|
+
status={status}
|
|
402
|
+
models={cloudOsModels}
|
|
403
|
+
selectedModel={model}
|
|
404
|
+
onModelChange={setModel}
|
|
405
|
+
attachments={attachments}
|
|
406
|
+
onFilesSelected={addFiles}
|
|
407
|
+
onAttachmentRemove={removeAttachment}
|
|
408
|
+
showThinkingTraces={showThinkingTraces}
|
|
409
|
+
onToggleThinkingTraces={() => {
|
|
410
|
+
setShowThinkingTraces((current) => !current);
|
|
411
|
+
setLastEvent(
|
|
412
|
+
showThinkingTraces ? "已隐藏思考过程" : "已显示思考过程",
|
|
413
|
+
);
|
|
414
|
+
}}
|
|
415
|
+
placeholder="Ask a follow-up…"
|
|
416
|
+
onSubmit={() => {
|
|
417
|
+
if (value.trim()) setPrompt(value.trim());
|
|
418
|
+
selectScenario("submitted");
|
|
419
|
+
setApproval(undefined);
|
|
420
|
+
setLastEvent(`已提交:${value.trim() || "附件"}`);
|
|
421
|
+
setValue("");
|
|
422
|
+
setAttachments([]);
|
|
423
|
+
}}
|
|
424
|
+
onStop={() => {
|
|
425
|
+
selectScenario("interrupted");
|
|
426
|
+
setLastEvent("已停止生成,保留当前回合");
|
|
427
|
+
}}
|
|
428
|
+
footnote={
|
|
429
|
+
<output aria-live="polite" className="truncate">
|
|
430
|
+
最近交互:{lastEvent}
|
|
431
|
+
</output>
|
|
432
|
+
}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
}
|
|
437
|
+
workspace={
|
|
438
|
+
<CloudOsWorkspacePanel
|
|
439
|
+
title="Agent 工作台"
|
|
440
|
+
titleIcon={Pulse}
|
|
441
|
+
activeTab={activeTab}
|
|
442
|
+
onTabChange={setActiveTab}
|
|
443
|
+
onClose={() => setWorkspaceOpen(false)}
|
|
444
|
+
actions={
|
|
445
|
+
<WorkshopIconButton
|
|
446
|
+
aria-label="Blueprints"
|
|
447
|
+
title="Blueprints"
|
|
448
|
+
onClick={() => setLastEvent("打开 Blueprints(演示占位)")}
|
|
449
|
+
>
|
|
450
|
+
<Blueprint size={16} />
|
|
451
|
+
</WorkshopIconButton>
|
|
452
|
+
}
|
|
453
|
+
tabs={[
|
|
454
|
+
{
|
|
455
|
+
value: "activity",
|
|
456
|
+
label: "Activity",
|
|
457
|
+
count: entries.filter((entry) => entry.type === "assistant").length,
|
|
458
|
+
content: <ActivityTab entries={entries} />,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
value: "parts",
|
|
462
|
+
label: "Parts",
|
|
463
|
+
content: <PartsTab messagesJson={messagesJson} />,
|
|
464
|
+
},
|
|
465
|
+
]}
|
|
466
|
+
/>
|
|
467
|
+
}
|
|
468
|
+
/>
|
|
469
|
+
</CloudOsRoot>
|
|
470
|
+
);
|
|
471
|
+
}
|
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.3",
|
|
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
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// camel 是 dev-only 的展示变体(App.tsx 的 `/__camel-chat` 路由),没有接生产宿主。
|
|
2
|
+
// 它的交互卡片仍用旧的 `onAnswer(payload)` 回调,**没有**迁到 client-settled tool 的
|
|
3
|
+
// `onRespond(toolCallId, response)` —— 生产聊天面板走的是 cloud-os,已经迁完。
|
|
4
|
+
// 哪天 camel 要上生产,这里必须一起改,否则卡片点了不会把答案回写给那次工具调用。
|
|
5
|
+
|
|
1
6
|
import type { UIMessage } from "ai";
|
|
2
7
|
import {
|
|
3
8
|
ArrowDownIcon,
|
|
@@ -71,10 +76,7 @@ export interface CamelChatMessagesProps {
|
|
|
71
76
|
finalToolNames?: readonly string[];
|
|
72
77
|
isHydrating?: boolean;
|
|
73
78
|
isRecovering?: boolean;
|
|
74
|
-
|
|
75
|
-
/** Show the opt-in activity bar for recovering or server-streaming states. */
|
|
76
|
-
showActivityStatus?: boolean;
|
|
77
|
-
activityStatusLabel?: string;
|
|
79
|
+
recoveryStatusLabel?: string;
|
|
78
80
|
hasPendingSteer?: boolean;
|
|
79
81
|
startedWithPending?: boolean;
|
|
80
82
|
error?: unknown;
|
|
@@ -164,6 +166,23 @@ export function CamelChatError({
|
|
|
164
166
|
);
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
export function CamelChatRecovery({
|
|
170
|
+
label = "模型响应暂时中断,正在重试…",
|
|
171
|
+
}: {
|
|
172
|
+
label?: string;
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
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"
|
|
177
|
+
data-camel-chat-recovery=""
|
|
178
|
+
role="status"
|
|
179
|
+
>
|
|
180
|
+
<Loader2Icon className="size-4 shrink-0 animate-spin" aria-hidden="true" />
|
|
181
|
+
<span>{label}</span>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
export function CamelApprovalPrompts({
|
|
168
187
|
approvals,
|
|
169
188
|
error,
|
|
@@ -1151,6 +1170,7 @@ function RenderAssistantPart({
|
|
|
1151
1170
|
function AssistantTurn({
|
|
1152
1171
|
turn,
|
|
1153
1172
|
isActive,
|
|
1173
|
+
keepTraceExpanded,
|
|
1154
1174
|
hasPendingSteer,
|
|
1155
1175
|
hasPendingApproval,
|
|
1156
1176
|
onFork,
|
|
@@ -1162,6 +1182,7 @@ function AssistantTurn({
|
|
|
1162
1182
|
}: {
|
|
1163
1183
|
turn: CamelAssistantTurn;
|
|
1164
1184
|
isActive: boolean;
|
|
1185
|
+
keepTraceExpanded: boolean;
|
|
1165
1186
|
hasPendingSteer: boolean;
|
|
1166
1187
|
hasPendingApproval: boolean;
|
|
1167
1188
|
onFork?: (messageId: string) => void | Promise<void>;
|
|
@@ -1185,7 +1206,10 @@ function AssistantTurn({
|
|
|
1185
1206
|
/>
|
|
1186
1207
|
);
|
|
1187
1208
|
const showWorkInPlace =
|
|
1188
|
-
isActive ||
|
|
1209
|
+
isActive ||
|
|
1210
|
+
keepTraceExpanded ||
|
|
1211
|
+
hasPendingApproval ||
|
|
1212
|
+
turn.terminalState !== undefined;
|
|
1189
1213
|
const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
|
|
1190
1214
|
const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
|
|
1191
1215
|
const messageTime =
|
|
@@ -1294,9 +1318,7 @@ export function CamelChatMessages({
|
|
|
1294
1318
|
finalToolNames,
|
|
1295
1319
|
isHydrating = false,
|
|
1296
1320
|
isRecovering = false,
|
|
1297
|
-
|
|
1298
|
-
showActivityStatus = false,
|
|
1299
|
-
activityStatusLabel,
|
|
1321
|
+
recoveryStatusLabel,
|
|
1300
1322
|
hasPendingSteer = false,
|
|
1301
1323
|
startedWithPending = false,
|
|
1302
1324
|
error,
|
|
@@ -1328,6 +1350,10 @@ export function CamelChatMessages({
|
|
|
1328
1350
|
isActive && lastItem?.kind === "assistant"
|
|
1329
1351
|
? lastItem.actionMessageId
|
|
1330
1352
|
: null;
|
|
1353
|
+
const recoveringAssistantMessageId =
|
|
1354
|
+
isRecovering && lastItem?.kind === "assistant"
|
|
1355
|
+
? lastItem.actionMessageId
|
|
1356
|
+
: null;
|
|
1331
1357
|
|
|
1332
1358
|
const syncScrollButton = useCallback(() => {
|
|
1333
1359
|
const element = scrollRef.current;
|
|
@@ -1341,7 +1367,7 @@ export function CamelChatMessages({
|
|
|
1341
1367
|
const element = scrollRef.current;
|
|
1342
1368
|
if (!element || showScrollButton) return;
|
|
1343
1369
|
element.scrollTo({ top: element.scrollHeight });
|
|
1344
|
-
}, [messages, status, showScrollButton]);
|
|
1370
|
+
}, [isRecovering, messages, status, showScrollButton]);
|
|
1345
1371
|
|
|
1346
1372
|
return (
|
|
1347
1373
|
<div
|
|
@@ -1351,15 +1377,6 @@ export function CamelChatMessages({
|
|
|
1351
1377
|
data-status={status}
|
|
1352
1378
|
aria-busy={isActive || isHydrating}
|
|
1353
1379
|
>
|
|
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
1380
|
<div
|
|
1364
1381
|
ref={scrollRef}
|
|
1365
1382
|
onScroll={syncScrollButton}
|
|
@@ -1380,6 +1397,9 @@ export function CamelChatMessages({
|
|
|
1380
1397
|
key={item.key}
|
|
1381
1398
|
turn={item}
|
|
1382
1399
|
isActive={item.actionMessageId === activeAssistantMessageId}
|
|
1400
|
+
keepTraceExpanded={
|
|
1401
|
+
item.actionMessageId === recoveringAssistantMessageId
|
|
1402
|
+
}
|
|
1383
1403
|
hasPendingSteer={hasPendingSteer}
|
|
1384
1404
|
hasPendingApproval={item.parts.some((partRef) => {
|
|
1385
1405
|
const approval =
|
|
@@ -1398,6 +1418,9 @@ export function CamelChatMessages({
|
|
|
1398
1418
|
<StandaloneMessage key={item.key} item={item} />
|
|
1399
1419
|
),
|
|
1400
1420
|
)}
|
|
1421
|
+
{isRecovering && (
|
|
1422
|
+
<CamelChatRecovery label={recoveryStatusLabel} />
|
|
1423
|
+
)}
|
|
1401
1424
|
{error != null && <CamelChatError error={error} onRetry={onRetry} />}
|
|
1402
1425
|
{isActive && activeAssistantMessageId === null && (
|
|
1403
1426
|
<ThinkingIndicator
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type ReactNode,
|
|
40
40
|
} from "react";
|
|
41
41
|
import { ComposerTriggerPopover } from "../composer/composer-trigger-popover";
|
|
42
|
+
import { isComposingKeyEvent } from "../composer/key-rules";
|
|
42
43
|
import type { ComposerCommand } from "../composer/types";
|
|
43
44
|
import { cn } from "../internal/cn";
|
|
44
45
|
|
|
@@ -345,7 +346,7 @@ function CamelPromptInputContent({
|
|
|
345
346
|
if (
|
|
346
347
|
event.key === "Enter" &&
|
|
347
348
|
!event.shiftKey &&
|
|
348
|
-
!event.nativeEvent
|
|
349
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
349
350
|
) {
|
|
350
351
|
event.preventDefault();
|
|
351
352
|
submit();
|