@springbrand/message-panel 0.1.1 → 0.1.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.
Files changed (99) hide show
  1. package/dist/demo/camel-chat-showcase.d.ts +1 -0
  2. package/dist/demo/camel-chat-showcase.js +1007 -0
  3. package/dist/demo/fixtures.d.ts +32 -0
  4. package/dist/demo/fixtures.js +250 -0
  5. package/dist/demo/index.js +4 -0
  6. package/dist/demo/message-panel-chat-showcase.d.ts +1 -0
  7. package/dist/demo/message-panel-chat-showcase.js +114 -0
  8. package/dist/demo/message-panel-gallery.d.ts +1 -0
  9. package/dist/demo/message-panel-gallery.js +130 -0
  10. package/dist/src/adapters/ai-sdk.d.ts +4 -0
  11. package/dist/src/adapters/ai-sdk.js +25 -0
  12. package/dist/src/camel/camel-chat-messages.d.ts +44 -0
  13. package/dist/src/camel/camel-chat-messages.js +421 -0
  14. package/dist/src/camel/camel-model-select.d.ts +13 -0
  15. package/dist/src/camel/camel-model-select.js +7 -0
  16. package/dist/src/camel/camel-plan-bar.d.ts +5 -0
  17. package/dist/src/camel/camel-plan-bar.js +24 -0
  18. package/dist/src/camel/camel-prompt-input.d.ts +32 -0
  19. package/dist/src/camel/camel-prompt-input.js +182 -0
  20. package/dist/src/camel/camel-tool-presentation.d.ts +21 -0
  21. package/dist/src/camel/camel-tool-presentation.js +784 -0
  22. package/dist/src/camel/camel-turn.d.ts +57 -0
  23. package/dist/src/camel/camel-turn.js +203 -0
  24. package/dist/src/camel/index.js +7 -0
  25. package/dist/src/camel/turn-summary-bar.d.ts +8 -0
  26. package/dist/src/camel/turn-summary-bar.js +12 -0
  27. package/dist/src/chat-summary-panel.d.ts +22 -0
  28. package/dist/src/chat-summary-panel.js +26 -0
  29. package/dist/src/composer/chat-composer.d.ts +4 -0
  30. package/dist/src/composer/chat-composer.js +90 -0
  31. package/dist/src/composer/composer-attachments.d.ts +13 -0
  32. package/dist/src/composer/composer-attachments.js +47 -0
  33. package/dist/src/composer/composer-trigger-popover.d.ts +31 -0
  34. package/dist/src/composer/composer-trigger-popover.js +29 -0
  35. package/dist/src/composer/composer.d.ts +15 -0
  36. package/dist/src/composer/composer.js +180 -0
  37. package/dist/src/composer/drop-controller.d.ts +8 -0
  38. package/dist/src/composer/drop-controller.js +69 -0
  39. package/dist/src/composer/index.js +6 -0
  40. package/dist/src/composer/types.d.ts +62 -0
  41. package/dist/src/composer/types.js +1 -0
  42. package/dist/src/contracts.d.ts +94 -0
  43. package/dist/src/contracts.js +1 -0
  44. package/dist/src/conversation.d.ts +13 -0
  45. package/dist/src/conversation.js +18 -0
  46. package/dist/src/index.js +12 -0
  47. package/dist/src/internal/cn.d.ts +1 -0
  48. package/dist/src/internal/cn.js +3 -0
  49. package/dist/src/message-panel.d.ts +4 -0
  50. package/dist/src/message-panel.js +144 -0
  51. package/dist/src/message.d.ts +36 -0
  52. package/dist/src/message.js +83 -0
  53. package/dist/src/parts/index.d.ts +187 -0
  54. package/dist/src/parts/index.js +332 -0
  55. package/dist/src/parts/kind.d.ts +3 -0
  56. package/dist/src/parts/kind.js +69 -0
  57. package/dist/src/parts/plan.d.ts +10 -0
  58. package/dist/src/parts/plan.js +40 -0
  59. package/dist/src/parts/primitives.d.ts +70 -0
  60. package/dist/src/parts/primitives.js +153 -0
  61. package/dist/src/registry.d.ts +4 -0
  62. package/dist/src/registry.js +31 -0
  63. package/dist/src/status.d.ts +26 -0
  64. package/dist/src/status.js +74 -0
  65. package/package.json +17 -8
  66. package/demo/camel-chat-showcase.tsx +0 -1265
  67. package/demo/fixtures.ts +0 -292
  68. package/demo/message-panel-chat-showcase.tsx +0 -250
  69. package/demo/message-panel-gallery.tsx +0 -389
  70. package/src/adapters/ai-sdk.ts +0 -32
  71. package/src/camel/camel-chat-messages.tsx +0 -1420
  72. package/src/camel/camel-model-select.tsx +0 -88
  73. package/src/camel/camel-plan-bar.tsx +0 -115
  74. package/src/camel/camel-prompt-input.tsx +0 -540
  75. package/src/camel/camel-tool-presentation.tsx +0 -1270
  76. package/src/camel/camel-turn.ts +0 -315
  77. package/src/camel/turn-summary-bar.tsx +0 -81
  78. package/src/chat-summary-panel.tsx +0 -229
  79. package/src/composer/chat-composer.tsx +0 -216
  80. package/src/composer/composer-attachments.tsx +0 -138
  81. package/src/composer/composer-trigger-popover.tsx +0 -236
  82. package/src/composer/composer.tsx +0 -425
  83. package/src/composer/drop-controller.ts +0 -80
  84. package/src/composer/types.ts +0 -79
  85. package/src/contracts.ts +0 -139
  86. package/src/conversation.tsx +0 -76
  87. package/src/internal/cn.ts +0 -3
  88. package/src/message-panel.tsx +0 -368
  89. package/src/message.tsx +0 -254
  90. package/src/parts/index.tsx +0 -1149
  91. package/src/parts/kind.ts +0 -67
  92. package/src/parts/plan.ts +0 -63
  93. package/src/parts/primitives.tsx +0 -439
  94. package/src/registry.ts +0 -44
  95. package/src/status.tsx +0 -132
  96. /package/{demo/index.ts → dist/demo/index.d.ts} +0 -0
  97. /package/{src/camel/index.ts → dist/src/camel/index.d.ts} +0 -0
  98. /package/{src/composer/index.ts → dist/src/composer/index.d.ts} +0 -0
  99. /package/{src/index.ts → dist/src/index.d.ts} +0 -0
