@assistant-ui/react 0.15.11 → 0.15.13
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/augmentations.d.ts +3 -25
- package/dist/augmentations.d.ts.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
- package/package.json +5 -6
- package/src/augmentations.ts +4 -2
- package/src/index.ts +2 -2
- package/src/tests/augmentations.test.ts +39 -0
- package/src/tests/in-memory-thread-list.test.tsx +1 -1
- package/dist/client/ExternalThread.d.ts +0 -48
- package/dist/client/ExternalThread.d.ts.map +0 -1
- package/dist/client/ExternalThread.js +0 -1348
- package/dist/client/ExternalThread.js.map +0 -1
- package/dist/client/SingleThreadList.d.ts +0 -9
- package/dist/client/SingleThreadList.d.ts.map +0 -1
- package/dist/client/SingleThreadList.js +0 -171
- package/dist/client/SingleThreadList.js.map +0 -1
- package/src/client/ExternalThread.ts +0 -1147
- package/src/client/SingleThreadList.ts +0 -118
- package/src/tests/external-thread-attachments.test.tsx +0 -370
- package/src/tests/external-thread-branches.test.tsx +0 -160
- package/src/tests/external-thread-feedback.test.tsx +0 -204
- package/src/tests/external-thread-parity.test.tsx +0 -494
- package/src/tests/external-thread-speech.test.tsx +0 -328
|
@@ -1,1147 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { resource, withKey } from "@assistant-ui/tap";
|
|
3
|
-
import {
|
|
4
|
-
type ClientElement,
|
|
5
|
-
type ClientOutput,
|
|
6
|
-
useClientLookup,
|
|
7
|
-
attachTransformScopes,
|
|
8
|
-
useClientResource,
|
|
9
|
-
Derived,
|
|
10
|
-
} from "@assistant-ui/store";
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
AddToolResultOptions,
|
|
14
|
-
AppendMessage,
|
|
15
|
-
Attachment,
|
|
16
|
-
AttachmentAdapter,
|
|
17
|
-
CreateAttachment,
|
|
18
|
-
PendingAttachment,
|
|
19
|
-
RespondToToolApprovalOptions,
|
|
20
|
-
ResumeToolCallOptions,
|
|
21
|
-
MessagePartStatus,
|
|
22
|
-
ThreadAssistantMessagePart,
|
|
23
|
-
ThreadUserMessagePart,
|
|
24
|
-
ThreadMessage,
|
|
25
|
-
ToolCallMessagePartStatus,
|
|
26
|
-
ExternalThreadQueueAdapter,
|
|
27
|
-
ExternalThreadBranchAdapter,
|
|
28
|
-
QueuePlacement,
|
|
29
|
-
FeedbackAdapter,
|
|
30
|
-
SpeechState,
|
|
31
|
-
SpeechSynthesisAdapter,
|
|
32
|
-
} from "@assistant-ui/core";
|
|
33
|
-
import { ToolResponse } from "assistant-stream";
|
|
34
|
-
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
35
|
-
import type { QueueItemState } from "@assistant-ui/core/store";
|
|
36
|
-
import type { ComposerSendOptions } from "@assistant-ui/core/store";
|
|
37
|
-
import {
|
|
38
|
-
fileMatchesAccept,
|
|
39
|
-
getThreadMessageText,
|
|
40
|
-
isCreateAttachment,
|
|
41
|
-
resolveToolApprovalResponse,
|
|
42
|
-
toMessagePartStatus,
|
|
43
|
-
} from "@assistant-ui/core/internal";
|
|
44
|
-
import { ModelContext, Suggestions } from "@assistant-ui/core/store";
|
|
45
|
-
import { Tools, DataRenderers } from "@assistant-ui/core/react";
|
|
46
|
-
import { SingleThreadList } from "./SingleThreadList";
|
|
47
|
-
|
|
48
|
-
const EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = [];
|
|
49
|
-
const EMPTY_BRANCH_IDS: readonly string[] = [];
|
|
50
|
-
|
|
51
|
-
export type ExternalThreadMessage = ThreadMessage & {
|
|
52
|
-
id: string;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
|
|
56
|
-
type: "complete",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const derivePartStatus = (
|
|
60
|
-
message: ExternalThreadMessage,
|
|
61
|
-
partIndex: number,
|
|
62
|
-
part: ThreadAssistantMessagePart | ThreadUserMessagePart,
|
|
63
|
-
): ToolCallMessagePartStatus => {
|
|
64
|
-
if (!message.status) return COMPLETE_STATUS;
|
|
65
|
-
return toMessagePartStatus(message, partIndex, part);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export type ExternalThreadProps = {
|
|
69
|
-
messages: readonly ExternalThreadMessage[];
|
|
70
|
-
isRunning?: boolean;
|
|
71
|
-
isLoading?: boolean | undefined;
|
|
72
|
-
state?: ReadonlyJSONValue | undefined;
|
|
73
|
-
extras?: unknown;
|
|
74
|
-
/**
|
|
75
|
-
* Whether sending new messages is currently disabled. When `true`, the
|
|
76
|
-
* thread composer's input remains usable but `send()` is a no-op and
|
|
77
|
-
* `composer.canSend` is `false`. Edit composers (saving message edits)
|
|
78
|
-
* intentionally ignore this flag.
|
|
79
|
-
*/
|
|
80
|
-
isSendDisabled?: boolean;
|
|
81
|
-
/**
|
|
82
|
-
* Callback for new messages (non-queue runtimes).
|
|
83
|
-
* @note Unused when `queue` is provided — new messages are routed through `queue.enqueue` instead.
|
|
84
|
-
*/
|
|
85
|
-
onNew?: (message: AppendMessage) => void;
|
|
86
|
-
onEdit?: (message: AppendMessage) => void;
|
|
87
|
-
onReload?: (parentId: string | null) => void;
|
|
88
|
-
onStartRun?: () => void;
|
|
89
|
-
onCancel?: () => void;
|
|
90
|
-
onResume?: (() => void) | undefined;
|
|
91
|
-
onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
|
|
92
|
-
/** Callback for resuming a tool call that is waiting for human input. */
|
|
93
|
-
onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
|
|
94
|
-
onLoadExternalState?: ((state: unknown) => void) | undefined;
|
|
95
|
-
attachmentAdapter?: AttachmentAdapter | undefined;
|
|
96
|
-
feedbackAdapter?: FeedbackAdapter | undefined;
|
|
97
|
-
speechAdapter?: SpeechSynthesisAdapter | undefined;
|
|
98
|
-
/** Queue adapter for runtimes that support message queuing and steering. */
|
|
99
|
-
queue?: ExternalThreadQueueAdapter;
|
|
100
|
-
/** Branch adapter for runtimes that track sibling variants of messages. */
|
|
101
|
-
branches?: ExternalThreadBranchAdapter;
|
|
102
|
-
/** Callback for tool approval decisions. Absent: responding to an approval throws a capability error. */
|
|
103
|
-
onRespondToToolApproval?: (options: RespondToToolApprovalOptions) => void;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
type MessageClientProps = {
|
|
107
|
-
message: ExternalThreadMessage;
|
|
108
|
-
index: number;
|
|
109
|
-
parentId: string | null;
|
|
110
|
-
onEdit?: (message: AppendMessage) => void;
|
|
111
|
-
onReload?: () => void;
|
|
112
|
-
queue?: ExternalThreadQueueAdapter | undefined;
|
|
113
|
-
branches?: ExternalThreadBranchAdapter | undefined;
|
|
114
|
-
onRespondToToolApproval?:
|
|
115
|
-
| ((options: RespondToToolApprovalOptions) => void)
|
|
116
|
-
| undefined;
|
|
117
|
-
onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
|
|
118
|
-
onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
|
|
119
|
-
attachmentAdapter?: AttachmentAdapter | undefined;
|
|
120
|
-
submittedFeedback: "positive" | "negative" | undefined;
|
|
121
|
-
onSubmitFeedback: (feedback: { type: "positive" | "negative" }) => void;
|
|
122
|
-
speech: SpeechState | undefined;
|
|
123
|
-
onSpeak: () => void;
|
|
124
|
-
onStopSpeaking: () => void;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Message Client - minimal implementation
|
|
128
|
-
const useMessageClient = ({
|
|
129
|
-
message,
|
|
130
|
-
index,
|
|
131
|
-
parentId,
|
|
132
|
-
onEdit,
|
|
133
|
-
onReload,
|
|
134
|
-
queue,
|
|
135
|
-
branches,
|
|
136
|
-
onRespondToToolApproval,
|
|
137
|
-
onAddToolResult,
|
|
138
|
-
onResumeToolCall,
|
|
139
|
-
attachmentAdapter,
|
|
140
|
-
submittedFeedback,
|
|
141
|
-
onSubmitFeedback,
|
|
142
|
-
speech,
|
|
143
|
-
onSpeak,
|
|
144
|
-
onStopSpeaking,
|
|
145
|
-
}: MessageClientProps): ClientOutput<"message"> => {
|
|
146
|
-
const [isCopied, setIsCopied] = useState(false);
|
|
147
|
-
const [isHovering, setIsHovering] = useState(false);
|
|
148
|
-
|
|
149
|
-
const partClients = useClientLookup(
|
|
150
|
-
message.content.map((part, idx) =>
|
|
151
|
-
withKey(
|
|
152
|
-
idx,
|
|
153
|
-
PartResource({
|
|
154
|
-
part,
|
|
155
|
-
status: derivePartStatus(message, idx, part),
|
|
156
|
-
messageId: message.id,
|
|
157
|
-
onRespondToToolApproval,
|
|
158
|
-
onAddToolResult,
|
|
159
|
-
onResumeToolCall,
|
|
160
|
-
}),
|
|
161
|
-
),
|
|
162
|
-
),
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
const attachmentClients = useClientLookup(
|
|
166
|
-
(message.attachments ?? []).map((attachment) =>
|
|
167
|
-
withKey(
|
|
168
|
-
attachment.id,
|
|
169
|
-
AttachmentResource({
|
|
170
|
-
attachment,
|
|
171
|
-
onRemove: () => {},
|
|
172
|
-
}),
|
|
173
|
-
),
|
|
174
|
-
),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const handleBeginEdit = () => {
|
|
178
|
-
if (!onEdit) throw new Error("Runtime does not support editing.");
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const handleSendEdit = (msg: AppendMessage) => {
|
|
182
|
-
if (!onEdit) throw new Error("Runtime does not support editing.");
|
|
183
|
-
onEdit({
|
|
184
|
-
...msg,
|
|
185
|
-
parentId,
|
|
186
|
-
sourceId: message.id,
|
|
187
|
-
});
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const composerClient = useClientResource(
|
|
191
|
-
ComposerClientResource({
|
|
192
|
-
type: "edit",
|
|
193
|
-
canCancel: true,
|
|
194
|
-
onBeginEdit: handleBeginEdit,
|
|
195
|
-
onSend: handleSendEdit,
|
|
196
|
-
message,
|
|
197
|
-
queue,
|
|
198
|
-
attachmentAdapter,
|
|
199
|
-
}),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const branchIds = branches?.getBranches(message.id) ?? EMPTY_BRANCH_IDS;
|
|
203
|
-
const branchIndex = branchIds.indexOf(message.id);
|
|
204
|
-
const branchNumber = branchIndex === -1 ? 1 : branchIndex + 1;
|
|
205
|
-
const branchCount = branchIndex === -1 ? 1 : branchIds.length;
|
|
206
|
-
|
|
207
|
-
const state = useMemo(() => {
|
|
208
|
-
const messageWithFeedback: ExternalThreadMessage =
|
|
209
|
-
submittedFeedback && message.role === "assistant"
|
|
210
|
-
? {
|
|
211
|
-
...message,
|
|
212
|
-
metadata: {
|
|
213
|
-
...message.metadata,
|
|
214
|
-
submittedFeedback: { type: submittedFeedback },
|
|
215
|
-
},
|
|
216
|
-
}
|
|
217
|
-
: message;
|
|
218
|
-
return {
|
|
219
|
-
...messageWithFeedback,
|
|
220
|
-
attachments: message.attachments ?? [],
|
|
221
|
-
parentId,
|
|
222
|
-
isLast: false, // Will be set by thread
|
|
223
|
-
branchNumber,
|
|
224
|
-
branchCount,
|
|
225
|
-
speech,
|
|
226
|
-
parts: partClients.state,
|
|
227
|
-
isCopied,
|
|
228
|
-
isHovering,
|
|
229
|
-
index,
|
|
230
|
-
composer: composerClient.state,
|
|
231
|
-
};
|
|
232
|
-
}, [
|
|
233
|
-
message,
|
|
234
|
-
parentId,
|
|
235
|
-
isCopied,
|
|
236
|
-
isHovering,
|
|
237
|
-
index,
|
|
238
|
-
composerClient.state,
|
|
239
|
-
partClients.state,
|
|
240
|
-
branchNumber,
|
|
241
|
-
branchCount,
|
|
242
|
-
submittedFeedback,
|
|
243
|
-
speech,
|
|
244
|
-
]);
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
getState: () => state,
|
|
248
|
-
composer: () => composerClient.methods,
|
|
249
|
-
delete: () => {},
|
|
250
|
-
reload: () => {
|
|
251
|
-
onReload?.();
|
|
252
|
-
},
|
|
253
|
-
speak: onSpeak,
|
|
254
|
-
stopSpeaking: onStopSpeaking,
|
|
255
|
-
submitFeedback: onSubmitFeedback,
|
|
256
|
-
switchToBranch: ({ position, branchId }) => {
|
|
257
|
-
if (!branches) return;
|
|
258
|
-
const target =
|
|
259
|
-
branchId ??
|
|
260
|
-
(branchIndex === -1
|
|
261
|
-
? undefined
|
|
262
|
-
: position === "previous"
|
|
263
|
-
? branchIds[branchIndex - 1]
|
|
264
|
-
: position === "next"
|
|
265
|
-
? branchIds[branchIndex + 1]
|
|
266
|
-
: undefined);
|
|
267
|
-
if (target !== undefined && target !== message.id)
|
|
268
|
-
branches.switchToBranch(target);
|
|
269
|
-
},
|
|
270
|
-
getCopyText: () => getThreadMessageText(message),
|
|
271
|
-
part: (selector) => {
|
|
272
|
-
if ("index" in selector) {
|
|
273
|
-
return partClients.get(selector);
|
|
274
|
-
}
|
|
275
|
-
const partIndex = state.parts.findIndex(
|
|
276
|
-
(p) => p.type === "tool-call" && p.toolCallId === selector.toolCallId,
|
|
277
|
-
);
|
|
278
|
-
return partClients.get({ index: partIndex });
|
|
279
|
-
},
|
|
280
|
-
attachment: (selector) => {
|
|
281
|
-
if ("id" in selector) {
|
|
282
|
-
return attachmentClients.get({ key: selector.id });
|
|
283
|
-
}
|
|
284
|
-
return attachmentClients.get(selector);
|
|
285
|
-
},
|
|
286
|
-
setIsCopied,
|
|
287
|
-
setIsHovering,
|
|
288
|
-
};
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const MessageClient = resource(useMessageClient);
|
|
292
|
-
|
|
293
|
-
type PartResourceProps = {
|
|
294
|
-
part: ThreadAssistantMessagePart | ThreadUserMessagePart;
|
|
295
|
-
status: ToolCallMessagePartStatus;
|
|
296
|
-
messageId: string;
|
|
297
|
-
onRespondToToolApproval?:
|
|
298
|
-
| ((options: RespondToToolApprovalOptions) => void)
|
|
299
|
-
| undefined;
|
|
300
|
-
onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
|
|
301
|
-
onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
// Part Client - minimal implementation
|
|
305
|
-
const usePartResource = ({
|
|
306
|
-
part,
|
|
307
|
-
status,
|
|
308
|
-
messageId,
|
|
309
|
-
onRespondToToolApproval,
|
|
310
|
-
onAddToolResult,
|
|
311
|
-
onResumeToolCall,
|
|
312
|
-
}: PartResourceProps): ClientOutput<"part"> => {
|
|
313
|
-
const state = useMemo(
|
|
314
|
-
() => ({ ...part, status: status as MessagePartStatus }),
|
|
315
|
-
[part, status],
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
getState: () => state,
|
|
320
|
-
addToolResult: (result) => {
|
|
321
|
-
if (!onAddToolResult)
|
|
322
|
-
throw new Error(
|
|
323
|
-
"Runtime does not support tool results (onAddToolResult is not set).",
|
|
324
|
-
);
|
|
325
|
-
if (part.type !== "tool-call")
|
|
326
|
-
throw new Error("Tried to add tool result on non-tool message part");
|
|
327
|
-
|
|
328
|
-
const response = ToolResponse.toResponse(result);
|
|
329
|
-
onAddToolResult({
|
|
330
|
-
messageId,
|
|
331
|
-
toolName: part.toolName,
|
|
332
|
-
toolCallId: part.toolCallId,
|
|
333
|
-
result: response.result as ReadonlyJSONValue,
|
|
334
|
-
isError: response.isError,
|
|
335
|
-
...(response.artifact !== undefined && { artifact: response.artifact }),
|
|
336
|
-
...(response.modelContent !== undefined && {
|
|
337
|
-
modelContent: response.modelContent,
|
|
338
|
-
}),
|
|
339
|
-
});
|
|
340
|
-
},
|
|
341
|
-
resumeToolCall: (payload) => {
|
|
342
|
-
if (!onResumeToolCall)
|
|
343
|
-
throw new Error(
|
|
344
|
-
"Runtime does not support resuming tool calls (onResumeToolCall is not set).",
|
|
345
|
-
);
|
|
346
|
-
if (part.type !== "tool-call")
|
|
347
|
-
throw new Error("Tried to resume tool call on non-tool message part");
|
|
348
|
-
|
|
349
|
-
onResumeToolCall({ toolCallId: part.toolCallId, payload });
|
|
350
|
-
},
|
|
351
|
-
respondToToolApproval: (response) => {
|
|
352
|
-
if (!onRespondToToolApproval)
|
|
353
|
-
throw new Error("Runtime does not support tool approvals.");
|
|
354
|
-
|
|
355
|
-
if (part.type !== "tool-call")
|
|
356
|
-
throw new Error(
|
|
357
|
-
"Tried to respond to tool approval on non-tool message part",
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
!part.approval ||
|
|
362
|
-
part.approval.approved !== undefined ||
|
|
363
|
-
part.approval.resolution !== undefined
|
|
364
|
-
)
|
|
365
|
-
throw new Error("Tool call has no pending approval");
|
|
366
|
-
|
|
367
|
-
onRespondToToolApproval(
|
|
368
|
-
resolveToolApprovalResponse(part.approval, response),
|
|
369
|
-
);
|
|
370
|
-
},
|
|
371
|
-
};
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const PartResource = resource(usePartResource);
|
|
375
|
-
|
|
376
|
-
type AttachmentResourceProps = {
|
|
377
|
-
attachment: Attachment;
|
|
378
|
-
onRemove?: () => void | Promise<void>;
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// Attachment Client - minimal implementation
|
|
382
|
-
const useAttachmentResource = ({
|
|
383
|
-
attachment,
|
|
384
|
-
onRemove,
|
|
385
|
-
}: AttachmentResourceProps): ClientOutput<"attachment"> => {
|
|
386
|
-
return {
|
|
387
|
-
getState: () => attachment,
|
|
388
|
-
remove: async () => {
|
|
389
|
-
await onRemove?.();
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const AttachmentResource = resource(useAttachmentResource);
|
|
395
|
-
|
|
396
|
-
type ComposerClientResourceProps = {
|
|
397
|
-
type: "thread" | "edit";
|
|
398
|
-
canCancel: boolean;
|
|
399
|
-
isSendDisabled?: boolean;
|
|
400
|
-
onCancel?: () => void;
|
|
401
|
-
onBeginEdit?: () => void;
|
|
402
|
-
onSend?: (message: AppendMessage) => void;
|
|
403
|
-
message?: ExternalThreadMessage;
|
|
404
|
-
queue?: ExternalThreadQueueAdapter | undefined;
|
|
405
|
-
attachmentAdapter?: AttachmentAdapter | undefined;
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const useQueueItemClient = ({
|
|
409
|
-
item,
|
|
410
|
-
onMove,
|
|
411
|
-
onRemove,
|
|
412
|
-
}: {
|
|
413
|
-
item: QueueItemState;
|
|
414
|
-
onMove: (placement: QueuePlacement) => void;
|
|
415
|
-
onRemove: () => void;
|
|
416
|
-
}): ClientOutput<"queueItem"> => {
|
|
417
|
-
return {
|
|
418
|
-
getState: () => item,
|
|
419
|
-
steer: () => onMove({ lane: "steer", insertAfter: null }),
|
|
420
|
-
move: onMove,
|
|
421
|
-
remove: onRemove,
|
|
422
|
-
};
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const QueueItemClient = resource(useQueueItemClient);
|
|
426
|
-
|
|
427
|
-
const drainAdapterAdd = async (
|
|
428
|
-
result: ReturnType<AttachmentAdapter["add"]>,
|
|
429
|
-
upsert: (attachment: Attachment) => void,
|
|
430
|
-
) => {
|
|
431
|
-
if (Symbol.asyncIterator in result) {
|
|
432
|
-
for await (const attachment of result) {
|
|
433
|
-
upsert(attachment);
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
upsert(await result);
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// State whose setter tracks the latest value in a ref, so imperative
|
|
441
|
-
// call sequences (setText immediately followed by send) observe the write
|
|
442
|
-
// before React re-renders — legacy composer parity.
|
|
443
|
-
const useLiveState = <T>(initial: T) => {
|
|
444
|
-
const [state, setState] = useState(initial);
|
|
445
|
-
const ref = useRef(state);
|
|
446
|
-
const set = useCallback((next: T | ((prev: T) => T)) => {
|
|
447
|
-
ref.current =
|
|
448
|
-
typeof next === "function" ? (next as (prev: T) => T)(ref.current) : next;
|
|
449
|
-
setState(ref.current);
|
|
450
|
-
}, []);
|
|
451
|
-
return [state, set, ref] as const;
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
// Composer Client - minimal implementation
|
|
455
|
-
const useComposerClientResource = ({
|
|
456
|
-
type,
|
|
457
|
-
canCancel,
|
|
458
|
-
isSendDisabled = false,
|
|
459
|
-
onCancel,
|
|
460
|
-
onBeginEdit,
|
|
461
|
-
onSend,
|
|
462
|
-
message,
|
|
463
|
-
queue,
|
|
464
|
-
attachmentAdapter,
|
|
465
|
-
}: ComposerClientResourceProps): ClientOutput<"composer"> => {
|
|
466
|
-
const [isEditing, setIsEditing, isEditingRef] = useLiveState(
|
|
467
|
-
type === "thread",
|
|
468
|
-
);
|
|
469
|
-
const [text, setText, textRef] = useLiveState("");
|
|
470
|
-
const [role, setRole, roleRef] = useLiveState<
|
|
471
|
-
"user" | "assistant" | "system"
|
|
472
|
-
>("user");
|
|
473
|
-
const [runConfig, setRunConfig, runConfigRef] = useLiveState<
|
|
474
|
-
Record<string, unknown>
|
|
475
|
-
>({});
|
|
476
|
-
const [attachments, setAttachments, attachmentsRef] = useLiveState<
|
|
477
|
-
readonly Attachment[]
|
|
478
|
-
>([]);
|
|
479
|
-
const [quote, setQuote, quoteRef] = useLiveState<
|
|
480
|
-
{ readonly text: string; readonly messageId: string } | undefined
|
|
481
|
-
>(undefined);
|
|
482
|
-
|
|
483
|
-
const updateFromMessage = () => {
|
|
484
|
-
if (!message) return;
|
|
485
|
-
const messageText = message.content
|
|
486
|
-
.filter((part) => part.type === "text")
|
|
487
|
-
.map((part) => part.text)
|
|
488
|
-
.join("\n\n");
|
|
489
|
-
setText(messageText);
|
|
490
|
-
setRole(message.role);
|
|
491
|
-
setAttachments(message.attachments ?? []);
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
const attachmentClients = useClientLookup(
|
|
495
|
-
attachments.map((attachment) =>
|
|
496
|
-
withKey(
|
|
497
|
-
attachment.id,
|
|
498
|
-
AttachmentResource({
|
|
499
|
-
attachment,
|
|
500
|
-
onRemove: async () => {
|
|
501
|
-
await attachmentAdapter?.remove(attachment);
|
|
502
|
-
setAttachments((prev) =>
|
|
503
|
-
prev.filter((a) => a.id !== attachment.id),
|
|
504
|
-
);
|
|
505
|
-
},
|
|
506
|
-
}),
|
|
507
|
-
),
|
|
508
|
-
),
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
const removePendingAttachments = async (removed: readonly Attachment[]) => {
|
|
512
|
-
if (!attachmentAdapter) return;
|
|
513
|
-
await Promise.all(
|
|
514
|
-
removed
|
|
515
|
-
.filter((a) => a.status.type !== "complete")
|
|
516
|
-
.map((a) => attachmentAdapter.remove(a)),
|
|
517
|
-
);
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
const upsertAttachment = (attachment: Attachment) => {
|
|
521
|
-
setAttachments((prev) => {
|
|
522
|
-
const idx = prev.findIndex((a) => a.id === attachment.id);
|
|
523
|
-
if (idx === -1) return [...prev, attachment];
|
|
524
|
-
const next = [...prev];
|
|
525
|
-
next[idx] = attachment;
|
|
526
|
-
return next;
|
|
527
|
-
});
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
const steerItems = queue?.steerItems ?? EMPTY_QUEUE_ITEMS;
|
|
531
|
-
const laneItems = queue?.items ?? EMPTY_QUEUE_ITEMS;
|
|
532
|
-
const queueItems = useMemo(
|
|
533
|
-
() =>
|
|
534
|
-
steerItems.length === 0
|
|
535
|
-
? laneItems
|
|
536
|
-
: laneItems.length === 0
|
|
537
|
-
? steerItems
|
|
538
|
-
: [...steerItems, ...laneItems],
|
|
539
|
-
[steerItems, laneItems],
|
|
540
|
-
);
|
|
541
|
-
const queueItemClients = useClientLookup(
|
|
542
|
-
queueItems.map((item) =>
|
|
543
|
-
withKey(
|
|
544
|
-
item.id,
|
|
545
|
-
QueueItemClient({
|
|
546
|
-
item,
|
|
547
|
-
onMove: (placement) => queue?.move(item.id, placement),
|
|
548
|
-
onRemove: () => queue?.remove(item.id),
|
|
549
|
-
}),
|
|
550
|
-
),
|
|
551
|
-
),
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
const state = useMemo(() => {
|
|
555
|
-
const isEmpty = !text.trim() && !attachments.length;
|
|
556
|
-
return {
|
|
557
|
-
text,
|
|
558
|
-
role,
|
|
559
|
-
attachments: attachmentClients.state,
|
|
560
|
-
runConfig,
|
|
561
|
-
isEditing,
|
|
562
|
-
canCancel,
|
|
563
|
-
canSend: isEditing && !isEmpty && !isSendDisabled,
|
|
564
|
-
attachmentAccept: attachmentAdapter?.accept ?? "*",
|
|
565
|
-
isEmpty,
|
|
566
|
-
type,
|
|
567
|
-
dictation: undefined,
|
|
568
|
-
quote,
|
|
569
|
-
queue: queueItems,
|
|
570
|
-
};
|
|
571
|
-
}, [
|
|
572
|
-
text,
|
|
573
|
-
role,
|
|
574
|
-
attachmentClients.state,
|
|
575
|
-
runConfig,
|
|
576
|
-
isEditing,
|
|
577
|
-
canCancel,
|
|
578
|
-
isSendDisabled,
|
|
579
|
-
type,
|
|
580
|
-
attachments.length,
|
|
581
|
-
quote,
|
|
582
|
-
queueItems,
|
|
583
|
-
attachmentAdapter?.accept,
|
|
584
|
-
]);
|
|
585
|
-
|
|
586
|
-
return {
|
|
587
|
-
getState: () => state,
|
|
588
|
-
setText,
|
|
589
|
-
setRole,
|
|
590
|
-
setRunConfig,
|
|
591
|
-
addAttachment: async (fileOrAttachment: File | CreateAttachment) => {
|
|
592
|
-
if (attachmentAdapter) {
|
|
593
|
-
const file = isCreateAttachment(fileOrAttachment)
|
|
594
|
-
? {
|
|
595
|
-
name: fileOrAttachment.name,
|
|
596
|
-
type: fileOrAttachment.contentType ?? "",
|
|
597
|
-
}
|
|
598
|
-
: { name: fileOrAttachment.name, type: fileOrAttachment.type };
|
|
599
|
-
if (!fileMatchesAccept(file, attachmentAdapter.accept))
|
|
600
|
-
throw new Error(
|
|
601
|
-
`File type ${file.type || "unknown"} is not accepted. Accepted types: ${attachmentAdapter.accept}`,
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
if (!isCreateAttachment(fileOrAttachment) && attachmentAdapter) {
|
|
605
|
-
await drainAdapterAdd(
|
|
606
|
-
attachmentAdapter.add({ file: fileOrAttachment }),
|
|
607
|
-
upsertAttachment,
|
|
608
|
-
);
|
|
609
|
-
} else if (!isCreateAttachment(fileOrAttachment)) {
|
|
610
|
-
const newAttachment: Attachment = {
|
|
611
|
-
id: Math.random().toString(36).substring(7),
|
|
612
|
-
type: "file",
|
|
613
|
-
name: fileOrAttachment.name,
|
|
614
|
-
contentType: fileOrAttachment.type,
|
|
615
|
-
file: fileOrAttachment,
|
|
616
|
-
status: { type: "complete" },
|
|
617
|
-
content: [],
|
|
618
|
-
};
|
|
619
|
-
setAttachments((prev) => [...prev, newAttachment]);
|
|
620
|
-
} else {
|
|
621
|
-
const newAttachment: Attachment = {
|
|
622
|
-
id: fileOrAttachment.id ?? Math.random().toString(36).substring(7),
|
|
623
|
-
type: fileOrAttachment.type ?? "document",
|
|
624
|
-
name: fileOrAttachment.name,
|
|
625
|
-
contentType: fileOrAttachment.contentType,
|
|
626
|
-
content: fileOrAttachment.content,
|
|
627
|
-
status: { type: "complete" },
|
|
628
|
-
};
|
|
629
|
-
setAttachments((prev) => [...prev, newAttachment]);
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
clearAttachments: async () => {
|
|
633
|
-
const removed = attachmentsRef.current;
|
|
634
|
-
setAttachments([]);
|
|
635
|
-
await removePendingAttachments(removed);
|
|
636
|
-
},
|
|
637
|
-
attachment: (selector) => {
|
|
638
|
-
if ("id" in selector) {
|
|
639
|
-
return attachmentClients.get({ key: selector.id });
|
|
640
|
-
}
|
|
641
|
-
return attachmentClients.get(selector);
|
|
642
|
-
},
|
|
643
|
-
reset: async () => {
|
|
644
|
-
const removed = attachmentsRef.current;
|
|
645
|
-
setText("");
|
|
646
|
-
setRole("user");
|
|
647
|
-
setRunConfig({});
|
|
648
|
-
setAttachments([]);
|
|
649
|
-
setQuote(undefined);
|
|
650
|
-
await removePendingAttachments(removed);
|
|
651
|
-
},
|
|
652
|
-
send: (opts?: ComposerSendOptions) => {
|
|
653
|
-
const currentQuote = quoteRef.current;
|
|
654
|
-
const currentText = textRef.current;
|
|
655
|
-
const currentAttachments = attachmentsRef.current;
|
|
656
|
-
const isEmpty = !currentText.trim() && !currentAttachments.length;
|
|
657
|
-
if (!isEditingRef.current) throw new Error("Composer is not available");
|
|
658
|
-
if (isEmpty || isSendDisabled) return;
|
|
659
|
-
|
|
660
|
-
setText("");
|
|
661
|
-
setAttachments([]);
|
|
662
|
-
setQuote(undefined);
|
|
663
|
-
|
|
664
|
-
const dispatch = (sendAttachments: readonly Attachment[]) => {
|
|
665
|
-
const composedMessage: AppendMessage = {
|
|
666
|
-
role: roleRef.current,
|
|
667
|
-
content: currentText
|
|
668
|
-
? [{ type: "text" as const, text: currentText }]
|
|
669
|
-
: [],
|
|
670
|
-
attachments: sendAttachments as any,
|
|
671
|
-
createdAt: new Date(),
|
|
672
|
-
parentId: null,
|
|
673
|
-
sourceId: null,
|
|
674
|
-
runConfig: runConfigRef.current,
|
|
675
|
-
startRun: opts?.startRun,
|
|
676
|
-
metadata: {
|
|
677
|
-
custom: { ...(currentQuote ? { quote: currentQuote } : {}) },
|
|
678
|
-
},
|
|
679
|
-
};
|
|
680
|
-
// edit sends carry a sourceId contract; only thread sends queue
|
|
681
|
-
if (queue && type === "thread") {
|
|
682
|
-
if (opts?.steer ?? canCancel) queue.steer(composedMessage);
|
|
683
|
-
else queue.enqueue(composedMessage);
|
|
684
|
-
} else {
|
|
685
|
-
onSend?.(composedMessage);
|
|
686
|
-
}
|
|
687
|
-
if (type === "edit") setIsEditing(false);
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
if (attachmentAdapter && currentAttachments.length > 0) {
|
|
691
|
-
void Promise.all(
|
|
692
|
-
currentAttachments.map((attachment) =>
|
|
693
|
-
attachment.status.type === "complete"
|
|
694
|
-
? attachment
|
|
695
|
-
: attachmentAdapter.send(attachment as PendingAttachment),
|
|
696
|
-
),
|
|
697
|
-
).then(dispatch, (error) => {
|
|
698
|
-
// Upload failed: merge the failed send back into the draft.
|
|
699
|
-
setText((prev) =>
|
|
700
|
-
currentText && prev
|
|
701
|
-
? currentText + "\n" + prev
|
|
702
|
-
: currentText || prev,
|
|
703
|
-
);
|
|
704
|
-
setQuote((prev) => prev ?? currentQuote);
|
|
705
|
-
setAttachments((prev) => [...currentAttachments, ...prev]);
|
|
706
|
-
console.error("Failed to send attachments", error);
|
|
707
|
-
});
|
|
708
|
-
} else {
|
|
709
|
-
dispatch(currentAttachments);
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
cancel: () => {
|
|
713
|
-
onCancel?.();
|
|
714
|
-
if (type === "edit") setIsEditing(false);
|
|
715
|
-
},
|
|
716
|
-
beginEdit: () => {
|
|
717
|
-
onBeginEdit?.();
|
|
718
|
-
if (type === "thread") return;
|
|
719
|
-
if (isEditingRef.current) throw new Error("Edit already in progress");
|
|
720
|
-
setIsEditing(true);
|
|
721
|
-
updateFromMessage();
|
|
722
|
-
},
|
|
723
|
-
startDictation: () => {},
|
|
724
|
-
stopDictation: () => {},
|
|
725
|
-
setQuote,
|
|
726
|
-
queueItem: (selector: { index: number } | { id: string }) => {
|
|
727
|
-
if ("id" in selector) {
|
|
728
|
-
return queueItemClients.get({ key: selector.id });
|
|
729
|
-
}
|
|
730
|
-
return queueItemClients.get(selector);
|
|
731
|
-
},
|
|
732
|
-
};
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
const ComposerClientResource = resource(useComposerClientResource);
|
|
736
|
-
|
|
737
|
-
const createSpeechController = (
|
|
738
|
-
notify: (speech: SpeechState | undefined) => void,
|
|
739
|
-
) => {
|
|
740
|
-
let session: { messageId: string; cancel: () => void } | undefined;
|
|
741
|
-
|
|
742
|
-
const clear = () => {
|
|
743
|
-
if (!session) return;
|
|
744
|
-
session.cancel();
|
|
745
|
-
session = undefined;
|
|
746
|
-
notify(undefined);
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
return {
|
|
750
|
-
speak: (
|
|
751
|
-
adapter: SpeechSynthesisAdapter,
|
|
752
|
-
message: ExternalThreadMessage,
|
|
753
|
-
) => {
|
|
754
|
-
clear();
|
|
755
|
-
|
|
756
|
-
const utterance = adapter.speak(getThreadMessageText(message));
|
|
757
|
-
let unsub: (() => void) | undefined;
|
|
758
|
-
unsub = utterance.subscribe(() => {
|
|
759
|
-
if (utterance.status.type === "ended") {
|
|
760
|
-
unsub?.();
|
|
761
|
-
session = undefined;
|
|
762
|
-
notify(undefined);
|
|
763
|
-
} else {
|
|
764
|
-
notify({ messageId: message.id, status: utterance.status });
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
if (utterance.status.type === "ended") {
|
|
769
|
-
unsub();
|
|
770
|
-
notify(undefined);
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
session = {
|
|
775
|
-
messageId: message.id,
|
|
776
|
-
cancel: () => {
|
|
777
|
-
unsub!();
|
|
778
|
-
utterance.cancel();
|
|
779
|
-
},
|
|
780
|
-
};
|
|
781
|
-
notify({ messageId: message.id, status: utterance.status });
|
|
782
|
-
},
|
|
783
|
-
stop: () => {
|
|
784
|
-
if (!session) throw new Error("No message is being spoken");
|
|
785
|
-
clear();
|
|
786
|
-
},
|
|
787
|
-
stopMessage: (messageId: string) => {
|
|
788
|
-
if (session?.messageId !== messageId)
|
|
789
|
-
throw new Error("Message is not being spoken");
|
|
790
|
-
clear();
|
|
791
|
-
},
|
|
792
|
-
dispose: clear,
|
|
793
|
-
};
|
|
794
|
-
};
|
|
795
|
-
|
|
796
|
-
const dedupeMessagesById = (messages: readonly ExternalThreadMessage[]) => {
|
|
797
|
-
const seenIds = new Set<string>();
|
|
798
|
-
const deduped: ExternalThreadMessage[] = [];
|
|
799
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
800
|
-
const message = messages[i]!;
|
|
801
|
-
if (seenIds.has(message.id)) {
|
|
802
|
-
console.warn(
|
|
803
|
-
`ExternalThread: duplicate message id "${message.id}" in the provided messages array; keeping the last occurrence.`,
|
|
804
|
-
);
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
seenIds.add(message.id);
|
|
808
|
-
deduped.push(message);
|
|
809
|
-
}
|
|
810
|
-
return deduped.length === messages.length ? messages : deduped.reverse();
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
// External Thread Client
|
|
814
|
-
const useExternalThread = ({
|
|
815
|
-
messages: messagesProp,
|
|
816
|
-
isRunning = false,
|
|
817
|
-
isLoading = false,
|
|
818
|
-
state: threadState,
|
|
819
|
-
extras,
|
|
820
|
-
isSendDisabled = false,
|
|
821
|
-
onNew,
|
|
822
|
-
onEdit,
|
|
823
|
-
onReload,
|
|
824
|
-
onStartRun,
|
|
825
|
-
onCancel,
|
|
826
|
-
onResume,
|
|
827
|
-
onAddToolResult,
|
|
828
|
-
onResumeToolCall,
|
|
829
|
-
onLoadExternalState,
|
|
830
|
-
attachmentAdapter,
|
|
831
|
-
feedbackAdapter,
|
|
832
|
-
speechAdapter,
|
|
833
|
-
queue,
|
|
834
|
-
branches,
|
|
835
|
-
onRespondToToolApproval,
|
|
836
|
-
}: ExternalThreadProps): ClientOutput<"thread"> => {
|
|
837
|
-
const messages = useMemo(
|
|
838
|
-
() => dedupeMessagesById(messagesProp),
|
|
839
|
-
[messagesProp],
|
|
840
|
-
);
|
|
841
|
-
|
|
842
|
-
// Local entries are optimistic: they apply only while the message's
|
|
843
|
-
// external submittedFeedback still equals the value seen at click time.
|
|
844
|
-
const [submittedFeedback, setSubmittedFeedback] = useState<
|
|
845
|
-
Record<
|
|
846
|
-
string,
|
|
847
|
-
{
|
|
848
|
-
type: "positive" | "negative";
|
|
849
|
-
external: "positive" | "negative" | undefined;
|
|
850
|
-
}
|
|
851
|
-
>
|
|
852
|
-
>({});
|
|
853
|
-
|
|
854
|
-
const feedbackFor = (msg: ExternalThreadMessage) => {
|
|
855
|
-
const entry = submittedFeedback[msg.id];
|
|
856
|
-
return entry && msg.metadata.submittedFeedback?.type === entry.external
|
|
857
|
-
? entry.type
|
|
858
|
-
: undefined;
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
useEffect(() => {
|
|
862
|
-
setSubmittedFeedback((prev) => {
|
|
863
|
-
const live = Object.entries(prev).filter(([id, entry]) => {
|
|
864
|
-
const msg = messages.find((m) => m.id === id);
|
|
865
|
-
return !!msg && msg.metadata.submittedFeedback?.type === entry.external;
|
|
866
|
-
});
|
|
867
|
-
return live.length === Object.keys(prev).length
|
|
868
|
-
? prev
|
|
869
|
-
: Object.fromEntries(live);
|
|
870
|
-
});
|
|
871
|
-
}, [messages]);
|
|
872
|
-
|
|
873
|
-
const handleSubmitFeedback = (
|
|
874
|
-
message: ExternalThreadMessage,
|
|
875
|
-
{ type }: { type: "positive" | "negative" },
|
|
876
|
-
) => {
|
|
877
|
-
if (!feedbackAdapter) throw new Error("Feedback adapter not configured");
|
|
878
|
-
feedbackAdapter.submit({ message, type });
|
|
879
|
-
|
|
880
|
-
if (message.role === "assistant") {
|
|
881
|
-
setSubmittedFeedback((prev) => ({
|
|
882
|
-
...prev,
|
|
883
|
-
[message.id]: {
|
|
884
|
-
type,
|
|
885
|
-
external: message.metadata.submittedFeedback?.type,
|
|
886
|
-
},
|
|
887
|
-
}));
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
const [speechState, setSpeech] = useState<SpeechState | undefined>(undefined);
|
|
892
|
-
const [speechController] = useState(() => createSpeechController(setSpeech));
|
|
893
|
-
|
|
894
|
-
const hasSpeechAdapter = !!speechAdapter;
|
|
895
|
-
const speech = hasSpeechAdapter ? speechState : undefined;
|
|
896
|
-
useEffect(() => {
|
|
897
|
-
if (!hasSpeechAdapter) speechController.dispose();
|
|
898
|
-
}, [hasSpeechAdapter, speechController]);
|
|
899
|
-
|
|
900
|
-
useEffect(() => () => speechController.dispose(), [speechController]);
|
|
901
|
-
|
|
902
|
-
const handleSpeak = (message: ExternalThreadMessage) => {
|
|
903
|
-
if (!speechAdapter) throw new Error("Speech adapter not configured");
|
|
904
|
-
speechController.speak(speechAdapter, message);
|
|
905
|
-
};
|
|
906
|
-
|
|
907
|
-
const handleReload = (messageId: string) => {
|
|
908
|
-
const messageIndex = messages.findIndex((m) => m.id === messageId);
|
|
909
|
-
if (messageIndex === -1) return;
|
|
910
|
-
|
|
911
|
-
const parentId = messageIndex > 0 ? messages[messageIndex - 1]!.id : null;
|
|
912
|
-
onReload?.(parentId);
|
|
913
|
-
};
|
|
914
|
-
|
|
915
|
-
const messageClients = useClientLookup(
|
|
916
|
-
messages.map((msg, index) => {
|
|
917
|
-
const props: MessageClientProps = {
|
|
918
|
-
message: msg,
|
|
919
|
-
index,
|
|
920
|
-
parentId: index > 0 ? messages[index - 1]!.id : null,
|
|
921
|
-
onReload: () => handleReload(msg.id),
|
|
922
|
-
queue,
|
|
923
|
-
branches,
|
|
924
|
-
onRespondToToolApproval,
|
|
925
|
-
onAddToolResult,
|
|
926
|
-
onResumeToolCall,
|
|
927
|
-
attachmentAdapter,
|
|
928
|
-
submittedFeedback: feedbackFor(msg),
|
|
929
|
-
onSubmitFeedback: (feedback) => handleSubmitFeedback(msg, feedback),
|
|
930
|
-
speech: speech?.messageId === msg.id ? speech : undefined,
|
|
931
|
-
onSpeak: () => handleSpeak(msg),
|
|
932
|
-
onStopSpeaking: () => speechController.stopMessage(msg.id),
|
|
933
|
-
};
|
|
934
|
-
if (onEdit) props.onEdit = onEdit;
|
|
935
|
-
return withKey(msg.id, MessageClient(props));
|
|
936
|
-
}),
|
|
937
|
-
);
|
|
938
|
-
|
|
939
|
-
const handleCancelRun = () => {
|
|
940
|
-
onCancel?.();
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
const handleSendNew = (message: AppendMessage) => {
|
|
944
|
-
// The composer does not know the thread; stamp the current head as the
|
|
945
|
-
// parent (legacy composer parity).
|
|
946
|
-
onNew?.({ ...message, parentId: messages.at(-1)?.id ?? null });
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
const headId = messages.at(-1)?.id ?? null;
|
|
950
|
-
const composerQueue = useMemo(
|
|
951
|
-
(): ExternalThreadQueueAdapter | undefined =>
|
|
952
|
-
queue && {
|
|
953
|
-
...queue,
|
|
954
|
-
enqueue: (message) =>
|
|
955
|
-
queue.enqueue({ ...message, parentId: message.parentId ?? headId }),
|
|
956
|
-
steer: (message) =>
|
|
957
|
-
queue.steer({ ...message, parentId: message.parentId ?? headId }),
|
|
958
|
-
},
|
|
959
|
-
[queue, headId],
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
const composerClient = useClientResource(
|
|
963
|
-
ComposerClientResource({
|
|
964
|
-
type: "thread",
|
|
965
|
-
canCancel: isRunning,
|
|
966
|
-
isSendDisabled,
|
|
967
|
-
onCancel: handleCancelRun,
|
|
968
|
-
onSend: handleSendNew,
|
|
969
|
-
queue: composerQueue,
|
|
970
|
-
attachmentAdapter,
|
|
971
|
-
}),
|
|
972
|
-
);
|
|
973
|
-
|
|
974
|
-
const hasQueue = !!queue;
|
|
975
|
-
const hasBranches = !!branches;
|
|
976
|
-
const hasEdit = !!onEdit;
|
|
977
|
-
const hasReload = !!onReload;
|
|
978
|
-
const hasAttachments = !!attachmentAdapter;
|
|
979
|
-
const hasFeedback = !!feedbackAdapter;
|
|
980
|
-
const hasSpeech = !!speechAdapter;
|
|
981
|
-
const state = useMemo(() => {
|
|
982
|
-
const messageStates = messageClients.state.map((s, idx, arr) => ({
|
|
983
|
-
...s,
|
|
984
|
-
isLast: idx === arr.length - 1,
|
|
985
|
-
}));
|
|
986
|
-
|
|
987
|
-
return {
|
|
988
|
-
isEmpty: messages.length === 0,
|
|
989
|
-
isDisabled: false,
|
|
990
|
-
isLoading,
|
|
991
|
-
isRunning,
|
|
992
|
-
capabilities: {
|
|
993
|
-
edit: hasEdit,
|
|
994
|
-
delete: false,
|
|
995
|
-
reload: hasReload,
|
|
996
|
-
refetchThread: false,
|
|
997
|
-
cancel: isRunning,
|
|
998
|
-
speech: hasSpeech,
|
|
999
|
-
attachments: hasAttachments,
|
|
1000
|
-
feedback: hasFeedback,
|
|
1001
|
-
voice: false,
|
|
1002
|
-
switchToBranch: hasBranches,
|
|
1003
|
-
switchBranchDuringRun: false,
|
|
1004
|
-
unstable_copy: false,
|
|
1005
|
-
dictation: false,
|
|
1006
|
-
queue: hasQueue,
|
|
1007
|
-
},
|
|
1008
|
-
messages: messageStates,
|
|
1009
|
-
state: threadState ?? {},
|
|
1010
|
-
suggestions: [],
|
|
1011
|
-
extras,
|
|
1012
|
-
speech,
|
|
1013
|
-
voice: undefined,
|
|
1014
|
-
composer: composerClient.state,
|
|
1015
|
-
};
|
|
1016
|
-
}, [
|
|
1017
|
-
messages,
|
|
1018
|
-
isRunning,
|
|
1019
|
-
isLoading,
|
|
1020
|
-
threadState,
|
|
1021
|
-
extras,
|
|
1022
|
-
hasQueue,
|
|
1023
|
-
hasBranches,
|
|
1024
|
-
hasEdit,
|
|
1025
|
-
hasReload,
|
|
1026
|
-
hasAttachments,
|
|
1027
|
-
hasFeedback,
|
|
1028
|
-
hasSpeech,
|
|
1029
|
-
speech,
|
|
1030
|
-
messageClients.state,
|
|
1031
|
-
composerClient.state,
|
|
1032
|
-
]);
|
|
1033
|
-
|
|
1034
|
-
return {
|
|
1035
|
-
getState: () => state,
|
|
1036
|
-
composer: () => composerClient.methods,
|
|
1037
|
-
append: (message) => {
|
|
1038
|
-
const appendMessage: AppendMessage =
|
|
1039
|
-
typeof message === "string"
|
|
1040
|
-
? {
|
|
1041
|
-
createdAt: new Date(),
|
|
1042
|
-
parentId: messages.at(-1)?.id ?? null,
|
|
1043
|
-
sourceId: null,
|
|
1044
|
-
runConfig: {},
|
|
1045
|
-
role: "user",
|
|
1046
|
-
content: [{ type: "text", text: message }],
|
|
1047
|
-
attachments: [],
|
|
1048
|
-
metadata: { custom: {} },
|
|
1049
|
-
}
|
|
1050
|
-
: {
|
|
1051
|
-
createdAt: message.createdAt ?? new Date(),
|
|
1052
|
-
parentId: message.parentId ?? messages.at(-1)?.id ?? null,
|
|
1053
|
-
sourceId: message.sourceId ?? null,
|
|
1054
|
-
role: message.role ?? "user",
|
|
1055
|
-
content: message.content,
|
|
1056
|
-
attachments: message.attachments ?? [],
|
|
1057
|
-
metadata: message.metadata ?? { custom: {} },
|
|
1058
|
-
runConfig: message.runConfig ?? {},
|
|
1059
|
-
startRun: message.startRun,
|
|
1060
|
-
};
|
|
1061
|
-
if (queue) {
|
|
1062
|
-
queue.enqueue(appendMessage);
|
|
1063
|
-
} else {
|
|
1064
|
-
onNew?.(appendMessage);
|
|
1065
|
-
}
|
|
1066
|
-
},
|
|
1067
|
-
deleteMessage: () => {},
|
|
1068
|
-
startRun: () => {
|
|
1069
|
-
onStartRun?.();
|
|
1070
|
-
},
|
|
1071
|
-
resumeRun: () => {
|
|
1072
|
-
if (!onResume)
|
|
1073
|
-
throw new Error(
|
|
1074
|
-
"Runtime does not support resuming runs (onResume is not set).",
|
|
1075
|
-
);
|
|
1076
|
-
onResume();
|
|
1077
|
-
},
|
|
1078
|
-
cancelRun: handleCancelRun,
|
|
1079
|
-
importExternalState: (state: unknown) => {
|
|
1080
|
-
if (!onLoadExternalState)
|
|
1081
|
-
throw new Error(
|
|
1082
|
-
"Runtime does not support importing external states (onLoadExternalState is not set).",
|
|
1083
|
-
);
|
|
1084
|
-
onLoadExternalState(state);
|
|
1085
|
-
},
|
|
1086
|
-
getModelContext: () => ({ tools: {}, config: {} }),
|
|
1087
|
-
export: () => ({ messages: [] }),
|
|
1088
|
-
import: () => {},
|
|
1089
|
-
reset: () => {},
|
|
1090
|
-
message: (selector) => {
|
|
1091
|
-
if ("id" in selector) {
|
|
1092
|
-
return messageClients.get({ key: selector.id });
|
|
1093
|
-
}
|
|
1094
|
-
return messageClients.get(selector);
|
|
1095
|
-
},
|
|
1096
|
-
stopSpeaking: speechController.stop,
|
|
1097
|
-
connectVoice: () => {},
|
|
1098
|
-
disconnectVoice: () => {},
|
|
1099
|
-
getVoiceVolume: () => 0,
|
|
1100
|
-
subscribeVoiceVolume: () => () => {},
|
|
1101
|
-
muteVoice: () => {},
|
|
1102
|
-
unmuteVoice: () => {},
|
|
1103
|
-
};
|
|
1104
|
-
};
|
|
1105
|
-
|
|
1106
|
-
export const ExternalThread = resource(useExternalThread);
|
|
1107
|
-
|
|
1108
|
-
attachTransformScopes(useExternalThread, (scopes, parent) => {
|
|
1109
|
-
if (!scopes.threads && parent.threads.source === null) {
|
|
1110
|
-
const threadElement = scopes.thread as ClientElement<"thread">;
|
|
1111
|
-
scopes.threads = SingleThreadList({ thread: threadElement });
|
|
1112
|
-
// scopes mount in key order; re-declare thread after the threads source it resolves from
|
|
1113
|
-
delete scopes.thread;
|
|
1114
|
-
scopes.thread = Derived({
|
|
1115
|
-
source: "threads",
|
|
1116
|
-
query: { type: "main" },
|
|
1117
|
-
get: (aui) => aui.threads.thread("main"),
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (!scopes.threadListItem && parent.threadListItem.source === null) {
|
|
1122
|
-
scopes.threadListItem = Derived({
|
|
1123
|
-
source: "threads",
|
|
1124
|
-
query: { type: "main" },
|
|
1125
|
-
get: (aui) => aui.threads.item("main"),
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
scopes.composer ??= Derived({
|
|
1130
|
-
source: "thread",
|
|
1131
|
-
query: {},
|
|
1132
|
-
get: (aui) => aui.thread.composer(),
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
if (!scopes.modelContext && parent.modelContext.source === null) {
|
|
1136
|
-
scopes.modelContext = ModelContext();
|
|
1137
|
-
}
|
|
1138
|
-
if (!scopes.tools && parent.tools.source === null) {
|
|
1139
|
-
scopes.tools = Tools({});
|
|
1140
|
-
}
|
|
1141
|
-
if (!scopes.dataRenderers && parent.dataRenderers.source === null) {
|
|
1142
|
-
scopes.dataRenderers = DataRenderers();
|
|
1143
|
-
}
|
|
1144
|
-
if (!scopes.suggestions && parent.suggestions.source === null) {
|
|
1145
|
-
scopes.suggestions = Suggestions();
|
|
1146
|
-
}
|
|
1147
|
-
});
|