@jskit-ai/assistant-core 0.1.171 → 0.1.173
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/examples/conversation/App.vue +62 -10
- package/examples/conversation/README.md +29 -4
- package/examples/conversation/attachments.js +45 -0
- package/examples/conversation/integrations.json +28 -0
- package/examples/conversation/package.json +6 -4
- package/examples/conversation/server.js +61 -10
- package/fixtures/responsive-assistant/ConversationFixture.vue +54 -1
- package/fixtures/responsive-assistant/ProgressFixture.vue +61 -0
- package/fixtures/responsive-assistant/main.js +3 -1
- package/package.json +21 -7
- package/src/client/conversation/AssistantAttachmentPreview.vue +38 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +563 -0
- package/src/client/conversation/AssistantComposerSupport.vue +235 -0
- package/src/client/conversation/AssistantConversationElement.vue +103 -9
- package/src/client/conversation/AssistantGoalControl.vue +81 -0
- package/src/client/conversation/AssistantMessageAttachments.vue +114 -0
- package/src/client/conversation/AssistantModelControl.vue +334 -0
- package/src/client/conversation/AssistantQuestionInputs.vue +124 -0
- package/src/client/conversation/AssistantTranscript.vue +139 -174
- package/src/client/conversation/index.js +9 -0
- package/src/client/conversation/useAssistantAttachments.js +991 -0
- package/src/client/conversation/useAssistantSuggestions.js +99 -0
- package/src/server/index.js +1 -0
- package/src/server/lib/aiConnectionClient.js +90 -0
- package/src/server/lib/providers/anthropicClient.js +6 -21
- package/src/shared/assistantResource.js +7 -0
- package/src/shared/conversation/attachments.js +13 -0
- package/src/shared/conversation/index.js +1 -0
- package/test/aiConnectionClient.test.js +159 -0
- package/test/assistantScroll.browser.test.js +217 -0
- package/test/suggestions.test.js +114 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
getCurrentScope,
|
|
4
|
+
onScopeDispose,
|
|
5
|
+
reactive,
|
|
6
|
+
ref,
|
|
7
|
+
toValue,
|
|
8
|
+
watch
|
|
9
|
+
} from "vue";
|
|
10
|
+
|
|
11
|
+
function assistantAttachmentSessionId(sessionId) {
|
|
12
|
+
return String(toValue(sessionId) || "").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assistantAttachmentUploadAllowed(canUpload) {
|
|
16
|
+
return toValue(canUpload) !== false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assistantAttachmentFiles(fileList = []) {
|
|
20
|
+
return Array.from(fileList || []).filter((file) => file && file.size >= 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assistantAttachmentFilesFromTransferItems(items = []) {
|
|
24
|
+
return assistantAttachmentFiles(Array.from(items || [])
|
|
25
|
+
.map((item) => {
|
|
26
|
+
if (item?.kind !== "file" || typeof item.getAsFile !== "function") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return item.getAsFile();
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assistantAttachmentFilesFromDropEvent(event) {
|
|
34
|
+
const dataTransfer = event?.dataTransfer;
|
|
35
|
+
const itemFiles = assistantAttachmentFilesFromTransferItems(dataTransfer?.items);
|
|
36
|
+
if (itemFiles.length > 0) {
|
|
37
|
+
return itemFiles;
|
|
38
|
+
}
|
|
39
|
+
return assistantAttachmentFiles(dataTransfer?.files);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assistantAttachmentFilesFromClipboardItems(items = []) {
|
|
43
|
+
return assistantAttachmentFilesFromTransferItems(items);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assistantAttachmentFilesFromPasteEvent(event) {
|
|
47
|
+
const clipboardData = event?.clipboardData;
|
|
48
|
+
const itemFiles = assistantAttachmentFilesFromClipboardItems(clipboardData?.items);
|
|
49
|
+
if (itemFiles.length > 0) {
|
|
50
|
+
return itemFiles;
|
|
51
|
+
}
|
|
52
|
+
return assistantAttachmentFiles(clipboardData?.files);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assistantAttachmentEventHasFiles(event) {
|
|
56
|
+
const dataTransfer = event?.dataTransfer;
|
|
57
|
+
return assistantAttachmentFilesFromDropEvent(event).length > 0 ||
|
|
58
|
+
Array.from(dataTransfer?.items || []).some((item) => item?.kind === "file") ||
|
|
59
|
+
Array.from(dataTransfer?.types || []).some((type) => (
|
|
60
|
+
type === "Files" ||
|
|
61
|
+
type === "application/x-moz-file"
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function assistantAttachmentClipboardText(event) {
|
|
66
|
+
if (typeof event?.clipboardData?.getData !== "function") {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
return String(event.clipboardData.getData("text/plain") || "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assistantAttachmentClipboardLocalFileReference(event) {
|
|
73
|
+
const clipboardData = event?.clipboardData;
|
|
74
|
+
if (!clipboardData || typeof clipboardData.getData !== "function") {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
const types = Array.from(clipboardData.types || []);
|
|
78
|
+
const text = [
|
|
79
|
+
clipboardData.getData("x-special/gnome-copied-files"),
|
|
80
|
+
clipboardData.getData("text/uri-list"),
|
|
81
|
+
clipboardData.getData("text/plain")
|
|
82
|
+
].map((value) => String(value || "").trim()).filter(Boolean).join("\n");
|
|
83
|
+
if (
|
|
84
|
+
types.includes("Files") ||
|
|
85
|
+
types.includes("application/x-moz-file") ||
|
|
86
|
+
types.includes("x-special/gnome-copied-files") ||
|
|
87
|
+
/^file:/imu.test(text)
|
|
88
|
+
) {
|
|
89
|
+
return text || "file://";
|
|
90
|
+
}
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function attachmentUploadError(attachment = {}) {
|
|
95
|
+
return attachment?.error || attachment?.errors?.[0]?.message || "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function attachmentIdentity(attachment = {}) {
|
|
99
|
+
return String(
|
|
100
|
+
attachment?.clientId ||
|
|
101
|
+
attachment?.attachmentId ||
|
|
102
|
+
attachment?.path ||
|
|
103
|
+
attachment?.fileName ||
|
|
104
|
+
""
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function deliverUploadedAttachments(onUploaded, uploaded = []) {
|
|
109
|
+
if (uploaded.length > 0) {
|
|
110
|
+
return await onUploaded(uploaded);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ASSISTANT_ATTACHMENT_MAX_BYTES = 100_000_000;
|
|
116
|
+
const ASSISTANT_ATTACHMENT_MAX_ITEMS = 10;
|
|
117
|
+
const ASSISTANT_ATTACHMENT_UPLOAD_CONCURRENCY = 2;
|
|
118
|
+
const ASSISTANT_ATTACHMENT_UNRESOLVED_PHASES = new Set([
|
|
119
|
+
"preparing",
|
|
120
|
+
"queued",
|
|
121
|
+
"uploading",
|
|
122
|
+
"delivering",
|
|
123
|
+
"failed"
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
let attachmentClientSequence = 0;
|
|
127
|
+
|
|
128
|
+
function nextAttachmentClientId() {
|
|
129
|
+
attachmentClientSequence += 1;
|
|
130
|
+
return `attachment-${Date.now().toString(36)}-${attachmentClientSequence.toString(36)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function attachmentFileName(file = {}) {
|
|
134
|
+
return String(file?.name || "attachment");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function attachmentUploadResult(upload = null) {
|
|
138
|
+
if (upload && typeof upload.then === "function") {
|
|
139
|
+
return {
|
|
140
|
+
cancel: typeof upload.cancel === "function" ? () => upload.cancel() : null,
|
|
141
|
+
promise: upload
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (upload?.promise && typeof upload.promise.then === "function") {
|
|
145
|
+
const cancel = typeof upload.abort === "function"
|
|
146
|
+
? () => upload.abort()
|
|
147
|
+
: typeof upload.cancel === "function"
|
|
148
|
+
? () => upload.cancel()
|
|
149
|
+
: null;
|
|
150
|
+
return {
|
|
151
|
+
cancel,
|
|
152
|
+
promise: upload.promise
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
cancel: null,
|
|
157
|
+
promise: Promise.resolve(upload)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function attachmentProgress(progress = {}, possibleTotal, fallbackTotal = 0) {
|
|
162
|
+
const directNumericProgress = typeof progress === "number" ? progress : null;
|
|
163
|
+
const progressObject = progress && typeof progress === "object" ? progress : {};
|
|
164
|
+
const totalBytes = Math.max(0, Number(
|
|
165
|
+
progressObject.totalBytes ??
|
|
166
|
+
progressObject.total ??
|
|
167
|
+
possibleTotal ??
|
|
168
|
+
fallbackTotal
|
|
169
|
+
) || 0);
|
|
170
|
+
const bytesValue = progressObject.bytesSent ??
|
|
171
|
+
progressObject.loaded ??
|
|
172
|
+
progressObject.uploadedBytes ??
|
|
173
|
+
directNumericProgress;
|
|
174
|
+
const bytesKnown = bytesValue !== null && bytesValue !== undefined && bytesValue !== "";
|
|
175
|
+
let bytesSent = bytesKnown ? Number(bytesValue) || 0 : 0;
|
|
176
|
+
const directPercent = progressObject.percent;
|
|
177
|
+
let percent = directPercent !== null && directPercent !== undefined && directPercent !== ""
|
|
178
|
+
? Number(directPercent)
|
|
179
|
+
: null;
|
|
180
|
+
const progressValue = progressObject.progress;
|
|
181
|
+
if (!Number.isFinite(percent) && progressValue !== null && progressValue !== undefined && progressValue !== "") {
|
|
182
|
+
const numericProgressValue = Number(progressValue);
|
|
183
|
+
if (Number.isFinite(numericProgressValue)) {
|
|
184
|
+
percent = numericProgressValue <= 1 ? numericProgressValue * 100 : numericProgressValue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isFinite(percent)) {
|
|
188
|
+
percent = bytesKnown && totalBytes > 0 ? (bytesSent / totalBytes) * 100 : null;
|
|
189
|
+
}
|
|
190
|
+
if (percent !== null) {
|
|
191
|
+
percent = Math.max(0, Math.min(100, percent));
|
|
192
|
+
}
|
|
193
|
+
if (totalBytes > 0) {
|
|
194
|
+
const inferredBytes = percent === null ? 0 : totalBytes * percent / 100;
|
|
195
|
+
bytesSent = Math.max(0, Math.min(totalBytes, bytesSent || inferredBytes));
|
|
196
|
+
} else {
|
|
197
|
+
bytesSent = Math.max(0, bytesSent);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
bytesSent,
|
|
201
|
+
percent,
|
|
202
|
+
totalBytes
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function attachmentReceipt(row, uploaded = {}) {
|
|
207
|
+
return {
|
|
208
|
+
...uploaded,
|
|
209
|
+
attachmentId: String(uploaded?.attachmentId || ""),
|
|
210
|
+
clientId: row.clientId,
|
|
211
|
+
fileName: String(uploaded?.fileName || row.fileName),
|
|
212
|
+
sessionId: row.sessionId,
|
|
213
|
+
size: Number(uploaded?.size ?? row.size) || 0
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function useAssistantAttachments({
|
|
218
|
+
canUpload = () => true,
|
|
219
|
+
deleteAttachment = null,
|
|
220
|
+
onError = null,
|
|
221
|
+
onUploaded = async () => null,
|
|
222
|
+
sessionId,
|
|
223
|
+
uploadAttachment,
|
|
224
|
+
maxBytes = ASSISTANT_ATTACHMENT_MAX_BYTES,
|
|
225
|
+
maxItems = ASSISTANT_ATTACHMENT_MAX_ITEMS,
|
|
226
|
+
uploadConcurrency = ASSISTANT_ATTACHMENT_UPLOAD_CONCURRENCY
|
|
227
|
+
} = {}) {
|
|
228
|
+
if (![maxBytes, maxItems, uploadConcurrency].every(value => Number.isSafeInteger(value) && value > 0)) {
|
|
229
|
+
throw new TypeError("Attachment limits must be positive integers.");
|
|
230
|
+
}
|
|
231
|
+
const attachments = ref([]);
|
|
232
|
+
const queueItems = ref([]);
|
|
233
|
+
const dragDepth = ref(0);
|
|
234
|
+
const status = ref("");
|
|
235
|
+
const activeUploadCount = ref(0);
|
|
236
|
+
const uploadQueue = [];
|
|
237
|
+
const rowsById = new Map();
|
|
238
|
+
const activeAttempts = new Map();
|
|
239
|
+
const cleanupTasks = new Set();
|
|
240
|
+
const receiptCleanupById = new Map();
|
|
241
|
+
const cleanedReceiptIds = new Set();
|
|
242
|
+
let disposed = false;
|
|
243
|
+
let stopSessionWatch = null;
|
|
244
|
+
|
|
245
|
+
const dragActive = computed(() => dragDepth.value > 0);
|
|
246
|
+
const uploading = computed(() => (
|
|
247
|
+
activeUploadCount.value > 0 || queueItems.value.some((item) => (
|
|
248
|
+
["preparing", "uploading", "delivering"].includes(item.phase)
|
|
249
|
+
))
|
|
250
|
+
));
|
|
251
|
+
const hasUnresolved = computed(() => queueItems.value.some((item) => (
|
|
252
|
+
ASSISTANT_ATTACHMENT_UNRESOLVED_PHASES.has(item.phase)
|
|
253
|
+
)));
|
|
254
|
+
const canSubmit = computed(() => !hasUnresolved.value);
|
|
255
|
+
const retainedItemCount = computed(() => queueItems.value.filter((item) => (
|
|
256
|
+
item.phase !== "cancelled"
|
|
257
|
+
)).length);
|
|
258
|
+
const atCapacity = computed(() => retainedItemCount.value >= maxItems);
|
|
259
|
+
const canAddFiles = computed(() => Boolean(
|
|
260
|
+
!disposed &&
|
|
261
|
+
assistantAttachmentSessionId(sessionId) &&
|
|
262
|
+
assistantAttachmentUploadAllowed(canUpload) &&
|
|
263
|
+
!atCapacity.value &&
|
|
264
|
+
typeof uploadAttachment === "function"
|
|
265
|
+
));
|
|
266
|
+
const aggregateProgress = computed(() => {
|
|
267
|
+
const retainedItems = queueItems.value.filter((item) => item.phase !== "cancelled");
|
|
268
|
+
const totalBytes = retainedItems.reduce((sum, item) => sum + item.totalBytes, 0);
|
|
269
|
+
const bytesSent = retainedItems.reduce((sum, item) => sum + item.bytesSent, 0);
|
|
270
|
+
return {
|
|
271
|
+
bytesSent,
|
|
272
|
+
percent: totalBytes > 0 ? Math.round((bytesSent / totalBytes) * 100) : 0,
|
|
273
|
+
totalBytes
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
function reportAttachmentError(error, fallbackMessage = "Attachment upload failed.", {
|
|
278
|
+
notify = true
|
|
279
|
+
} = {}) {
|
|
280
|
+
const message = String(error?.message || error || fallbackMessage);
|
|
281
|
+
status.value = message;
|
|
282
|
+
if (notify && typeof onError === "function") {
|
|
283
|
+
try {
|
|
284
|
+
onError(error || new Error(message), fallbackMessage);
|
|
285
|
+
} catch {
|
|
286
|
+
// Feedback presentation must not replace the recoverable attachment state.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return message;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resetDragState() {
|
|
293
|
+
dragDepth.value = 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function clearStatus() {
|
|
297
|
+
status.value = "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createWaiter(row) {
|
|
301
|
+
let resolve = null;
|
|
302
|
+
const promise = new Promise((currentResolve) => {
|
|
303
|
+
resolve = currentResolve;
|
|
304
|
+
});
|
|
305
|
+
row.waiter = {
|
|
306
|
+
promise,
|
|
307
|
+
resolve,
|
|
308
|
+
settled: false
|
|
309
|
+
};
|
|
310
|
+
return row.waiter;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function settleWaiter(row, value = null, waiter = row.waiter) {
|
|
314
|
+
if (!waiter || waiter.settled) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
waiter.settled = true;
|
|
318
|
+
waiter.resolve(value);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function canonicalRow(attachment = {}) {
|
|
322
|
+
const identity = attachmentIdentity(attachment);
|
|
323
|
+
return identity ? rowsById.get(identity) || queueItems.value.find((row) => (
|
|
324
|
+
attachmentIdentity(row) === identity || row.attachmentId === identity
|
|
325
|
+
)) : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function trackCleanup(task) {
|
|
329
|
+
const cleanup = Promise.resolve(task).catch((error) => {
|
|
330
|
+
reportAttachmentError(error, "Attachment cleanup failed.");
|
|
331
|
+
return false;
|
|
332
|
+
});
|
|
333
|
+
cleanupTasks.add(cleanup);
|
|
334
|
+
cleanup.finally(() => cleanupTasks.delete(cleanup));
|
|
335
|
+
return cleanup;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function deleteUploadedAttachment(currentSessionId, attachment = {}) {
|
|
339
|
+
const attachmentId = String(attachment?.attachmentId || "").trim();
|
|
340
|
+
if (!attachmentId || typeof deleteAttachment !== "function") {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const response = await deleteAttachment(currentSessionId, attachmentId);
|
|
344
|
+
if (response?.ok === false) {
|
|
345
|
+
throw new Error(attachmentUploadError(response) || "Attachment cleanup failed.");
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function cleanupReceipt(currentSessionId, receipt = {}) {
|
|
351
|
+
const attachmentId = String(receipt?.attachmentId || "").trim();
|
|
352
|
+
const cleanupId = `${currentSessionId}:${attachmentId}`;
|
|
353
|
+
if (!attachmentId) {
|
|
354
|
+
return Promise.resolve(false);
|
|
355
|
+
}
|
|
356
|
+
if (cleanedReceiptIds.has(cleanupId)) {
|
|
357
|
+
return Promise.resolve(false);
|
|
358
|
+
}
|
|
359
|
+
if (receiptCleanupById.has(cleanupId)) {
|
|
360
|
+
return receiptCleanupById.get(cleanupId);
|
|
361
|
+
}
|
|
362
|
+
const cleanup = trackCleanup(deleteUploadedAttachment(currentSessionId, receipt)).then((deleted) => {
|
|
363
|
+
if (deleted) {
|
|
364
|
+
cleanedReceiptIds.add(cleanupId);
|
|
365
|
+
}
|
|
366
|
+
return deleted;
|
|
367
|
+
});
|
|
368
|
+
receiptCleanupById.set(cleanupId, cleanup);
|
|
369
|
+
cleanup.finally(() => receiptCleanupById.delete(cleanupId));
|
|
370
|
+
return cleanup;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function updateProgress(row, progress, total) {
|
|
374
|
+
if (!row.retained || row.phase !== "uploading") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const next = attachmentProgress(progress, total, row.totalBytes);
|
|
378
|
+
row.bytesSent = Math.max(row.bytesSent, next.bytesSent);
|
|
379
|
+
row.totalBytes = next.totalBytes || row.totalBytes;
|
|
380
|
+
if (next.percent !== null) {
|
|
381
|
+
row.progress = Math.max(Number(row.progress) || 0, next.percent);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function removeReadyReceipt(row) {
|
|
386
|
+
const removed = attachments.value.filter((attachment) => (
|
|
387
|
+
attachment.clientId === row.clientId
|
|
388
|
+
));
|
|
389
|
+
attachments.value = attachments.value.filter((attachment) => (
|
|
390
|
+
attachment.clientId !== row.clientId
|
|
391
|
+
));
|
|
392
|
+
return removed;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function handOffUploadedAttachment(row, receipt, waiter = row.waiter) {
|
|
396
|
+
const token = Symbol(`${row.clientId}-handoff`);
|
|
397
|
+
row.activeHandoffToken = token;
|
|
398
|
+
row.phase = "delivering";
|
|
399
|
+
if (!attachments.value.some((attachment) => attachment.clientId === row.clientId)) {
|
|
400
|
+
attachments.value.push(receipt);
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const handoff = await deliverUploadedAttachments(onUploaded, [receipt]);
|
|
404
|
+
const accepted = handoff?.accepted === true;
|
|
405
|
+
if (row.activeHandoffToken !== token || !row.retained || row.phase === "cancelled") {
|
|
406
|
+
removeReadyReceipt(row);
|
|
407
|
+
if (!accepted) {
|
|
408
|
+
await cleanupReceipt(row.sessionId, receipt);
|
|
409
|
+
}
|
|
410
|
+
settleWaiter(row, null, waiter);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
row.activeHandoffToken = null;
|
|
414
|
+
row.error = "";
|
|
415
|
+
row.failureStage = "";
|
|
416
|
+
if (accepted) {
|
|
417
|
+
removeReadyReceipt(row);
|
|
418
|
+
row.retained = false;
|
|
419
|
+
rowsById.delete(row.clientId);
|
|
420
|
+
queueItems.value = queueItems.value.filter((candidate) => candidate.clientId !== row.clientId);
|
|
421
|
+
settleWaiter(row, receipt, waiter);
|
|
422
|
+
return receipt;
|
|
423
|
+
}
|
|
424
|
+
row.phase = "ready";
|
|
425
|
+
settleWaiter(row, receipt, waiter);
|
|
426
|
+
return receipt;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (row.activeHandoffToken !== token || !row.retained || row.phase === "cancelled") {
|
|
429
|
+
await cleanupReceipt(row.sessionId, receipt);
|
|
430
|
+
settleWaiter(row, null, waiter);
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
row.activeHandoffToken = null;
|
|
434
|
+
removeReadyReceipt(row);
|
|
435
|
+
row.error = String(error?.message || error || "Uploaded attachment could not be used.");
|
|
436
|
+
row.failureStage = "handoff";
|
|
437
|
+
row.phase = "failed";
|
|
438
|
+
settleWaiter(row, null, waiter);
|
|
439
|
+
reportAttachmentError(error, "Uploaded attachment could not be used.", { notify: false });
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function finishUploadAttempt(attempt, uploaded, uploadError = null) {
|
|
445
|
+
activeAttempts.delete(attempt.token);
|
|
446
|
+
activeUploadCount.value = activeAttempts.size;
|
|
447
|
+
pumpQueue();
|
|
448
|
+
const { row } = attempt;
|
|
449
|
+
const isCurrentAttempt = row.activeToken === attempt.token;
|
|
450
|
+
const uploadSucceeded = Boolean(
|
|
451
|
+
!uploadError &&
|
|
452
|
+
uploaded &&
|
|
453
|
+
uploaded.ok !== false &&
|
|
454
|
+
String(uploaded.attachmentId || "").trim()
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (!isCurrentAttempt || !row.retained || disposed) {
|
|
458
|
+
if (uploadSucceeded) {
|
|
459
|
+
await cleanupReceipt(attempt.sessionId, uploaded);
|
|
460
|
+
}
|
|
461
|
+
settleWaiter(row, null, attempt.waiter);
|
|
462
|
+
pumpQueue();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
row.activeToken = null;
|
|
467
|
+
if (uploadSucceeded) {
|
|
468
|
+
const receipt = attachmentReceipt(row, uploaded);
|
|
469
|
+
row.receipt = receipt;
|
|
470
|
+
row.attachmentId = receipt.attachmentId;
|
|
471
|
+
row.path = String(receipt.path || "");
|
|
472
|
+
row.bytesSent = row.totalBytes;
|
|
473
|
+
row.progress = 100;
|
|
474
|
+
row.error = "";
|
|
475
|
+
row.failureStage = "";
|
|
476
|
+
await handOffUploadedAttachment(row, receipt, attempt.waiter);
|
|
477
|
+
} else {
|
|
478
|
+
const error = uploadError || new Error(
|
|
479
|
+
attachmentUploadError(uploaded) || "Attachment upload failed."
|
|
480
|
+
);
|
|
481
|
+
row.error = String(error?.message || error || "Attachment upload failed.");
|
|
482
|
+
row.failureStage = "upload";
|
|
483
|
+
row.phase = "failed";
|
|
484
|
+
settleWaiter(row, null, attempt.waiter);
|
|
485
|
+
reportAttachmentError(error, "Attachment upload failed.", { notify: false });
|
|
486
|
+
}
|
|
487
|
+
pumpQueue();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function startUpload(row) {
|
|
491
|
+
const token = Symbol(row.clientId);
|
|
492
|
+
const abortController = typeof AbortController === "function"
|
|
493
|
+
? new AbortController()
|
|
494
|
+
: null;
|
|
495
|
+
const attempt = {
|
|
496
|
+
abortController,
|
|
497
|
+
cancel: null,
|
|
498
|
+
row,
|
|
499
|
+
sessionId: row.sessionId,
|
|
500
|
+
settled: null,
|
|
501
|
+
token,
|
|
502
|
+
waiter: row.waiter
|
|
503
|
+
};
|
|
504
|
+
row.activeToken = token;
|
|
505
|
+
row.phase = "uploading";
|
|
506
|
+
row.error = "";
|
|
507
|
+
activeAttempts.set(token, attempt);
|
|
508
|
+
activeUploadCount.value = activeAttempts.size;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const transport = attachmentUploadResult(uploadAttachment(row.sessionId, row.file, {
|
|
512
|
+
onProgress: (progress, total) => updateProgress(row, progress, total),
|
|
513
|
+
signal: abortController?.signal
|
|
514
|
+
}));
|
|
515
|
+
attempt.cancel = transport.cancel;
|
|
516
|
+
attempt.settled = transport.promise.then(
|
|
517
|
+
(uploaded) => finishUploadAttempt(attempt, uploaded),
|
|
518
|
+
(error) => finishUploadAttempt(attempt, null, error)
|
|
519
|
+
);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
attempt.settled = finishUploadAttempt(attempt, null, error);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function pumpQueue() {
|
|
526
|
+
if (disposed) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
while (activeAttempts.size < uploadConcurrency && uploadQueue.length > 0) {
|
|
530
|
+
const row = uploadQueue.shift();
|
|
531
|
+
if (!row?.retained || row.phase !== "queued") {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
startUpload(row);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function cancelAttachment(attachment = {}) {
|
|
539
|
+
const row = canonicalRow(attachment);
|
|
540
|
+
if (!row || !row.retained || !["preparing", "queued", "uploading"].includes(row.phase)) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
const token = row.activeToken;
|
|
544
|
+
row.activeToken = null;
|
|
545
|
+
row.activeHandoffToken = null;
|
|
546
|
+
row.phase = "cancelled";
|
|
547
|
+
row.error = "";
|
|
548
|
+
const receipt = row.receipt;
|
|
549
|
+
removeReadyReceipt(row);
|
|
550
|
+
if (receipt) {
|
|
551
|
+
void cleanupReceipt(row.sessionId, receipt);
|
|
552
|
+
}
|
|
553
|
+
row.receipt = null;
|
|
554
|
+
row.failureStage = "";
|
|
555
|
+
settleWaiter(row, null);
|
|
556
|
+
const attempt = token ? activeAttempts.get(token) : null;
|
|
557
|
+
try {
|
|
558
|
+
row.producerAbortController?.abort();
|
|
559
|
+
attempt?.abortController?.abort();
|
|
560
|
+
attempt?.cancel?.();
|
|
561
|
+
} catch {
|
|
562
|
+
// A cancelled transport is already detached; late success is still cleaned up.
|
|
563
|
+
}
|
|
564
|
+
row.producerAbortController = null;
|
|
565
|
+
row.producerToken = null;
|
|
566
|
+
pumpQueue();
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function retryAttachment(attachment = {}) {
|
|
571
|
+
const row = canonicalRow(attachment);
|
|
572
|
+
if (
|
|
573
|
+
!row ||
|
|
574
|
+
!row.retained ||
|
|
575
|
+
!["failed", "cancelled"].includes(row.phase) ||
|
|
576
|
+
row.sessionId !== assistantAttachmentSessionId(sessionId) ||
|
|
577
|
+
!assistantAttachmentUploadAllowed(canUpload) ||
|
|
578
|
+
typeof uploadAttachment !== "function"
|
|
579
|
+
) {
|
|
580
|
+
return Promise.resolve(null);
|
|
581
|
+
}
|
|
582
|
+
if (row.phase === "cancelled" && retainedItemCount.value >= maxItems) {
|
|
583
|
+
reportAttachmentError(`A message can keep at most ${maxItems} attachments.`);
|
|
584
|
+
return Promise.resolve(null);
|
|
585
|
+
}
|
|
586
|
+
if (row.failureStage === "handoff" && row.receipt) {
|
|
587
|
+
row.error = "";
|
|
588
|
+
row.phase = "uploading";
|
|
589
|
+
const waiter = createWaiter(row);
|
|
590
|
+
void handOffUploadedAttachment(row, row.receipt, waiter);
|
|
591
|
+
return waiter.promise;
|
|
592
|
+
}
|
|
593
|
+
if (row.producer && !row.file) {
|
|
594
|
+
const waiter = createWaiter(row);
|
|
595
|
+
startAttachmentProducer(row);
|
|
596
|
+
return waiter.promise;
|
|
597
|
+
}
|
|
598
|
+
if (row.size > maxBytes) {
|
|
599
|
+
row.error = `${row.fileName} is larger than the ${maxBytes / 1_000_000} MB attachment limit.`;
|
|
600
|
+
row.failureStage = "validation";
|
|
601
|
+
row.phase = "failed";
|
|
602
|
+
reportAttachmentError(row.error, "Attachment upload failed.", { notify: false });
|
|
603
|
+
return Promise.resolve(null);
|
|
604
|
+
}
|
|
605
|
+
row.bytesSent = 0;
|
|
606
|
+
row.progress = null;
|
|
607
|
+
row.error = "";
|
|
608
|
+
row.failureStage = "";
|
|
609
|
+
row.phase = "queued";
|
|
610
|
+
const waiter = createWaiter(row);
|
|
611
|
+
uploadQueue.push(row);
|
|
612
|
+
pumpQueue();
|
|
613
|
+
return waiter.promise;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function detachRow(row) {
|
|
617
|
+
if (["preparing", "queued", "uploading"].includes(row.phase)) {
|
|
618
|
+
cancelAttachment(row);
|
|
619
|
+
}
|
|
620
|
+
row.retained = false;
|
|
621
|
+
rowsById.delete(row.clientId);
|
|
622
|
+
settleWaiter(row, null);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function removeAttachment(attachment = {}) {
|
|
626
|
+
const row = canonicalRow(attachment);
|
|
627
|
+
if (!row) {
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
const removedReceipts = removeReadyReceipt(row);
|
|
631
|
+
const removed = removedReceipts.length > 0 ? removedReceipts : [row];
|
|
632
|
+
const receiptToClean = removedReceipts[0] || row.receipt;
|
|
633
|
+
detachRow(row);
|
|
634
|
+
queueItems.value = queueItems.value.filter((candidate) => candidate.clientId !== row.clientId);
|
|
635
|
+
if (receiptToClean) {
|
|
636
|
+
void cleanupReceipt(row.sessionId, receiptToClean);
|
|
637
|
+
}
|
|
638
|
+
return removed;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function clearAttachments({ accepted = true, attachmentIds = null } = {}) {
|
|
642
|
+
if (!accepted) {
|
|
643
|
+
return abandonAttachments();
|
|
644
|
+
}
|
|
645
|
+
const selectedIds = Array.isArray(attachmentIds) ? new Set(attachmentIds) : null;
|
|
646
|
+
const cleared = attachments.value.filter((attachment) => !selectedIds || selectedIds.has(attachment.attachmentId));
|
|
647
|
+
const readyIds = new Set(cleared.map((attachment) => attachment.clientId));
|
|
648
|
+
const remaining = [];
|
|
649
|
+
for (const row of [...queueItems.value]) {
|
|
650
|
+
if (selectedIds && !readyIds.has(row.clientId)) {
|
|
651
|
+
remaining.push(row);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (readyIds.has(row.clientId)) {
|
|
655
|
+
row.retained = false;
|
|
656
|
+
rowsById.delete(row.clientId);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
detachRow(row);
|
|
660
|
+
if (row.receipt) {
|
|
661
|
+
void cleanupReceipt(row.sessionId, row.receipt);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
attachments.value = attachments.value.filter((attachment) => !readyIds.has(attachment.clientId));
|
|
665
|
+
queueItems.value = remaining;
|
|
666
|
+
return cleared;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function abandonAttachments({ onlySessionId = "" } = {}) {
|
|
670
|
+
const normalizedSessionId = String(onlySessionId || "").trim();
|
|
671
|
+
const rows = queueItems.value.filter((row) => (
|
|
672
|
+
!normalizedSessionId || row.sessionId === normalizedSessionId
|
|
673
|
+
));
|
|
674
|
+
const rowIds = new Set(rows.map((row) => row.clientId));
|
|
675
|
+
const readyReceipts = rows.map((row) => row.receipt).filter(Boolean);
|
|
676
|
+
const attempts = [...activeAttempts.values()].filter((attempt) => rowIds.has(attempt.row.clientId));
|
|
677
|
+
|
|
678
|
+
for (const row of rows) {
|
|
679
|
+
detachRow(row);
|
|
680
|
+
}
|
|
681
|
+
queueItems.value = queueItems.value.filter((row) => !rowIds.has(row.clientId));
|
|
682
|
+
attachments.value = attachments.value.filter((attachment) => !rowIds.has(attachment.clientId));
|
|
683
|
+
|
|
684
|
+
const readyCleanup = readyReceipts.map((attachment) => cleanupReceipt(
|
|
685
|
+
attachment.sessionId,
|
|
686
|
+
attachment
|
|
687
|
+
));
|
|
688
|
+
await Promise.allSettled([
|
|
689
|
+
...readyCleanup,
|
|
690
|
+
...attempts.map((attempt) => attempt.settled).filter(Boolean)
|
|
691
|
+
]);
|
|
692
|
+
await waitForAttachmentCleanup();
|
|
693
|
+
return rows;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function waitForAttachmentCleanup() {
|
|
697
|
+
while (cleanupTasks.size > 0) {
|
|
698
|
+
await Promise.allSettled([...cleanupTasks]);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function disposeAttachments() {
|
|
703
|
+
if (disposed) {
|
|
704
|
+
await waitForAttachmentCleanup();
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
disposed = true;
|
|
708
|
+
stopSessionWatch?.();
|
|
709
|
+
stopSessionWatch = null;
|
|
710
|
+
return abandonAttachments();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function handleDragEnter(event) {
|
|
714
|
+
if (!assistantAttachmentEventHasFiles(event)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
dragDepth.value += 1;
|
|
718
|
+
if (event?.dataTransfer) {
|
|
719
|
+
event.dataTransfer.dropEffect = "copy";
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function handleDragOver(event) {
|
|
724
|
+
if (event?.dataTransfer) {
|
|
725
|
+
event.dataTransfer.dropEffect = "copy";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function handleDragLeave() {
|
|
730
|
+
dragDepth.value = Math.max(0, dragDepth.value - 1);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function createQueueRow({
|
|
734
|
+
currentSessionId,
|
|
735
|
+
file = null,
|
|
736
|
+
fileName = "attachment",
|
|
737
|
+
phase = "queued",
|
|
738
|
+
producer = null,
|
|
739
|
+
size = 0
|
|
740
|
+
} = {}) {
|
|
741
|
+
const normalizedSize = Number(file?.size ?? size) || 0;
|
|
742
|
+
const row = reactive({
|
|
743
|
+
activeHandoffToken: null,
|
|
744
|
+
activeToken: null,
|
|
745
|
+
attachmentId: "",
|
|
746
|
+
bytesSent: 0,
|
|
747
|
+
clientId: nextAttachmentClientId(),
|
|
748
|
+
error: "",
|
|
749
|
+
failureStage: "",
|
|
750
|
+
file,
|
|
751
|
+
fileName: file ? attachmentFileName(file) : String(fileName || "attachment"),
|
|
752
|
+
path: "",
|
|
753
|
+
phase,
|
|
754
|
+
producer,
|
|
755
|
+
producerAbortController: null,
|
|
756
|
+
producerToken: null,
|
|
757
|
+
progress: null,
|
|
758
|
+
receipt: null,
|
|
759
|
+
retained: true,
|
|
760
|
+
sessionId: currentSessionId,
|
|
761
|
+
size: normalizedSize,
|
|
762
|
+
totalBytes: normalizedSize,
|
|
763
|
+
waiter: null
|
|
764
|
+
});
|
|
765
|
+
rowsById.set(row.clientId, row);
|
|
766
|
+
queueItems.value.push(row);
|
|
767
|
+
createWaiter(row);
|
|
768
|
+
return row;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function queuePreparedFile(row, file, waiter = row.waiter) {
|
|
772
|
+
const preparedFile = assistantAttachmentFiles([file])[0];
|
|
773
|
+
if (!preparedFile) {
|
|
774
|
+
throw new Error("The prepared attachment did not produce a file.");
|
|
775
|
+
}
|
|
776
|
+
row.file = preparedFile;
|
|
777
|
+
row.fileName = attachmentFileName(preparedFile);
|
|
778
|
+
row.size = Number(preparedFile.size) || 0;
|
|
779
|
+
row.totalBytes = row.size;
|
|
780
|
+
if (row.size > maxBytes) {
|
|
781
|
+
row.error = `${row.fileName} is larger than the ${maxBytes / 1_000_000} MB attachment limit.`;
|
|
782
|
+
row.failureStage = "validation";
|
|
783
|
+
row.phase = "failed";
|
|
784
|
+
settleWaiter(row, null, waiter);
|
|
785
|
+
reportAttachmentError(row.error, "Attachment upload failed.", { notify: false });
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
row.phase = "queued";
|
|
789
|
+
uploadQueue.push(row);
|
|
790
|
+
pumpQueue();
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function startAttachmentProducer(row) {
|
|
795
|
+
const token = Symbol(`${row.clientId}-producer`);
|
|
796
|
+
const waiter = row.waiter;
|
|
797
|
+
const abortController = typeof AbortController === "function"
|
|
798
|
+
? new AbortController()
|
|
799
|
+
: null;
|
|
800
|
+
row.file = null;
|
|
801
|
+
row.producerAbortController = abortController;
|
|
802
|
+
row.producerToken = token;
|
|
803
|
+
row.phase = "preparing";
|
|
804
|
+
row.error = "";
|
|
805
|
+
row.failureStage = "";
|
|
806
|
+
row.producerSettled = Promise.resolve().then(() => row.producer({
|
|
807
|
+
signal: abortController?.signal
|
|
808
|
+
})).then((file) => {
|
|
809
|
+
if (row.producerToken !== token || !row.retained || disposed) {
|
|
810
|
+
settleWaiter(row, null, waiter);
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
row.producerAbortController = null;
|
|
814
|
+
row.producerToken = null;
|
|
815
|
+
try {
|
|
816
|
+
queuePreparedFile(row, file, waiter);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
row.error = String(error?.message || error || "Attachment preparation failed.");
|
|
819
|
+
row.failureStage = "preparing";
|
|
820
|
+
row.phase = "failed";
|
|
821
|
+
settleWaiter(row, null, waiter);
|
|
822
|
+
reportAttachmentError(error, "Attachment preparation failed.", { notify: false });
|
|
823
|
+
}
|
|
824
|
+
return file;
|
|
825
|
+
}, (error) => {
|
|
826
|
+
if (row.producerToken !== token || !row.retained || disposed) {
|
|
827
|
+
settleWaiter(row, null, waiter);
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
row.producerAbortController = null;
|
|
831
|
+
row.producerToken = null;
|
|
832
|
+
if (error?.name === "AbortError") {
|
|
833
|
+
row.phase = "cancelled";
|
|
834
|
+
row.error = "";
|
|
835
|
+
} else {
|
|
836
|
+
row.error = String(error?.message || error || "Attachment preparation failed.");
|
|
837
|
+
row.failureStage = "preparing";
|
|
838
|
+
row.phase = "failed";
|
|
839
|
+
reportAttachmentError(error, "Attachment preparation failed.", { notify: false });
|
|
840
|
+
}
|
|
841
|
+
settleWaiter(row, null, waiter);
|
|
842
|
+
return null;
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function uploadFileProducer({
|
|
847
|
+
fileName = "Preparing attachment…",
|
|
848
|
+
produce
|
|
849
|
+
} = {}) {
|
|
850
|
+
const currentSessionId = assistantAttachmentSessionId(sessionId);
|
|
851
|
+
if (!canAddFiles.value || typeof produce !== "function") {
|
|
852
|
+
if (atCapacity.value) {
|
|
853
|
+
reportAttachmentError(`A message can keep at most ${maxItems} attachments.`);
|
|
854
|
+
}
|
|
855
|
+
return Promise.resolve(null);
|
|
856
|
+
}
|
|
857
|
+
status.value = "";
|
|
858
|
+
const row = createQueueRow({
|
|
859
|
+
currentSessionId,
|
|
860
|
+
fileName,
|
|
861
|
+
phase: "preparing",
|
|
862
|
+
producer: produce
|
|
863
|
+
});
|
|
864
|
+
const waiter = row.waiter;
|
|
865
|
+
startAttachmentProducer(row);
|
|
866
|
+
return waiter.promise;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function uploadFiles(files = []) {
|
|
870
|
+
const uploadableFiles = assistantAttachmentFiles(files);
|
|
871
|
+
const currentSessionId = assistantAttachmentSessionId(sessionId);
|
|
872
|
+
if (
|
|
873
|
+
uploadableFiles.length < 1 ||
|
|
874
|
+
!currentSessionId ||
|
|
875
|
+
disposed ||
|
|
876
|
+
!assistantAttachmentUploadAllowed(canUpload) ||
|
|
877
|
+
typeof uploadAttachment !== "function"
|
|
878
|
+
) {
|
|
879
|
+
return [];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
status.value = "";
|
|
883
|
+
const availableSlots = Math.max(0, maxItems - retainedItemCount.value);
|
|
884
|
+
const acceptedFiles = uploadableFiles.slice(0, availableSlots);
|
|
885
|
+
if (acceptedFiles.length < uploadableFiles.length) {
|
|
886
|
+
reportAttachmentError(`A message can keep at most ${maxItems} attachments.`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const waiters = [];
|
|
890
|
+
for (const file of acceptedFiles) {
|
|
891
|
+
const row = createQueueRow({ currentSessionId, file });
|
|
892
|
+
const waiter = row.waiter;
|
|
893
|
+
waiters.push(waiter.promise);
|
|
894
|
+
if (row.size > maxBytes) {
|
|
895
|
+
row.error = `${row.fileName} is larger than the ${maxBytes / 1_000_000} MB attachment limit.`;
|
|
896
|
+
row.failureStage = "validation";
|
|
897
|
+
row.phase = "failed";
|
|
898
|
+
settleWaiter(row, null);
|
|
899
|
+
reportAttachmentError(row.error, "Attachment upload failed.", { notify: false });
|
|
900
|
+
} else {
|
|
901
|
+
uploadQueue.push(row);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
pumpQueue();
|
|
906
|
+
const settled = await Promise.all(waiters);
|
|
907
|
+
return settled.filter((attachment) => (
|
|
908
|
+
attachment && attachments.value.some((candidate) => candidate.clientId === attachment.clientId)
|
|
909
|
+
));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function handleDrop(event) {
|
|
913
|
+
resetDragState();
|
|
914
|
+
return uploadFiles(assistantAttachmentFilesFromDropEvent(event));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function handlePaste(event) {
|
|
918
|
+
const files = assistantAttachmentFilesFromPasteEvent(event);
|
|
919
|
+
if (files.length < 1) {
|
|
920
|
+
if (assistantAttachmentClipboardLocalFileReference(event)) {
|
|
921
|
+
reportAttachmentError(
|
|
922
|
+
"Copied local files cannot be pasted from this browser. Drop the file or use Attach files."
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
if (!assistantAttachmentClipboardText(event)) {
|
|
928
|
+
event?.preventDefault?.();
|
|
929
|
+
}
|
|
930
|
+
return uploadFiles(files);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
stopSessionWatch = watch(
|
|
934
|
+
() => assistantAttachmentSessionId(sessionId),
|
|
935
|
+
(nextSessionId, previousSessionId) => {
|
|
936
|
+
if (nextSessionId !== previousSessionId && previousSessionId) {
|
|
937
|
+
void abandonAttachments({ onlySessionId: previousSessionId });
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
{ flush: "sync" }
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
if (getCurrentScope()) {
|
|
944
|
+
onScopeDispose(() => {
|
|
945
|
+
void disposeAttachments();
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
maxBytes,
|
|
951
|
+
maxItems,
|
|
952
|
+
abandonAttachments,
|
|
953
|
+
aggregateProgress,
|
|
954
|
+
attachments,
|
|
955
|
+
atCapacity,
|
|
956
|
+
canAddFiles,
|
|
957
|
+
cancelAttachment,
|
|
958
|
+
canSubmit,
|
|
959
|
+
clearAttachments,
|
|
960
|
+
clearStatus,
|
|
961
|
+
disposeAttachments,
|
|
962
|
+
dragActive,
|
|
963
|
+
hasUnresolved,
|
|
964
|
+
handleDragEnter,
|
|
965
|
+
handleDragLeave,
|
|
966
|
+
handleDragOver,
|
|
967
|
+
handleDrop,
|
|
968
|
+
handlePaste,
|
|
969
|
+
queueItems,
|
|
970
|
+
removeAttachment,
|
|
971
|
+
resetDragState,
|
|
972
|
+
retryAttachment,
|
|
973
|
+
status,
|
|
974
|
+
uploading,
|
|
975
|
+
uploadFiles,
|
|
976
|
+
uploadFileProducer,
|
|
977
|
+
waitForAttachmentCleanup
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export {
|
|
982
|
+
ASSISTANT_ATTACHMENT_MAX_BYTES,
|
|
983
|
+
ASSISTANT_ATTACHMENT_MAX_ITEMS,
|
|
984
|
+
ASSISTANT_ATTACHMENT_UPLOAD_CONCURRENCY,
|
|
985
|
+
assistantAttachmentEventHasFiles,
|
|
986
|
+
assistantAttachmentFiles,
|
|
987
|
+
assistantAttachmentFilesFromDropEvent,
|
|
988
|
+
assistantAttachmentFilesFromPasteEvent,
|
|
989
|
+
assistantAttachmentFilesFromTransferItems,
|
|
990
|
+
useAssistantAttachments
|
|
991
|
+
};
|