@assistant-ui/react 0.10.26 → 0.10.28
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/dist/api/AssistantRuntime.d.ts +1 -1
- package/dist/api/AssistantRuntime.js.map +1 -1
- package/dist/api/ThreadRuntime.d.ts +9 -0
- package/dist/api/ThreadRuntime.d.ts.map +1 -1
- package/dist/api/ThreadRuntime.js +4 -0
- package/dist/api/ThreadRuntime.js.map +1 -1
- package/dist/primitives/message/MessagePartsGroupedByParentId.d.ts +122 -0
- package/dist/primitives/message/MessagePartsGroupedByParentId.d.ts.map +1 -0
- package/dist/primitives/message/MessagePartsGroupedByParentId.js +184 -0
- package/dist/primitives/message/MessagePartsGroupedByParentId.js.map +1 -0
- package/dist/primitives/message/index.d.ts +1 -0
- package/dist/primitives/message/index.d.ts.map +1 -1
- package/dist/primitives/message/index.js +3 -1
- package/dist/primitives/message/index.js.map +1 -1
- package/dist/runtimes/core/BaseThreadRuntimeCore.d.ts +2 -0
- package/dist/runtimes/core/BaseThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/core/BaseThreadRuntimeCore.js +4 -0
- package/dist/runtimes/core/BaseThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/core/ThreadRuntimeCore.d.ts +2 -0
- package/dist/runtimes/core/ThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/external-store/ExternalStoreAdapter.d.ts +3 -1
- package/dist/runtimes/external-store/ExternalStoreAdapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.js +39 -24
- package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/external-store/ThreadMessageLike.d.ts +1 -0
- package/dist/runtimes/external-store/ThreadMessageLike.d.ts.map +1 -1
- package/dist/runtimes/external-store/ThreadMessageLike.js +8 -4
- package/dist/runtimes/external-store/ThreadMessageLike.js.map +1 -1
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js +3 -0
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js.map +1 -1
- package/dist/runtimes/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/runtimes/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
- package/dist/runtimes/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/types/AssistantTypes.d.ts +4 -0
- package/dist/types/AssistantTypes.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/AssistantRuntime.ts +1 -1
- package/src/api/ThreadRuntime.ts +14 -0
- package/src/primitives/message/MessagePartsGroupedByParentId.tsx +415 -0
- package/src/primitives/message/index.ts +1 -0
- package/src/runtimes/core/BaseThreadRuntimeCore.tsx +5 -0
- package/src/runtimes/core/ThreadRuntimeCore.tsx +3 -0
- package/src/runtimes/external-store/ExternalStoreAdapter.tsx +3 -1
- package/src/runtimes/external-store/ExternalStoreThreadRuntimeCore.tsx +66 -39
- package/src/runtimes/external-store/ThreadMessageLike.tsx +10 -4
- package/src/runtimes/remote-thread-list/EMPTY_THREAD_CORE.tsx +4 -0
- package/src/types/AssistantTypes.ts +4 -0
package/src/api/ThreadRuntime.ts
CHANGED
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
} from "../runtimes/core/ThreadRuntimeCore";
|
10
10
|
import { ExportedMessageRepository } from "../runtimes/utils/MessageRepository";
|
11
11
|
import { AppendMessage, ThreadMessage, Unsubscribe } from "../types";
|
12
|
+
import { ThreadMessageLike } from "../runtimes/external-store";
|
12
13
|
import {
|
13
14
|
MessageRuntime,
|
14
15
|
MessageRuntimeImpl,
|
@@ -266,6 +267,14 @@ export type ThreadRuntime = {
|
|
266
267
|
|
267
268
|
export(): ExportedMessageRepository;
|
268
269
|
import(repository: ExportedMessageRepository): void;
|
270
|
+
|
271
|
+
/**
|
272
|
+
* Reset the thread with optional initial messages.
|
273
|
+
*
|
274
|
+
* @param initialMessages - Optional array of initial messages to populate the thread
|
275
|
+
*/
|
276
|
+
reset(initialMessages?: readonly ThreadMessageLike[]): void;
|
277
|
+
|
269
278
|
getMesssageByIndex(idx: number): MessageRuntime;
|
270
279
|
getMesssageById(messageId: string): MessageRuntime;
|
271
280
|
|
@@ -340,6 +349,7 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
|
|
340
349
|
this.stopSpeaking = this.stopSpeaking.bind(this);
|
341
350
|
this.export = this.export.bind(this);
|
342
351
|
this.import = this.import.bind(this);
|
352
|
+
this.reset = this.reset.bind(this);
|
343
353
|
this.getMesssageByIndex = this.getMesssageByIndex.bind(this);
|
344
354
|
this.getMesssageById = this.getMesssageById.bind(this);
|
345
355
|
this.subscribe = this.subscribe.bind(this);
|
@@ -403,6 +413,10 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
|
|
403
413
|
this._threadBinding.getState().import(data);
|
404
414
|
}
|
405
415
|
|
416
|
+
public reset(initialMessages?: readonly ThreadMessageLike[]) {
|
417
|
+
this._threadBinding.getState().reset(initialMessages);
|
418
|
+
}
|
419
|
+
|
406
420
|
public getMesssageByIndex(idx: number) {
|
407
421
|
if (idx < 0) throw new Error("Message index must be >= 0");
|
408
422
|
|
@@ -0,0 +1,415 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import {
|
4
|
+
type ComponentType,
|
5
|
+
type FC,
|
6
|
+
memo,
|
7
|
+
PropsWithChildren,
|
8
|
+
useMemo,
|
9
|
+
} from "react";
|
10
|
+
import {
|
11
|
+
TextMessagePartProvider,
|
12
|
+
useMessagePart,
|
13
|
+
useMessagePartRuntime,
|
14
|
+
useToolUIs,
|
15
|
+
} from "../../context";
|
16
|
+
import {
|
17
|
+
useMessage,
|
18
|
+
useMessageRuntime,
|
19
|
+
} from "../../context/react/MessageContext";
|
20
|
+
import { MessagePartRuntimeProvider } from "../../context/providers/MessagePartRuntimeProvider";
|
21
|
+
import { MessagePartPrimitiveText } from "../messagePart/MessagePartText";
|
22
|
+
import { MessagePartPrimitiveImage } from "../messagePart/MessagePartImage";
|
23
|
+
import type {
|
24
|
+
Unstable_AudioMessagePartComponent,
|
25
|
+
EmptyMessagePartComponent,
|
26
|
+
TextMessagePartComponent,
|
27
|
+
ImageMessagePartComponent,
|
28
|
+
SourceMessagePartComponent,
|
29
|
+
ToolCallMessagePartComponent,
|
30
|
+
ToolCallMessagePartProps,
|
31
|
+
FileMessagePartComponent,
|
32
|
+
ReasoningMessagePartComponent,
|
33
|
+
} from "../../types/MessagePartComponentTypes";
|
34
|
+
import { MessagePartPrimitiveInProgress } from "../messagePart/MessagePartInProgress";
|
35
|
+
import { MessagePartStatus } from "../../types/AssistantTypes";
|
36
|
+
|
37
|
+
type MessagePartGroup = {
|
38
|
+
parentId: string | undefined;
|
39
|
+
indices: number[];
|
40
|
+
};
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Groups message parts by their parent ID.
|
44
|
+
* Parts without a parent ID appear after grouped parts and remain ungrouped.
|
45
|
+
* The position of groups is based on the first occurrence of each parent ID.
|
46
|
+
*/
|
47
|
+
const groupMessagePartsByParentId = (
|
48
|
+
parts: readonly any[],
|
49
|
+
): MessagePartGroup[] => {
|
50
|
+
const groups: MessagePartGroup[] = [];
|
51
|
+
const parentIdToGroupIndex = new Map<string | undefined, number>();
|
52
|
+
const processedIndices = new Set<number>();
|
53
|
+
|
54
|
+
// First pass: process all parts with parent IDs
|
55
|
+
for (let i = 0; i < parts.length; i++) {
|
56
|
+
const part = parts[i];
|
57
|
+
const parentId = part?.parentId as string | undefined;
|
58
|
+
|
59
|
+
if (parentId !== undefined) {
|
60
|
+
let groupIndex = parentIdToGroupIndex.get(parentId);
|
61
|
+
|
62
|
+
if (groupIndex === undefined) {
|
63
|
+
// Create new group for this parent ID
|
64
|
+
groupIndex = groups.length;
|
65
|
+
groups.push({ parentId, indices: [] });
|
66
|
+
parentIdToGroupIndex.set(parentId, groupIndex);
|
67
|
+
}
|
68
|
+
|
69
|
+
groups[groupIndex]!.indices.push(i);
|
70
|
+
processedIndices.add(i);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
// Second pass: add ungrouped parts (those without parent ID)
|
75
|
+
for (let i = 0; i < parts.length; i++) {
|
76
|
+
if (!processedIndices.has(i)) {
|
77
|
+
// Add individual group for parts without parent ID
|
78
|
+
groups.push({ parentId: undefined, indices: [i] });
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
return groups;
|
83
|
+
};
|
84
|
+
|
85
|
+
const useMessagePartsGroupedByParentId = (): MessagePartGroup[] => {
|
86
|
+
const parts = useMessage((m) => m.content);
|
87
|
+
|
88
|
+
return useMemo(() => {
|
89
|
+
if (parts.length === 0) {
|
90
|
+
return [];
|
91
|
+
}
|
92
|
+
return groupMessagePartsByParentId(parts);
|
93
|
+
}, [parts]);
|
94
|
+
};
|
95
|
+
|
96
|
+
export namespace MessagePrimitiveUnstable_PartsGroupedByParentId {
|
97
|
+
export type Props = {
|
98
|
+
/**
|
99
|
+
* Component configuration for rendering different types of message content.
|
100
|
+
*
|
101
|
+
* You can provide custom components for each content type (text, image, file, etc.)
|
102
|
+
* and configure tool rendering behavior. If not provided, default components will be used.
|
103
|
+
*/
|
104
|
+
components?:
|
105
|
+
| {
|
106
|
+
/** Component for rendering empty messages */
|
107
|
+
Empty?: EmptyMessagePartComponent | undefined;
|
108
|
+
/** Component for rendering text content */
|
109
|
+
Text?: TextMessagePartComponent | undefined;
|
110
|
+
/** Component for rendering reasoning content (typically hidden) */
|
111
|
+
Reasoning?: ReasoningMessagePartComponent | undefined;
|
112
|
+
/** Component for rendering source content */
|
113
|
+
Source?: SourceMessagePartComponent | undefined;
|
114
|
+
/** Component for rendering image content */
|
115
|
+
Image?: ImageMessagePartComponent | undefined;
|
116
|
+
/** Component for rendering file content */
|
117
|
+
File?: FileMessagePartComponent | undefined;
|
118
|
+
/** Component for rendering audio content (experimental) */
|
119
|
+
Unstable_Audio?: Unstable_AudioMessagePartComponent | undefined;
|
120
|
+
/** Configuration for tool call rendering */
|
121
|
+
tools?:
|
122
|
+
| {
|
123
|
+
/** Map of tool names to their specific components */
|
124
|
+
by_name?:
|
125
|
+
| Record<string, ToolCallMessagePartComponent | undefined>
|
126
|
+
| undefined;
|
127
|
+
/** Fallback component for unregistered tools */
|
128
|
+
Fallback?: ComponentType<ToolCallMessagePartProps> | undefined;
|
129
|
+
}
|
130
|
+
| {
|
131
|
+
/** Override component that handles all tool calls */
|
132
|
+
Override: ComponentType<ToolCallMessagePartProps>;
|
133
|
+
}
|
134
|
+
| undefined;
|
135
|
+
|
136
|
+
/**
|
137
|
+
* Component for rendering grouped message parts with the same parent ID.
|
138
|
+
*
|
139
|
+
* When provided, this component will automatically wrap message parts that share
|
140
|
+
* the same parent ID, allowing you to create collapsible sections, custom styling,
|
141
|
+
* or other grouped presentations.
|
142
|
+
*
|
143
|
+
* The component receives:
|
144
|
+
* - `parentId`: The parent ID shared by all parts in the group (or undefined for ungrouped parts)
|
145
|
+
* - `indices`: Array of indices for the parts in this group
|
146
|
+
* - `children`: The rendered message part components
|
147
|
+
*
|
148
|
+
* @example
|
149
|
+
* ```tsx
|
150
|
+
* // Collapsible parent ID group
|
151
|
+
* Group: ({ parentId, indices, children }) => {
|
152
|
+
* if (!parentId) return <>{children}</>;
|
153
|
+
* return (
|
154
|
+
* <details className="parent-group">
|
155
|
+
* <summary>
|
156
|
+
* Group {parentId} ({indices.length} parts)
|
157
|
+
* </summary>
|
158
|
+
* <div className="parent-group-content">
|
159
|
+
* {children}
|
160
|
+
* </div>
|
161
|
+
* </details>
|
162
|
+
* );
|
163
|
+
* }
|
164
|
+
* ```
|
165
|
+
*
|
166
|
+
* @example
|
167
|
+
* ```tsx
|
168
|
+
* // Custom styled parent ID group
|
169
|
+
* Group: ({ parentId, indices, children }) => {
|
170
|
+
* if (!parentId) return <>{children}</>;
|
171
|
+
* return (
|
172
|
+
* <div className="border rounded-lg p-4 my-2">
|
173
|
+
* <div className="text-sm text-gray-600 mb-2">
|
174
|
+
* Related content ({parentId})
|
175
|
+
* </div>
|
176
|
+
* <div className="space-y-2">
|
177
|
+
* {children}
|
178
|
+
* </div>
|
179
|
+
* </div>
|
180
|
+
* );
|
181
|
+
* }
|
182
|
+
* ```
|
183
|
+
*
|
184
|
+
* @param parentId - The parent ID shared by all parts in this group (undefined for ungrouped parts)
|
185
|
+
* @param indices - Array of indices for the parts in this group
|
186
|
+
* @param children - Rendered message part components to display within the group
|
187
|
+
*/
|
188
|
+
Group?: ComponentType<
|
189
|
+
PropsWithChildren<{
|
190
|
+
parentId: string | undefined;
|
191
|
+
indices: number[];
|
192
|
+
}>
|
193
|
+
>;
|
194
|
+
}
|
195
|
+
| undefined;
|
196
|
+
};
|
197
|
+
}
|
198
|
+
|
199
|
+
const ToolUIDisplay = ({
|
200
|
+
Fallback,
|
201
|
+
...props
|
202
|
+
}: {
|
203
|
+
Fallback: ToolCallMessagePartComponent | undefined;
|
204
|
+
} & ToolCallMessagePartProps) => {
|
205
|
+
const Render = useToolUIs((s) => s.getToolUI(props.toolName)) ?? Fallback;
|
206
|
+
if (!Render) return null;
|
207
|
+
return <Render {...props} />;
|
208
|
+
};
|
209
|
+
|
210
|
+
const defaultComponents = {
|
211
|
+
Text: () => (
|
212
|
+
<p style={{ whiteSpace: "pre-line" }}>
|
213
|
+
<MessagePartPrimitiveText />
|
214
|
+
<MessagePartPrimitiveInProgress>
|
215
|
+
<span style={{ fontFamily: "revert" }}>{" \u25CF"}</span>
|
216
|
+
</MessagePartPrimitiveInProgress>
|
217
|
+
</p>
|
218
|
+
),
|
219
|
+
Reasoning: () => null,
|
220
|
+
Source: () => null,
|
221
|
+
Image: () => <MessagePartPrimitiveImage />,
|
222
|
+
File: () => null,
|
223
|
+
Unstable_Audio: () => null,
|
224
|
+
Group: ({ children }) => children,
|
225
|
+
} satisfies MessagePrimitiveUnstable_PartsGroupedByParentId.Props["components"];
|
226
|
+
|
227
|
+
type MessagePartComponentProps = {
|
228
|
+
components: MessagePrimitiveUnstable_PartsGroupedByParentId.Props["components"];
|
229
|
+
};
|
230
|
+
|
231
|
+
const MessagePartComponent: FC<MessagePartComponentProps> = ({
|
232
|
+
components: {
|
233
|
+
Text = defaultComponents.Text,
|
234
|
+
Reasoning = defaultComponents.Reasoning,
|
235
|
+
Image = defaultComponents.Image,
|
236
|
+
Source = defaultComponents.Source,
|
237
|
+
File = defaultComponents.File,
|
238
|
+
Unstable_Audio: Audio = defaultComponents.Unstable_Audio,
|
239
|
+
tools = {},
|
240
|
+
} = {},
|
241
|
+
}) => {
|
242
|
+
const MessagePartRuntime = useMessagePartRuntime();
|
243
|
+
|
244
|
+
const part = useMessagePart();
|
245
|
+
|
246
|
+
const type = part.type;
|
247
|
+
if (type === "tool-call") {
|
248
|
+
const addResult = (result: any) => MessagePartRuntime.addToolResult(result);
|
249
|
+
if ("Override" in tools)
|
250
|
+
return <tools.Override {...part} addResult={addResult} />;
|
251
|
+
const Tool = tools.by_name?.[part.toolName] ?? tools.Fallback;
|
252
|
+
return <ToolUIDisplay {...part} Fallback={Tool} addResult={addResult} />;
|
253
|
+
}
|
254
|
+
|
255
|
+
if (part.status.type === "requires-action")
|
256
|
+
throw new Error("Encountered unexpected requires-action status");
|
257
|
+
|
258
|
+
switch (type) {
|
259
|
+
case "text":
|
260
|
+
return <Text {...part} />;
|
261
|
+
|
262
|
+
case "reasoning":
|
263
|
+
return <Reasoning {...part} />;
|
264
|
+
|
265
|
+
case "source":
|
266
|
+
return <Source {...part} />;
|
267
|
+
|
268
|
+
case "image":
|
269
|
+
// eslint-disable-next-line jsx-a11y/alt-text
|
270
|
+
return <Image {...part} />;
|
271
|
+
|
272
|
+
case "file":
|
273
|
+
return <File {...part} />;
|
274
|
+
|
275
|
+
case "audio":
|
276
|
+
return <Audio {...part} />;
|
277
|
+
|
278
|
+
default:
|
279
|
+
const unhandledType: never = type;
|
280
|
+
throw new Error(`Unknown message part type: ${unhandledType}`);
|
281
|
+
}
|
282
|
+
};
|
283
|
+
|
284
|
+
type MessagePartProps = {
|
285
|
+
partIndex: number;
|
286
|
+
components: MessagePrimitiveUnstable_PartsGroupedByParentId.Props["components"];
|
287
|
+
};
|
288
|
+
|
289
|
+
const MessagePartImpl: FC<MessagePartProps> = ({ partIndex, components }) => {
|
290
|
+
const messageRuntime = useMessageRuntime();
|
291
|
+
const runtime = useMemo(
|
292
|
+
() => messageRuntime.getMessagePartByIndex(partIndex),
|
293
|
+
[messageRuntime, partIndex],
|
294
|
+
);
|
295
|
+
|
296
|
+
return (
|
297
|
+
<MessagePartRuntimeProvider runtime={runtime}>
|
298
|
+
<MessagePartComponent components={components} />
|
299
|
+
</MessagePartRuntimeProvider>
|
300
|
+
);
|
301
|
+
};
|
302
|
+
|
303
|
+
const MessagePart = memo(
|
304
|
+
MessagePartImpl,
|
305
|
+
(prev, next) =>
|
306
|
+
prev.partIndex === next.partIndex &&
|
307
|
+
prev.components?.Text === next.components?.Text &&
|
308
|
+
prev.components?.Reasoning === next.components?.Reasoning &&
|
309
|
+
prev.components?.Source === next.components?.Source &&
|
310
|
+
prev.components?.Image === next.components?.Image &&
|
311
|
+
prev.components?.File === next.components?.File &&
|
312
|
+
prev.components?.Unstable_Audio === next.components?.Unstable_Audio &&
|
313
|
+
prev.components?.tools === next.components?.tools &&
|
314
|
+
prev.components?.Group === next.components?.Group,
|
315
|
+
);
|
316
|
+
|
317
|
+
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
|
318
|
+
type: "complete",
|
319
|
+
});
|
320
|
+
|
321
|
+
const EmptyPartFallback: FC<{
|
322
|
+
status: MessagePartStatus;
|
323
|
+
component: TextMessagePartComponent;
|
324
|
+
}> = ({ status, component: Component }) => {
|
325
|
+
return (
|
326
|
+
<TextMessagePartProvider text="" isRunning={status.type === "running"}>
|
327
|
+
<Component type="text" text="" status={status} />
|
328
|
+
</TextMessagePartProvider>
|
329
|
+
);
|
330
|
+
};
|
331
|
+
|
332
|
+
const EmptyPartsImpl: FC<MessagePartComponentProps> = ({ components }) => {
|
333
|
+
const status =
|
334
|
+
useMessage((s) => s.status as MessagePartStatus) ?? COMPLETE_STATUS;
|
335
|
+
|
336
|
+
if (components?.Empty) return <components.Empty status={status} />;
|
337
|
+
|
338
|
+
return (
|
339
|
+
<EmptyPartFallback
|
340
|
+
status={status}
|
341
|
+
component={components?.Text ?? defaultComponents.Text}
|
342
|
+
/>
|
343
|
+
);
|
344
|
+
};
|
345
|
+
|
346
|
+
const EmptyParts = memo(
|
347
|
+
EmptyPartsImpl,
|
348
|
+
(prev, next) =>
|
349
|
+
prev.components?.Empty === next.components?.Empty &&
|
350
|
+
prev.components?.Text === next.components?.Text,
|
351
|
+
);
|
352
|
+
|
353
|
+
/**
|
354
|
+
* Renders the parts of a message grouped by their parent ID.
|
355
|
+
*
|
356
|
+
* This component automatically groups message parts that share the same parent ID,
|
357
|
+
* allowing you to create hierarchical or related content presentations. Parts without
|
358
|
+
* a parent ID appear after grouped parts and remain ungrouped.
|
359
|
+
*
|
360
|
+
* @example
|
361
|
+
* ```tsx
|
362
|
+
* <MessagePrimitive.Unstable_PartsGroupedByParentId
|
363
|
+
* components={{
|
364
|
+
* Text: ({ text }) => <p className="message-text">{text}</p>,
|
365
|
+
* Image: ({ image }) => <img src={image} alt="Message image" />,
|
366
|
+
* Group: ({ parentId, indices, children }) => {
|
367
|
+
* if (!parentId) return <>{children}</>;
|
368
|
+
* return (
|
369
|
+
* <div className="parent-group border rounded p-4">
|
370
|
+
* <h4>Related Content</h4>
|
371
|
+
* {children}
|
372
|
+
* </div>
|
373
|
+
* );
|
374
|
+
* }
|
375
|
+
* }}
|
376
|
+
* />
|
377
|
+
* ```
|
378
|
+
*/
|
379
|
+
export const MessagePrimitiveUnstable_PartsGroupedByParentId: FC<
|
380
|
+
MessagePrimitiveUnstable_PartsGroupedByParentId.Props
|
381
|
+
> = ({ components }) => {
|
382
|
+
const contentLength = useMessage((s) => s.content.length);
|
383
|
+
const messageGroups = useMessagePartsGroupedByParentId();
|
384
|
+
|
385
|
+
const partsElements = useMemo(() => {
|
386
|
+
if (contentLength === 0) {
|
387
|
+
return <EmptyParts components={components} />;
|
388
|
+
}
|
389
|
+
|
390
|
+
return messageGroups.map((group, groupIndex) => {
|
391
|
+
const GroupComponent = components?.Group ?? defaultComponents.Group;
|
392
|
+
|
393
|
+
return (
|
394
|
+
<GroupComponent
|
395
|
+
key={`group-${groupIndex}-${group.parentId ?? "ungrouped"}`}
|
396
|
+
parentId={group.parentId}
|
397
|
+
indices={group.indices}
|
398
|
+
>
|
399
|
+
{group.indices.map((partIndex) => (
|
400
|
+
<MessagePart
|
401
|
+
key={partIndex}
|
402
|
+
partIndex={partIndex}
|
403
|
+
components={components}
|
404
|
+
/>
|
405
|
+
))}
|
406
|
+
</GroupComponent>
|
407
|
+
);
|
408
|
+
});
|
409
|
+
}, [messageGroups, components, contentLength]);
|
410
|
+
|
411
|
+
return <>{partsElements}</>;
|
412
|
+
};
|
413
|
+
|
414
|
+
MessagePrimitiveUnstable_PartsGroupedByParentId.displayName =
|
415
|
+
"MessagePrimitive.Unstable_PartsGroupedByParentId";
|
@@ -4,3 +4,4 @@ export { MessagePrimitiveParts as Content } from "./MessageParts";
|
|
4
4
|
export { MessagePrimitiveIf as If } from "./MessageIf";
|
5
5
|
export { MessagePrimitiveAttachments as Attachments } from "./MessageAttachments";
|
6
6
|
export { MessagePrimitiveError as Error } from "./MessageError";
|
7
|
+
export { MessagePrimitiveUnstable_PartsGroupedByParentId as Unstable_PartsGroupedByParentId } from "./MessagePartsGroupedByParentId";
|
@@ -22,6 +22,7 @@ import { FeedbackAdapter } from "../adapters/feedback/FeedbackAdapter";
|
|
22
22
|
import { AttachmentAdapter } from "../adapters/attachment";
|
23
23
|
import { getThreadMessageText } from "../../utils/getThreadMessageText";
|
24
24
|
import { ModelContextProvider } from "../../model-context";
|
25
|
+
import { ThreadMessageLike } from "../external-store";
|
25
26
|
|
26
27
|
type BaseThreadAdapters = {
|
27
28
|
speech?: SpeechSynthesisAdapter | undefined;
|
@@ -194,6 +195,10 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
|
|
194
195
|
this._notifySubscribers();
|
195
196
|
}
|
196
197
|
|
198
|
+
public reset(initialMessages?: readonly ThreadMessageLike[]) {
|
199
|
+
this.import(ExportedMessageRepository.fromArray(initialMessages ?? []));
|
200
|
+
}
|
201
|
+
|
197
202
|
private _eventSubscribers = new Map<
|
198
203
|
ThreadRuntimeEventType,
|
199
204
|
Set<() => void>
|
@@ -6,6 +6,7 @@ import type { Unsubscribe } from "../../types/Unsubscribe";
|
|
6
6
|
import { SpeechSynthesisAdapter } from "../adapters/speech/SpeechAdapterTypes";
|
7
7
|
import { ChatModelRunOptions, ChatModelRunResult } from "../local";
|
8
8
|
import { ExportedMessageRepository } from "../utils/MessageRepository";
|
9
|
+
import { ThreadMessageLike } from "../external-store";
|
9
10
|
import {
|
10
11
|
ComposerRuntimeCore,
|
11
12
|
ThreadComposerRuntimeCore,
|
@@ -118,5 +119,7 @@ export type ThreadRuntimeCore = Readonly<{
|
|
118
119
|
import(repository: ExportedMessageRepository): void;
|
119
120
|
export(): ExportedMessageRepository;
|
120
121
|
|
122
|
+
reset(initialMessages?: readonly ThreadMessageLike[]): void;
|
123
|
+
|
121
124
|
unstable_on(event: ThreadRuntimeEventType, callback: () => void): Unsubscribe;
|
122
125
|
}>;
|
@@ -8,6 +8,7 @@ import {
|
|
8
8
|
import { FeedbackAdapter } from "../adapters/feedback/FeedbackAdapter";
|
9
9
|
import { SpeechSynthesisAdapter } from "../adapters/speech/SpeechAdapterTypes";
|
10
10
|
import { ThreadMessageLike } from "./ThreadMessageLike";
|
11
|
+
import { ExportedMessageRepository } from "../utils/MessageRepository";
|
11
12
|
|
12
13
|
export type ExternalStoreThreadData<TState extends "regular" | "archived"> = {
|
13
14
|
status: TState;
|
@@ -53,7 +54,8 @@ type ExternalStoreAdapterBase<T> = {
|
|
53
54
|
isDisabled?: boolean | undefined;
|
54
55
|
isRunning?: boolean | undefined;
|
55
56
|
isLoading?: boolean | undefined;
|
56
|
-
messages
|
57
|
+
messages?: readonly T[];
|
58
|
+
messageRepository?: ExportedMessageRepository;
|
57
59
|
suggestions?: readonly ThreadSuggestion[] | undefined;
|
58
60
|
extras?: unknown;
|
59
61
|
|
@@ -108,46 +108,79 @@ export class ExternalStoreThreadRuntimeCore
|
|
108
108
|
feedback: !!this._store.adapters?.feedback,
|
109
109
|
};
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
111
|
+
let messages: readonly ThreadMessage[];
|
112
|
+
|
113
|
+
if (store.messageRepository) {
|
114
|
+
// Handle messageRepository
|
115
|
+
if (
|
116
|
+
oldStore &&
|
116
117
|
oldStore.isRunning === store.isRunning &&
|
117
|
-
oldStore.
|
118
|
+
oldStore.messageRepository === store.messageRepository
|
118
119
|
) {
|
119
120
|
this._notifySubscribers();
|
120
|
-
// no conversion update
|
121
121
|
return;
|
122
122
|
}
|
123
|
-
}
|
124
123
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
124
|
+
// Clear and import the message repository
|
125
|
+
this.repository.clear();
|
126
|
+
this.repository.import(store.messageRepository);
|
127
|
+
|
128
|
+
messages = this.repository.getMessages();
|
129
|
+
} else if (store.messages) {
|
130
|
+
// Handle messages array
|
131
|
+
|
132
|
+
if (oldStore) {
|
133
|
+
// flush the converter cache when the convertMessage prop changes
|
134
|
+
if (oldStore.convertMessage !== store.convertMessage) {
|
135
|
+
this._converter = new ThreadMessageConverter();
|
136
|
+
} else if (
|
137
|
+
oldStore.isRunning === store.isRunning &&
|
138
|
+
oldStore.messages === store.messages
|
139
|
+
) {
|
140
|
+
this._notifySubscribers();
|
141
|
+
// no conversion update
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
messages = !store.convertMessage
|
147
|
+
? store.messages
|
148
|
+
: this._converter.convertMessages(store.messages, (cache, m, idx) => {
|
149
|
+
if (!store.convertMessage) return m;
|
150
|
+
|
151
|
+
const isLast = idx === store.messages!.length - 1;
|
152
|
+
const autoStatus = getAutoStatus(isLast, isRunning);
|
153
|
+
|
154
|
+
if (
|
155
|
+
cache &&
|
156
|
+
(cache.role !== "assistant" ||
|
157
|
+
!isAutoStatus(cache.status) ||
|
158
|
+
cache.status === autoStatus)
|
159
|
+
)
|
160
|
+
return cache;
|
161
|
+
|
162
|
+
const messageLike = store.convertMessage(m, idx);
|
163
|
+
const newMessage = fromThreadMessageLike(
|
164
|
+
messageLike,
|
165
|
+
idx.toString(),
|
166
|
+
autoStatus,
|
167
|
+
);
|
168
|
+
(newMessage as any)[symbolInnerMessage] = m;
|
169
|
+
return newMessage;
|
170
|
+
});
|
171
|
+
|
172
|
+
for (let i = 0; i < messages.length; i++) {
|
173
|
+
const message = messages[i]!;
|
174
|
+
const parent = messages[i - 1];
|
175
|
+
this.repository.addOrUpdateMessage(parent?.id ?? null, message);
|
176
|
+
}
|
177
|
+
} else {
|
178
|
+
throw new Error(
|
179
|
+
"ExternalStoreAdapter must provide either 'messages' or 'messageRepository'",
|
180
|
+
);
|
181
|
+
}
|
150
182
|
|
183
|
+
// Common logic for both paths
|
151
184
|
if (messages.length > 0) this.ensureInitialized();
|
152
185
|
|
153
186
|
if (oldStore?.isRunning ?? false !== store.isRunning ?? false) {
|
@@ -158,12 +191,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
158
191
|
}
|
159
192
|
}
|
160
193
|
|
161
|
-
for (let i = 0; i < messages.length; i++) {
|
162
|
-
const message = messages[i]!;
|
163
|
-
const parent = messages[i - 1];
|
164
|
-
this.repository.addOrUpdateMessage(parent?.id ?? null, message);
|
165
|
-
}
|
166
|
-
|
167
194
|
if (this.assistantOptimisticId) {
|
168
195
|
this.repository.deleteMessage(this.assistantOptimisticId);
|
169
196
|
this.assistantOptimisticId = null;
|
@@ -41,6 +41,7 @@ export type ThreadMessageLike = {
|
|
41
41
|
readonly artifact?: any;
|
42
42
|
readonly result?: any | undefined;
|
43
43
|
readonly isError?: boolean | undefined;
|
44
|
+
readonly parentId?: string | undefined;
|
44
45
|
}
|
45
46
|
)[];
|
46
47
|
readonly id?: string | undefined;
|
@@ -104,17 +105,22 @@ export const fromThreadMessageLike = (
|
|
104
105
|
return part;
|
105
106
|
|
106
107
|
case "tool-call": {
|
108
|
+
const { parentId, ...basePart } = part;
|
109
|
+
const commonProps = {
|
110
|
+
...basePart,
|
111
|
+
toolCallId: part.toolCallId ?? "tool-" + generateId(),
|
112
|
+
...(parentId !== undefined && { parentId }),
|
113
|
+
};
|
114
|
+
|
107
115
|
if (part.args) {
|
108
116
|
return {
|
109
|
-
...
|
110
|
-
toolCallId: part.toolCallId ?? "tool-" + generateId(),
|
117
|
+
...commonProps,
|
111
118
|
args: part.args,
|
112
119
|
argsText: JSON.stringify(part.args),
|
113
120
|
};
|
114
121
|
}
|
115
122
|
return {
|
116
|
-
...
|
117
|
-
toolCallId: part.toolCallId ?? "tool-" + generateId(),
|
123
|
+
...commonProps,
|
118
124
|
args:
|
119
125
|
part.args ??
|
120
126
|
parsePartialJsonObject(part.argsText ?? "") ??
|