@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.
Files changed (99) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/message-panel-chat-showcase.tsx +250 -0
  4. package/demo/message-panel-gallery.tsx +389 -0
  5. package/package.json +6 -16
  6. package/src/adapters/ai-sdk.ts +32 -0
  7. package/src/camel/camel-chat-messages.tsx +1435 -0
  8. package/src/camel/camel-model-select.tsx +88 -0
  9. package/src/camel/camel-plan-bar.tsx +115 -0
  10. package/src/camel/camel-prompt-input.tsx +547 -0
  11. package/src/camel/camel-tool-presentation.tsx +1270 -0
  12. package/src/camel/camel-turn.ts +334 -0
  13. package/src/camel/turn-summary-bar.tsx +81 -0
  14. package/src/chat-summary-panel.tsx +229 -0
  15. package/src/composer/chat-composer.tsx +216 -0
  16. package/src/composer/composer-attachments.tsx +138 -0
  17. package/src/composer/composer-trigger-popover.tsx +236 -0
  18. package/src/composer/composer.tsx +425 -0
  19. package/src/composer/drop-controller.ts +80 -0
  20. package/src/composer/types.ts +79 -0
  21. package/src/contracts.ts +138 -0
  22. package/src/conversation.tsx +76 -0
  23. package/src/internal/cn.ts +3 -0
  24. package/src/message-panel.tsx +368 -0
  25. package/src/message.tsx +254 -0
  26. package/src/parts/index.tsx +1149 -0
  27. package/src/parts/kind.ts +67 -0
  28. package/src/parts/plan.ts +64 -0
  29. package/src/parts/primitives.tsx +439 -0
  30. package/src/registry.ts +44 -0
  31. package/src/status.tsx +132 -0
  32. package/dist/demo/camel-chat-showcase.d.ts +0 -1
  33. package/dist/demo/camel-chat-showcase.js +0 -1007
  34. package/dist/demo/fixtures.d.ts +0 -32
  35. package/dist/demo/fixtures.js +0 -250
  36. package/dist/demo/index.js +0 -4
  37. package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
  38. package/dist/demo/message-panel-chat-showcase.js +0 -114
  39. package/dist/demo/message-panel-gallery.d.ts +0 -1
  40. package/dist/demo/message-panel-gallery.js +0 -130
  41. package/dist/src/adapters/ai-sdk.d.ts +0 -4
  42. package/dist/src/adapters/ai-sdk.js +0 -25
  43. package/dist/src/camel/camel-chat-messages.d.ts +0 -44
  44. package/dist/src/camel/camel-chat-messages.js +0 -421
  45. package/dist/src/camel/camel-model-select.d.ts +0 -13
  46. package/dist/src/camel/camel-model-select.js +0 -7
  47. package/dist/src/camel/camel-plan-bar.d.ts +0 -5
  48. package/dist/src/camel/camel-plan-bar.js +0 -24
  49. package/dist/src/camel/camel-prompt-input.d.ts +0 -32
  50. package/dist/src/camel/camel-prompt-input.js +0 -182
  51. package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
  52. package/dist/src/camel/camel-tool-presentation.js +0 -784
  53. package/dist/src/camel/camel-turn.d.ts +0 -57
  54. package/dist/src/camel/camel-turn.js +0 -203
  55. package/dist/src/camel/index.js +0 -7
  56. package/dist/src/camel/turn-summary-bar.d.ts +0 -8
  57. package/dist/src/camel/turn-summary-bar.js +0 -12
  58. package/dist/src/chat-summary-panel.d.ts +0 -22
  59. package/dist/src/chat-summary-panel.js +0 -26
  60. package/dist/src/composer/chat-composer.d.ts +0 -4
  61. package/dist/src/composer/chat-composer.js +0 -90
  62. package/dist/src/composer/composer-attachments.d.ts +0 -13
  63. package/dist/src/composer/composer-attachments.js +0 -47
  64. package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
  65. package/dist/src/composer/composer-trigger-popover.js +0 -29
  66. package/dist/src/composer/composer.d.ts +0 -15
  67. package/dist/src/composer/composer.js +0 -180
  68. package/dist/src/composer/drop-controller.d.ts +0 -8
  69. package/dist/src/composer/drop-controller.js +0 -69
  70. package/dist/src/composer/index.js +0 -6
  71. package/dist/src/composer/types.d.ts +0 -62
  72. package/dist/src/composer/types.js +0 -1
  73. package/dist/src/contracts.d.ts +0 -94
  74. package/dist/src/contracts.js +0 -1
  75. package/dist/src/conversation.d.ts +0 -13
  76. package/dist/src/conversation.js +0 -18
  77. package/dist/src/index.js +0 -12
  78. package/dist/src/internal/cn.d.ts +0 -1
  79. package/dist/src/internal/cn.js +0 -3
  80. package/dist/src/message-panel.d.ts +0 -4
  81. package/dist/src/message-panel.js +0 -144
  82. package/dist/src/message.d.ts +0 -36
  83. package/dist/src/message.js +0 -83
  84. package/dist/src/parts/index.d.ts +0 -187
  85. package/dist/src/parts/index.js +0 -332
  86. package/dist/src/parts/kind.d.ts +0 -3
  87. package/dist/src/parts/kind.js +0 -69
  88. package/dist/src/parts/plan.d.ts +0 -10
  89. package/dist/src/parts/plan.js +0 -40
  90. package/dist/src/parts/primitives.d.ts +0 -70
  91. package/dist/src/parts/primitives.js +0 -153
  92. package/dist/src/registry.d.ts +0 -4
  93. package/dist/src/registry.js +0 -31
  94. package/dist/src/status.d.ts +0 -26
  95. package/dist/src/status.js +0 -74
  96. /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
  97. /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
  98. /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
  99. /package/{dist/src/index.d.ts → src/index.ts} +0 -0