package/src/contracts.ts DELETED
@@ -1,139 +0,0 @@
1
- import type { ComponentType, HTMLAttributes, ReactNode } from "react";
2
- import type { ApprovalDecision } from "@springbrand/agent-runtime/contracts";
3
-
4
- export type MessagePanelStatus = "ready" | "submitted" | "streaming" | "error";
5
- export type MessageRole = "user" | "assistant" | "system" | "tool";
6
-
7
- export interface ToolApprovalView {
8
- id: string;
9
- approved?: boolean;
10
- }
11
-
12
- export type ToolApprovalDecision = ApprovalDecision["decision"];
13
-
14
- export interface MessageAdapter<TMessage> {
15
- getId: (message: TMessage, index: number) => string;
16
- getRole: (message: TMessage) => MessageRole;
17
- getParts: (message: TMessage) => readonly unknown[];
18
- getMetadata?: (message: TMessage) => unknown;
19
- getText?: (message: TMessage) => string;
20
- isPartRenderable?: (part: unknown) => boolean;
21
- /** Extracts provider-specific tool approval state without leaking it into UI code. */
22
- getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
23
- /**
24
- * Marks snapshot-like parts that supersede older parts with the same key.
25
- * `update_plan` uses the built-in "plan" key when this is omitted.
26
- */
27
- getPartPresentationKey?: (part: unknown) => string | undefined;
28
- }
29
-
30
- export interface MessagePanelActions {
31
- onCopy?: (text: string) => void | Promise<void>;
32
- onRetry?: (messageId: string) => void | Promise<void>;
33
- onFork?: (messageId: string) => void | Promise<void>;
34
- onNavigate?: (target: string) => void;
35
- onPreview?: (artifact: unknown) => void;
36
- onDownload?: (artifact: unknown) => void;
37
- onAnswer?: (payload: unknown) => void | Promise<void>;
38
- onApprove?: (
39
- approvalId: string,
40
- decision: ToolApprovalDecision,
41
- ) => void | Promise<void>;
42
- onSuggestion?: (text: string) => void | Promise<void>;
43
- }
44
-
45
- export interface PartRendererContext {
46
- messageId: string;
47
- partIndex: number;
48
- activityId: string;
49
- isActive: boolean;
50
- isLatestOfKind: boolean;
51
- /** First user message after the assistant message that owns this part. */
52
- replyText?: string;
53
- approval?: ToolApprovalView;
54
- status: MessagePanelStatus;
55
- actions: MessagePanelActions;
56
- }
57
-
58
- export interface PartRendererProps<TPart = unknown> {
59
- part: TPart;
60
- context: PartRendererContext;
61
- }
62
-
63
- export type PartRendererComponent<TPart = unknown> = ComponentType<
64
- PartRendererProps<TPart>
65
- >;
66
-
67
- export interface PartRendererDefinition {
68
- name: string;
69
- match: (part: unknown) => boolean;
70
- component: PartRendererComponent;
71
- }
72
-
73
- export interface PartRendererRegistry {
74
- resolve: (part: unknown) => PartRendererComponent | undefined;
75
- names: readonly string[];
76
- }
77
-
78
- export interface MessagePanelSlots {
79
- assistantHeader?: ReactNode | ((messageId: string) => ReactNode);
80
- empty?: ReactNode;
81
- hydrating?: ReactNode;
82
- recovering?: ReactNode;
83
- background?: ReactNode;
84
- error?: ReactNode | ((error?: unknown) => ReactNode);
85
- beforeMessages?: ReactNode;
86
- afterMessages?: ReactNode;
87
- }
88
-
89
- export interface MessagePanelProps<TMessage>
90
- extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
91
- messages: readonly TMessage[];
92
- status: MessagePanelStatus;
93
- isHydrating?: boolean;
94
- isRecovering?: boolean;
95
- isServerStreaming?: boolean;
96
- error?: unknown;
97
- adapter: MessageAdapter<TMessage>;
98
- renderers?: PartRendererRegistry;
99
- actions?: MessagePanelActions;
100
- slots?: MessagePanelSlots;
101
- emptyTitle?: string;
102
- emptyDescription?: string;
103
- }
104
-
105
- export type RichPartKind =
106
- | "text"
107
- | "reasoning"
108
- | "tool"
109
- | "error"
110
- | "image"
111
- | "chart"
112
- | "agent_card"
113
- | "plan"
114
- | "schedule"
115
- | "parallel"
116
- | "subagents"
117
- | "confirmation"
118
- | "file"
119
- | "dashboard"
120
- | "ask_user"
121
- | "suggestions";
122
-
123
- export type MessageToolState =
124
- | "input-streaming"
125
- | "input-available"
126
- | "approval-requested"
127
- | "approval-responded"
128
- | "output-available"
129
- | "output-denied"
130
- | "output-error";
131
-
132
- export interface AIToolPart {
133
- type: string;
134
- state: MessageToolState;
135
- input?: unknown;
136
- output?: unknown;
137
- errorText?: string;
138
- toolName?: string;
139
- }
@@ -1,76 +0,0 @@
1
- import { ArrowDownIcon } from "lucide-react";
2
- import { forwardRef, useCallback, type ComponentProps, type HTMLAttributes } from "react";
3
- import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
4
- import { cn } from "./internal/cn";
5
-
6
- export type ConversationProps = ComponentProps<typeof StickToBottom>;
7
-
8
- export const Conversation = ({ className, ...props }: ConversationProps) => (
9
- <StickToBottom
10
- className={cn("sb-conversation", className)}
11
- initial="instant"
12
- resize="instant"
13
- role="log"
14
- {...props}
15
- />
16
- );
17
-
18
- export type ConversationContentProps = ComponentProps<
19
- typeof StickToBottom.Content
20
- >;
21
-
22
- export const ConversationContent = ({
23
- className,
24
- ...props
25
- }: ConversationContentProps) => (
26
- <StickToBottom.Content
27
- className={cn("sb-conversation__content", className)}
28
- {...props}
29
- />
30
- );
31
-
32
- export interface ConversationEmptyStateProps
33
- extends HTMLAttributes<HTMLDivElement> {
34
- title?: string;
35
- description?: string;
36
- icon?: React.ReactNode;
37
- }
38
-
39
- export const ConversationEmptyState = forwardRef<
40
- HTMLDivElement,
41
- ConversationEmptyStateProps
42
- >(({ className, title = "开始对话", description, icon, children, ...props }, ref) => (
43
- <div ref={ref} className={cn("sb-conversation__empty", className)} {...props}>
44
- {children ?? (
45
- <>
46
- {icon}
47
- <strong>{title}</strong>
48
- {description && <p>{description}</p>}
49
- </>
50
- )}
51
- </div>
52
- ));
53
- ConversationEmptyState.displayName = "ConversationEmptyState";
54
-
55
- export function ConversationScrollButton({
56
- className,
57
- ...props
58
- }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
59
- const { isAtBottom, scrollToBottom } = useStickToBottomContext();
60
- const handleClick = useCallback(() => {
61
- void scrollToBottom({ animation: "smooth" });
62
- }, [scrollToBottom]);
63
-
64
- if (isAtBottom) return null;
65
- return (
66
- <button
67
- type="button"
68
- className={cn("sb-conversation__scroll", className)}
69
- onClick={handleClick}
70
- aria-label="回到最新消息"
71
- {...props}
72
- >
73
- <ArrowDownIcon size={16} />
74
- </button>
75
- );
76
- }
@@ -1,3 +0,0 @@
1
- export function cn(...values: Array<string | false | null | undefined>): string {
2
- return values.filter(Boolean).join(" ");
3
- }
@@ -1,368 +0,0 @@
1
- import {
2
- forwardRef,
3
- useMemo,
4
- type ReactNode,
5
- } from "react";
6
- import type {
7
- MessageAdapter,
8
- MessagePanelActions,
9
- MessagePanelProps,
10
- MessagePanelSlots,
11
- MessageRole,
12
- PartRendererRegistry,
13
- } from "./contracts";
14
- import {
15
- Conversation,
16
- ConversationContent,
17
- ConversationEmptyState,
18
- ConversationScrollButton,
19
- } from "./conversation";
20
- import { cn } from "./internal/cn";
21
- import {
22
- AssistantMessageActions,
23
- BrandMark,
24
- Message,
25
- MessageContent,
26
- } from "./message";
27
- import {
28
- DefaultPartRenderer,
29
- resolvePartKind,
30
- stripNoticePrefix,
31
- } from "./parts";
32
- import { PanelStatus, ThinkingIndicator, TopLoadingBar } from "./status";
33
-
34
- function roleOf<T>(
35
- adapter: MessageAdapter<T>,
36
- message: T,
37
- ): MessageRole {
38
- return adapter.getRole(message);
39
- }
40
-
41
- function messageText<T>(
42
- adapter: MessageAdapter<T>,
43
- message: T,
44
- ): string {
45
- if (adapter.getText) return adapter.getText(message);
46
- return adapter
47
- .getParts(message)
48
- .map((part) => {
49
- if (
50
- part != null &&
51
- typeof part === "object" &&
52
- (part as { type?: unknown }).type === "text"
53
- ) {
54
- return String((part as { text?: unknown }).text ?? "");
55
- }
56
- return "";
57
- })
58
- .filter(Boolean)
59
- .join("\n");
60
- }
61
-
62
- function isNotice(metadata: unknown): boolean {
63
- if (metadata == null || typeof metadata !== "object") return false;
64
- const kind = (metadata as { deliveryKind?: unknown }).deliveryKind;
65
- return kind === "notice" || kind === "interim";
66
- }
67
-
68
- function slotValue(
69
- slot: MessagePanelSlots["assistantHeader"],
70
- messageId: string,
71
- ): ReactNode {
72
- return typeof slot === "function" ? slot(messageId) : slot;
73
- }
74
-
75
- function presentationKey<T>(
76
- adapter: MessageAdapter<T>,
77
- part: unknown,
78
- ): string | undefined {
79
- const customKey = adapter.getPartPresentationKey?.(part);
80
- if (customKey) return customKey;
81
- return resolvePartKind(part) === "plan" ? "plan" : undefined;
82
- }
83
-
84
- interface MessageRowProps<T> {
85
- message: T;
86
- index: number;
87
- messageId: string;
88
- isLastAssistant: boolean;
89
- isActive: boolean;
90
- adapter: MessageAdapter<T>;
91
- renderers?: PartRendererRegistry;
92
- actions: MessagePanelActions;
93
- slots: MessagePanelSlots;
94
- status: MessagePanelProps<T>["status"];
95
- latestActivityByKind: ReadonlyMap<string, string>;
96
- replyText?: string;
97
- }
98
-
99
- function MessageRow<T>({
100
- message,
101
- messageId,
102
- isLastAssistant,
103
- isActive,
104
- adapter,
105
- renderers,
106
- actions,
107
- slots,
108
- status,
109
- latestActivityByKind,
110
- replyText,
111
- }: MessageRowProps<T>) {
112
- const role = roleOf(adapter, message);
113
- const parts = adapter.getParts(message);
114
- const visibleParts = adapter.isPartRenderable
115
- ? parts.filter(adapter.isPartRenderable)
116
- : parts;
117
- const metadata = adapter.getMetadata?.(message);
118
- const text = messageText(adapter, message);
119
-
120
- if (isNotice(metadata)) {
121
- return (
122
- <Message from="assistant" data-message-id={messageId}>
123
- <div className="sb-notice-line">{stripNoticePrefix(text)}</div>
124
- </Message>
125
- );
126
- }
127
-
128
- if (role === "user") {
129
- return (
130
- <Message from="user" data-message-id={messageId} data-message-role="user">
131
- <MessageContent>
132
- {parts.map((part, partIndex) => {
133
- const Renderer = renderers?.resolve(part) ?? DefaultPartRenderer;
134
- const activityId = `${messageId}:${partIndex}`;
135
- const key = presentationKey(adapter, part);
136
- return (
137
- <Renderer
138
- key={activityId}
139
- part={part}
140
- context={{
141
- messageId,
142
- partIndex,
143
- activityId,
144
- isActive: false,
145
- isLatestOfKind:
146
- key === undefined ||
147
- latestActivityByKind.get(key) === activityId,
148
- approval: adapter.getToolApproval?.(part),
149
- status,
150
- actions,
151
- }}
152
- />
153
- );
154
- })}
155
- </MessageContent>
156
- </Message>
157
- );
158
- }
159
-
160
- return (
161
- <Message
162
- from="assistant"
163
- data-message-id={messageId}
164
- data-message-role="assistant"
165
- data-active={isActive ? "true" : undefined}
166
- >
167
- {slots.assistantHeader && (
168
- <div className="sb-assistant-header">
169
- {slotValue(slots.assistantHeader, messageId)}
170
- </div>
171
- )}
172
- <MessageContent>
173
- {parts.map((part, partIndex) => {
174
- const Renderer = renderers?.resolve(part) ?? DefaultPartRenderer;
175
- const activityId = `${messageId}:${partIndex}`;
176
- const key = presentationKey(adapter, part);
177
- return (
178
- <Renderer
179
- key={activityId}
180
- part={part}
181
- context={{
182
- messageId,
183
- partIndex,
184
- activityId,
185
- isActive,
186
- isLatestOfKind:
187
- key === undefined ||
188
- latestActivityByKind.get(key) === activityId,
189
- replyText,
190
- approval: adapter.getToolApproval?.(part),
191
- status,
192
- actions,
193
- }}
194
- />
195
- );
196
- })}
197
- {isActive && visibleParts.length === 0 && <ThinkingIndicator />}
198
- </MessageContent>
199
- {!isActive && (
200
- <AssistantMessageActions
201
- text={text}
202
- messageId={messageId}
203
- canRetry={isLastAssistant && status !== "streaming"}
204
- onCopy={actions.onCopy}
205
- onRetry={actions.onRetry}
206
- onFork={actions.onFork}
207
- />
208
- )}
209
- </Message>
210
- );
211
- }
212
-
213
- function MessagePanelInner<T>(
214
- {
215
- messages,
216
- status,
217
- isHydrating = false,
218
- isRecovering = false,
219
- isServerStreaming = false,
220
- error,
221
- adapter,
222
- renderers,
223
- actions = {},
224
- slots = {},
225
- className,
226
- emptyTitle = "开始对话",
227
- emptyDescription = "发送消息,Agent 会在这里展示思考、工具和产出。",
228
- ...props
229
- }: MessagePanelProps<T>,
230
- ref: React.ForwardedRef<HTMLDivElement>,
231
- ) {
232
- const ids = useMemo(
233
- () => messages.map((message, index) => adapter.getId(message, index)),
234
- [messages, adapter],
235
- );
236
- const lastAssistantIndex = useMemo(() => {
237
- for (let index = messages.length - 1; index >= 0; index -= 1) {
238
- if (roleOf(adapter, messages[index]) === "assistant") return index;
239
- }
240
- return -1;
241
- }, [messages, adapter]);
242
- const lastRole =
243
- messages.length > 0 ? roleOf(adapter, messages[messages.length - 1]) : undefined;
244
- const showPending =
245
- status === "submitted" &&
246
- (lastRole === "user" || messages.length === 0);
247
- const latestActivityByKind = useMemo(() => {
248
- const latest = new Map<string, string>();
249
- messages.forEach((message, messageIndex) => {
250
- if (roleOf(adapter, message) !== "assistant") return;
251
- adapter.getParts(message).forEach((part, partIndex) => {
252
- const key = presentationKey(adapter, part);
253
- if (key) latest.set(key, `${ids[messageIndex]}:${partIndex}`);
254
- });
255
- });
256
- return latest;
257
- }, [adapter, ids, messages]);
258
- const replyTextByMessageId = useMemo(() => {
259
- const replies = new Map<string, string>();
260
- messages.forEach((message, messageIndex) => {
261
- if (roleOf(adapter, message) !== "assistant") return;
262
- for (
263
- let replyIndex = messageIndex + 1;
264
- replyIndex < messages.length;
265
- replyIndex += 1
266
- ) {
267
- const reply = messages[replyIndex];
268
- if (roleOf(adapter, reply) !== "user") continue;
269
- const text = messageText(adapter, reply).trim();
270
- if (text) replies.set(ids[messageIndex], text);
271
- break;
272
- }
273
- });
274
- return replies;
275
- }, [adapter, ids, messages]);
276
-
277
- return (
278
- <div
279
- ref={ref}
280
- className={cn("sb-message-panel", className)}
281
- data-sb-message-panel=""
282
- data-status={status}
283
- data-hydrating={isHydrating ? "true" : undefined}
284
- aria-busy={status === "submitted" || status === "streaming" || isHydrating}
285
- {...props}
286
- >
287
- <TopLoadingBar status={status} />
288
- {isRecovering &&
289
- (slots.recovering ?? (
290
- <PanelStatus kind="recovering">正在恢复对话流…</PanelStatus>
291
- ))}
292
- {!isRecovering &&
293
- isServerStreaming &&
294
- (slots.background ?? (
295
- <PanelStatus kind="background">Agent 正在后台更新…</PanelStatus>
296
- ))}
297
- {status === "error" &&
298
- (typeof slots.error === "function"
299
- ? slots.error(error)
300
- : slots.error ?? (
301
- <PanelStatus kind="error">
302
- {error instanceof Error ? error.message : "这一轮出错了,请重试。"}
303
- </PanelStatus>
304
- ))}
305
- <Conversation>
306
- <ConversationContent>
307
- {slots.beforeMessages}
308
- {messages.length === 0 &&
309
- !isHydrating &&
310
- (slots.empty ?? (
311
- <ConversationEmptyState
312
- title={emptyTitle}
313
- description={emptyDescription}
314
- icon={<BrandMark className="sb-brand-mark--large" />}
315
- />
316
- ))}
317
- {messages.map((message, index) => (
318
- <MessageRow
319
- key={ids[index]}
320
- message={message}
321
- index={index}
322
- messageId={ids[index]}
323
- isLastAssistant={index === lastAssistantIndex}
324
- isActive={
325
- status === "streaming" &&
326
- index === messages.length - 1 &&
327
- roleOf(adapter, message) === "assistant"
328
- }
329
- adapter={adapter}
330
- renderers={renderers}
331
- actions={actions}
332
- slots={slots}
333
- status={status}
334
- latestActivityByKind={latestActivityByKind}
335
- replyText={replyTextByMessageId.get(ids[index])}
336
- />
337
- ))}
338
- {showPending && (
339
- <Message
340
- from="assistant"
341
- data-message-role="assistant"
342
- data-pending="true"
343
- >
344
- {slots.assistantHeader && (
345
- <div className="sb-assistant-header">
346
- {slotValue(slots.assistantHeader, "pending")}
347
- </div>
348
- )}
349
- <ThinkingIndicator />
350
- </Message>
351
- )}
352
- {slots.afterMessages}
353
- </ConversationContent>
354
- {isHydrating &&
355
- (slots.hydrating ?? (
356
- <div className="sb-hydrating-overlay">
357
- <PanelStatus kind="hydrating">正在恢复历史消息…</PanelStatus>
358
- </div>
359
- ))}
360
- <ConversationScrollButton />
361
- </Conversation>
362
- </div>
363
- );
364
- }
365
-
366
- export const MessagePanel = forwardRef(MessagePanelInner) as <T>(
367
- props: MessagePanelProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> },
368
- ) => React.ReactElement;