@springbrand/message-panel 0.1.3-alpha.0 → 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.
@@ -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
@@ -1,4 +1,6 @@
1
1
  export * from "./camel-chat-showcase";
2
+ export * from "./chat-scenarios";
3
+ export * from "./cloud-os-chat-showcase";
2
4
  export * from "./fixtures";
3
5
  export * from "./message-panel-chat-showcase";
4
6
  export * from "./message-panel-gallery";
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.1.3-alpha.0",
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
- "typescript": "^7.0.2"
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
  }