@@ -0,0 +1,389 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ MessagePanel,
4
+ UnknownPart,
5
+ type PartRendererContext,
6
+ } from "../src";
7
+ import {
8
+ ChatComposer,
9
+ Composer,
10
+ DropOverlay,
11
+ } from "../src/composer";
12
+ import {
13
+ allPartsMessage,
14
+ basicMessages,
15
+ demoMessageAdapter,
16
+ errorAttachment,
17
+ panelScenarios,
18
+ readyAttachment,
19
+ uploadingAttachment,
20
+ type DemoAttachment,
21
+ } from "./fixtures";
22
+
23
+ const noOp = () => undefined;
24
+
25
+ function GalleryHeading({
26
+ eyebrow,
27
+ title,
28
+ description,
29
+ }: {
30
+ eyebrow: string;
31
+ title: string;
32
+ description: string;
33
+ }) {
34
+ return (
35
+ <header style={{ display: "grid", gap: 8 }}>
36
+ <small style={{ color: "var(--primary)", fontWeight: 700 }}>{eyebrow}</small>
37
+ <h2 style={{ margin: 0, fontSize: 24 }}>{title}</h2>
38
+ <p style={{ maxWidth: 720, margin: 0, color: "var(--muted-foreground)", lineHeight: 1.6 }}>
39
+ {description}
40
+ </p>
41
+ </header>
42
+ );
43
+ }
44
+
45
+ function ComposerScenario({
46
+ id,
47
+ title,
48
+ value,
49
+ attachments = [],
50
+ status = "ready",
51
+ overlay = false,
52
+ }: {
53
+ id: string;
54
+ title: string;
55
+ value?: string;
56
+ attachments?: DemoAttachment[];
57
+ status?: "ready" | "submitted" | "streaming" | "error";
58
+ overlay?: boolean;
59
+ }) {
60
+ return (
61
+ <article
62
+ data-composer-scenario={id}
63
+ style={{ position: "relative", display: "grid", gap: 8 }}
64
+ >
65
+ <strong style={{ fontSize: 13 }}>{title}</strong>
66
+ <Composer
67
+ value={value}
68
+ status={status}
69
+ attachments={attachments}
70
+ onValueChange={noOp}
71
+ onSubmit={noOp}
72
+ onStop={noOp}
73
+ onFilesAdded={noOp}
74
+ onAttachmentRemove={noOp}
75
+ onAttachmentRetry={noOp}
76
+ onAttachmentReorder={noOp}
77
+ />
78
+ {overlay && <DropOverlay />}
79
+ </article>
80
+ );
81
+ }
82
+
83
+ function ComposerInteractionHarness() {
84
+ const [value, setValue] = useState("保留中的草稿");
85
+ const [attachments, setAttachments] = useState<DemoAttachment[]>([
86
+ {
87
+ id: "alpha",
88
+ filename: "alpha.txt",
89
+ status: "ready",
90
+ mediaType: "text/plain",
91
+ size: 12,
92
+ },
93
+ {
94
+ id: "bravo",
95
+ filename: "bravo.txt",
96
+ status: "ready",
97
+ mediaType: "text/plain",
98
+ size: 12,
99
+ },
100
+ ]);
101
+ const [events, setEvents] = useState<string[]>([]);
102
+ const record = (event: string) =>
103
+ setEvents((current) => [...current, event]);
104
+
105
+ return (
106
+ <section
107
+ data-composer-interaction-harness=""
108
+ style={{ display: "grid", gap: 16, marginTop: 48 }}
109
+ >
110
+ <GalleryHeading
111
+ eyebrow="INTERACTION CONTRACTS"
112
+ title="Composer 交互验收"
113
+ description="这个可操作夹具覆盖受控与非受控输入、三种文件入口、局部拖放隔离、校验、提交、停止、重试、移除和附件重排。"
114
+ />
115
+ <div
116
+ style={{
117
+ display: "grid",
118
+ gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
119
+ gap: 16,
120
+ }}
121
+ >
122
+ <Composer
123
+ data-harness-composer="primary"
124
+ value={value}
125
+ onValueChange={setValue}
126
+ status="ready"
127
+ attachments={attachments}
128
+ accept=".txt,image/png"
129
+ maxFiles={6}
130
+ maxFileSize={1024}
131
+ onFilesAdded={(files) => {
132
+ record(`add:${files.map((file) => file.name).join(",")}`);
133
+ setAttachments((current) => [
134
+ ...current,
135
+ ...files.map((file, index) => ({
136
+ id: `${file.name}-${current.length + index}`,
137
+ filename: file.name,
138
+ status: "ready" as const,
139
+ mediaType: file.type,
140
+ size: file.size,
141
+ })),
142
+ ]);
143
+ }}
144
+ onAttachmentRemove={(attachment) => {
145
+ record(`remove:${attachment.filename}`);
146
+ setAttachments((current) =>
147
+ current.filter((item) => item.id !== attachment.id),
148
+ );
149
+ }}
150
+ onAttachmentRetry={(attachment) =>
151
+ record(`retry:${attachment.filename}`)
152
+ }
153
+ onAttachmentReorder={(fromIndex, toIndex) => {
154
+ record(`reorder:${fromIndex}:${toIndex}`);
155
+ setAttachments((current) => {
156
+ const next = [...current];
157
+ const [moved] = next.splice(fromIndex, 1);
158
+ next.splice(toIndex, 0, moved);
159
+ return next;
160
+ });
161
+ }}
162
+ onValidationError={(message) => record(`invalid:${message}`)}
163
+ onSubmit={({ text }) => record(`submit:${text}`)}
164
+ />
165
+ <ChatComposer
166
+ data-harness-composer="secondary"
167
+ defaultValue=""
168
+ status="ready"
169
+ commands={[
170
+ {
171
+ name: "plan",
172
+ description: "先制定计划",
173
+ prompt: "请先制定计划:\n",
174
+ },
175
+ {
176
+ name: "research",
177
+ description: "深入调研",
178
+ prompt: "请深入调研:\n",
179
+ },
180
+ ]}
181
+ onFilesAdded={(files) =>
182
+ record(`secondary:${files.map((file) => file.name).join(",")}`)
183
+ }
184
+ onSubmit={noOp}
185
+ />
186
+ <Composer
187
+ data-harness-composer="uncontrolled"
188
+ defaultValue="非受控草稿"
189
+ status="ready"
190
+ onSubmit={({ text }) => record(`uncontrolled:${text}`)}
191
+ />
192
+ <Composer
193
+ data-harness-composer="streaming"
194
+ value="生成中"
195
+ status="streaming"
196
+ onValueChange={noOp}
197
+ onSubmit={noOp}
198
+ onStop={() => record("stop")}
199
+ />
200
+ <div data-sb-message-panel="" data-harness-panel-drop="">
201
+ <Composer
202
+ data-harness-composer="panel-scope"
203
+ defaultValue=""
204
+ status="ready"
205
+ dropScope="panel"
206
+ onFilesAdded={(files) =>
207
+ record(`panel:${files.map((file) => file.name).join(",")}`)
208
+ }
209
+ onSubmit={noOp}
210
+ />
211
+ </div>
212
+ </div>
213
+ <output data-harness-order="">
214
+ {attachments.map((attachment) => attachment.filename).join(",")}
215
+ </output>
216
+ <output data-harness-events="">{events.join("|")}</output>
217
+ </section>
218
+ );
219
+ }
220
+
221
+ export function MessagePanelGallery() {
222
+ const [theme, setTheme] = useState<"light" | "dark">("dark");
223
+ const rootClass = theme === "dark" ? "dark" : "";
224
+ useEffect(() => {
225
+ document.documentElement.classList.toggle("dark", theme === "dark");
226
+ }, [theme]);
227
+ const unknownContext: PartRendererContext = {
228
+ messageId: "unknown",
229
+ partIndex: 0,
230
+ activityId: "unknown:0",
231
+ isActive: false,
232
+ isLatestOfKind: true,
233
+ status: "ready",
234
+ actions: {},
235
+ };
236
+
237
+ return (
238
+ <main
239
+ className={rootClass}
240
+ data-message-panel-gallery=""
241
+ style={{
242
+ minHeight: "100dvh",
243
+ padding: "32px clamp(16px, 5vw, 72px) 80px",
244
+ color: "var(--foreground)",
245
+ background: "var(--background)",
246
+ fontFamily: "var(--font-sans)",
247
+ }}
248
+ >
249
+ <header
250
+ style={{
251
+ display: "flex",
252
+ alignItems: "flex-start",
253
+ justifyContent: "space-between",
254
+ gap: 24,
255
+ marginBottom: 48,
256
+ }}
257
+ >
258
+ <div>
259
+ <small style={{ color: "var(--primary)", fontWeight: 700 }}>
260
+ @springbrand/message-panel
261
+ </small>
262
+ <h1 style={{ margin: "8px 0", fontSize: "clamp(30px, 5vw, 52px)" }}>
263
+ Message Panel Gallery
264
+ </h1>
265
+ <p style={{ margin: 0, color: "var(--muted-foreground)" }}>
266
+ 16 类 Part、7 类 Panel 状态和 7 类 Composer 状态的稳定验收展厅。
267
+ </p>
268
+ </div>
269
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
270
+ <a
271
+ href="/__message-panel-chat"
272
+ style={{
273
+ padding: "8px 12px",
274
+ border: "1px solid var(--border)",
275
+ borderRadius: 10,
276
+ color: "var(--foreground)",
277
+ background: "var(--card)",
278
+ fontSize: 13,
279
+ textDecoration: "none",
280
+ }}
281
+ >
282
+ 打开单 Chat 验收
283
+ </a>
284
+ <button
285
+ type="button"
286
+ onClick={() =>
287
+ setTheme((current) =>
288
+ current === "dark" ? "light" : "dark",
289
+ )
290
+ }
291
+ style={{
292
+ padding: "8px 12px",
293
+ border: "1px solid var(--border)",
294
+ borderRadius: 10,
295
+ color: "var(--foreground)",
296
+ background: "var(--card)",
297
+ }}
298
+ >
299
+ {theme === "dark" ? "切换浅色" : "切换深色"}
300
+ </button>
301
+ </div>
302
+ </header>
303
+
304
+ <section style={{ display: "grid", gap: 20, marginBottom: 56 }}>
305
+ <GalleryHeading
306
+ eyebrow="ROLE + PARTS"
307
+ title="完整叙事流"
308
+ description="用户与 Assistant 两种角色保持原始 Part 顺序;下方 Assistant 消息覆盖首版全部 16 种展示形态。"
309
+ />
310
+ <div style={{ height: 1100, overflow: "hidden", border: "1px solid var(--border)", borderRadius: 18 }}>
311
+ <MessagePanel
312
+ messages={[basicMessages[0], allPartsMessage]}
313
+ status="ready"
314
+ adapter={demoMessageAdapter}
315
+ actions={{
316
+ onSuggestion: noOp,
317
+ onAnswer: noOp,
318
+ onApprove: noOp,
319
+ onPreview: noOp,
320
+ onDownload: noOp,
321
+ onRetry: noOp,
322
+ }}
323
+ />
324
+ </div>
325
+ <div data-scenario="unknown-fallback">
326
+ <UnknownPart part={{ type: "data-future-component", data: { hello: "world" } }} />
327
+ <span style={{ display: "none" }}>{unknownContext.activityId}</span>
328
+ </div>
329
+ </section>
330
+
331
+ <section style={{ display: "grid", gap: 20, marginBottom: 56 }}>
332
+ <GalleryHeading
333
+ eyebrow="PANEL STATES"
334
+ title="7 类消息生命周期"
335
+ description="Hydrating、Recovering 与 Background 是正交覆盖态;错误和中断不会遗留无限 spinner。"
336
+ />
337
+ <div
338
+ style={{
339
+ display: "grid",
340
+ gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
341
+ gap: 16,
342
+ }}
343
+ >
344
+ {panelScenarios.map((scenario) => (
345
+ <article key={scenario.id} data-panel-scenario={scenario.id} style={{ display: "grid", gap: 8 }}>
346
+ <strong style={{ fontSize: 13 }}>{scenario.title}</strong>
347
+ <div style={{ height: 300, overflow: "hidden", border: "1px solid var(--border)", borderRadius: 16 }}>
348
+ <MessagePanel
349
+ messages={scenario.messages}
350
+ status={scenario.status}
351
+ isHydrating={scenario.flags?.isHydrating}
352
+ isRecovering={scenario.flags?.isRecovering}
353
+ isServerStreaming={scenario.flags?.isServerStreaming}
354
+ error={scenario.error}
355
+ adapter={demoMessageAdapter}
356
+ actions={{ onRetry: noOp }}
357
+ />
358
+ </div>
359
+ </article>
360
+ ))}
361
+ </div>
362
+ </section>
363
+
364
+ <section style={{ display: "grid", gap: 20 }}>
365
+ <GalleryHeading
366
+ eyebrow="COMPOSER STATES"
367
+ title="7 类输入与附件状态"
368
+ description="选择、粘贴和拖入文件共享一个 callback;上传中阻止发送,失败可重试,附件支持键盘等价重排。"
369
+ />
370
+ <div
371
+ style={{
372
+ display: "grid",
373
+ gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))",
374
+ gap: 22,
375
+ }}
376
+ >
377
+ <ComposerScenario id="empty" title="Empty" />
378
+ <ComposerScenario id="drafting" title="Drafting" value="为这份数据生成报告" />
379
+ <ComposerScenario id="drag-active" title="Drag active" overlay />
380
+ <ComposerScenario id="uploading" title="Uploading" attachments={[uploadingAttachment]} />
381
+ <ComposerScenario id="attachment-error" title="Attachment error" value="不要丢掉这份草稿" attachments={[errorAttachment]} />
382
+ <ComposerScenario id="ready-with-attachments" title="Ready with attachments" attachments={[readyAttachment]} />
383
+ <ComposerScenario id="submitted-streaming" title="Submitted / Streaming" value="正在发送…" status="streaming" />
384
+ </div>
385
+ </section>
386
+ <ComposerInteractionHarness />
387
+ </main>
388
+ );
389
+ }
package/package.json CHANGED
@@ -1,28 +1,19 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.1.2",
3
+ "version": "0.1.3-alpha.1",
4
4
  "type": "module",
5
5
  "files": [
6
- "dist",
7
- "src/styles"
6
+ "src",
7
+ "demo"
8
8
  ],
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
12
  "exports": {
13
- ".": {
14
- "types": "./dist/src/index.d.ts",
15
- "import": "./dist/src/index.js"
16
- },
17
- "./composer": {
18
- "types": "./dist/src/composer/index.d.ts",
19
- "import": "./dist/src/composer/index.js"
20
- },
13
+ ".": "./src/index.ts",
14
+ "./composer": "./src/composer/index.ts",
21
15
  "./styles.css": "./src/styles/index.css",
22
- "./demo": {
23
- "types": "./dist/demo/index.d.ts",
24
- "import": "./dist/demo/index.js"
25
- }
16
+ "./demo": "./demo/index.ts"
26
17
  },
27
18
  "peerDependencies": {
28
19
  "ai": "^7.0.0",
@@ -47,7 +38,6 @@
47
38
  "typescript": "^7.0.2"
48
39
  },
49
40
  "scripts": {
50
- "build": "tsc -p tsconfig.build.json",
51
41
  "typecheck": "tsc --noEmit"
52
42
  }
53
43
  }
@@ -0,0 +1,32 @@
1
+ import type { UIMessage } from "ai";
2
+ import type { MessageAdapter } from "../contracts";
3
+ import { resolvePartKind } from "../parts/kind";
4
+
5
+ export function uiMessageText(message: UIMessage): string {
6
+ return message.parts
7
+ .filter(
8
+ (part): part is Extract<UIMessage["parts"][number], { type: "text" }> =>
9
+ part.type === "text",
10
+ )
11
+ .map((part) => part.text)
12
+ .join("\n");
13
+ }
14
+
15
+ export const aiSdkMessageAdapter: MessageAdapter<UIMessage> = {
16
+ getId: (message, index) => message.id || `message-${index}`,
17
+ getRole: (message) => message.role,
18
+ getParts: (message) => message.parts,
19
+ getMetadata: (message) => message.metadata,
20
+ getText: uiMessageText,
21
+ isPartRenderable: (part) => {
22
+ const record =
23
+ part != null && typeof part === "object"
24
+ ? (part as Record<string, unknown>)
25
+ : {};
26
+ if (record.type === "step-start") return false;
27
+ if (record.type === "text" || record.type === "reasoning") {
28
+ return typeof record.text === "string" && record.text.trim().length > 0;
29
+ }
30
+ return resolvePartKind(part) !== null || typeof record.type === "string";
31
+ },
32
+ };