@aleph-alpha/chat-kit 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-CyGLItQZ.js → CkInput.vue_vue_type_script_setup_true_lang-C5C09u0z.js} +9 -8
  2. package/dist/adapters/index.d.ts +18 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/index.js +4 -0
  5. package/dist/adapters/responses-api.d.ts +180 -0
  6. package/dist/adapters/responses-api.d.ts.map +1 -0
  7. package/dist/adapters/responses-api.js +153 -0
  8. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts +1 -1
  9. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +11 -3
  10. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  11. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +5 -1
  12. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  13. package/dist/components/index.js +1 -1
  14. package/dist/composables/fileUpload/index.d.ts +7 -0
  15. package/dist/composables/fileUpload/index.d.ts.map +1 -0
  16. package/dist/composables/fileUpload/types.d.ts +295 -0
  17. package/dist/composables/fileUpload/types.d.ts.map +1 -0
  18. package/dist/composables/fileUpload/useFileUpload.d.ts +4 -0
  19. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -0
  20. package/dist/composables/fileUpload/useFileUploadWrapper.d.ts +3 -0
  21. package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -0
  22. package/dist/composables/index.d.ts +2 -1
  23. package/dist/composables/index.d.ts.map +1 -1
  24. package/dist/composables/index.js +5 -2
  25. package/dist/composables/useConversations.d.ts +12 -0
  26. package/dist/composables/useConversations.d.ts.map +1 -1
  27. package/dist/generateId-BHf0W4Rq.js +6 -0
  28. package/dist/handlers/index.js +1 -1
  29. package/dist/handlers/useMessageHandler.d.ts +2 -2
  30. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  31. package/dist/index-BbTgIoYa.js +1557 -0
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +6 -8
  35. package/dist/services/index.d.ts +0 -2
  36. package/dist/services/index.d.ts.map +1 -1
  37. package/dist/services/index.js +1 -37
  38. package/dist/services/types.d.ts +2 -2
  39. package/dist/services/types.d.ts.map +1 -1
  40. package/dist/types.d.ts +47 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/{useMessageHandler-BmBdu2EW.js → useMessageHandler-ZRC-gIOJ.js} +5 -5
  43. package/package.json +10 -2
  44. package/src/adapters/index.ts +17 -0
  45. package/src/adapters/responses-api.spec.ts +365 -0
  46. package/src/adapters/responses-api.ts +444 -0
  47. package/src/components/composed/CkConversation/CkConversation.spec.ts +171 -25
  48. package/src/components/composed/CkConversation/CkConversation.stories.ts +266 -46
  49. package/src/components/composed/CkConversation/CkConversation.vue +19 -7
  50. package/src/composables/fileUpload/index.spec.ts +213 -0
  51. package/src/composables/fileUpload/index.ts +83 -0
  52. package/src/composables/fileUpload/types.ts +409 -0
  53. package/src/composables/fileUpload/useFileUpload.ts +1242 -0
  54. package/src/composables/fileUpload/useFileUploadWrapper.ts +382 -0
  55. package/src/composables/index.ts +2 -1
  56. package/src/composables/useConversations.spec.ts +118 -0
  57. package/src/composables/useConversations.ts +32 -0
  58. package/src/handlers/useChatKitLabels.spec.ts +16 -2
  59. package/src/handlers/useMessageHandler.spec.ts +112 -44
  60. package/src/handlers/useMessageHandler.ts +16 -2
  61. package/src/helpers/conversationTree.spec.ts +46 -19
  62. package/src/index.ts +11 -2
  63. package/src/services/index.ts +0 -2
  64. package/src/services/types.ts +2 -2
  65. package/src/types.ts +55 -2
  66. package/dist/ChatService-LogwJNxh.js +0 -9
  67. package/dist/services/ChatService.d.ts +0 -42
  68. package/dist/services/ChatService.d.ts.map +0 -1
  69. package/dist/services/MockChatService.d.ts +0 -10
  70. package/dist/services/MockChatService.d.ts.map +0 -1
  71. package/dist/useConversationSessions-ulFhtpQr.js +0 -243
  72. package/src/services/ChatService.ts +0 -60
  73. package/src/services/MockChatService.ts +0 -47
@@ -0,0 +1,1557 @@
1
+ import { g as generateId } from "./generateId-BHf0W4Rq.js";
2
+ import { ref, shallowRef, computed, readonly, toRaw, toValue, watch } from "vue";
3
+ import { u as useMessageHandler } from "./useMessageHandler-ZRC-gIOJ.js";
4
+ import "./purify.es-md74IFlY.js";
5
+ function secondsToDate(seconds) {
6
+ return new Date(seconds * 1e3);
7
+ }
8
+ function toError(value) {
9
+ return value instanceof Error ? value : new Error(String(value));
10
+ }
11
+ function useChat() {
12
+ }
13
+ const DEFAULT_PAGE_SIZE = 20;
14
+ function useConversations(options) {
15
+ const http = options.httpClient;
16
+ const path = options.path;
17
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
18
+ const conversations = ref([]);
19
+ const inflightCount = shallowRef(0);
20
+ const isLoading = computed(() => inflightCount.value > 0);
21
+ const hasMore = shallowRef(false);
22
+ const error = shallowRef(null);
23
+ let nextCursor = null;
24
+ function beginRequest() {
25
+ inflightCount.value++;
26
+ error.value = null;
27
+ }
28
+ function endRequest() {
29
+ inflightCount.value--;
30
+ }
31
+ async function fetchPage(params) {
32
+ const query = {
33
+ limit: params.limit,
34
+ order: "desc"
35
+ };
36
+ if (params.cursor) query.after = params.cursor;
37
+ const raw = await http.get(path, { params: query });
38
+ return mapConversationList(raw);
39
+ }
40
+ async function fetch2(opts) {
41
+ beginRequest();
42
+ try {
43
+ const page = await fetchPage({
44
+ limit: opts?.limit ?? pageSize,
45
+ cursor: null
46
+ });
47
+ conversations.value = page.items;
48
+ nextCursor = page.nextCursor;
49
+ hasMore.value = page.nextCursor !== null;
50
+ } catch (e) {
51
+ error.value = toError(e);
52
+ throw error.value;
53
+ } finally {
54
+ endRequest();
55
+ }
56
+ }
57
+ async function fetchMore() {
58
+ if (!hasMore.value || nextCursor === null) return;
59
+ beginRequest();
60
+ try {
61
+ const page = await fetchPage({ limit: pageSize, cursor: nextCursor });
62
+ conversations.value = [...conversations.value, ...page.items];
63
+ nextCursor = page.nextCursor;
64
+ hasMore.value = page.nextCursor !== null;
65
+ } catch (e) {
66
+ error.value = toError(e);
67
+ throw error.value;
68
+ } finally {
69
+ endRequest();
70
+ }
71
+ }
72
+ async function fetchById(id) {
73
+ beginRequest();
74
+ try {
75
+ const raw = await http.get(`${path}/${id}`);
76
+ return mapConversation(raw);
77
+ } catch (e) {
78
+ error.value = toError(e);
79
+ throw error.value;
80
+ } finally {
81
+ endRequest();
82
+ }
83
+ }
84
+ async function create(input, opts) {
85
+ const metadata = { ...input?.metadata ?? {} };
86
+ if (input?.title !== void 0) metadata.title = input.title;
87
+ const body = { metadata };
88
+ const previous = conversations.value;
89
+ let placeholder = null;
90
+ if (opts?.optimistic) {
91
+ placeholder = makePlaceholder(input?.title);
92
+ conversations.value = [placeholder, ...previous];
93
+ }
94
+ beginRequest();
95
+ try {
96
+ const raw = await http.post(path, body);
97
+ const created = mapConversation(raw);
98
+ conversations.value = placeholder ? conversations.value.map(
99
+ (c) => c.id === placeholder.id ? created : c
100
+ ) : [created, ...conversations.value];
101
+ return created;
102
+ } catch (e) {
103
+ if (placeholder) conversations.value = previous;
104
+ error.value = toError(e);
105
+ throw error.value;
106
+ } finally {
107
+ endRequest();
108
+ }
109
+ }
110
+ async function rename(id, title, opts) {
111
+ const previous = conversations.value;
112
+ if (opts?.optimistic) {
113
+ conversations.value = previous.map(
114
+ (c) => c.id === id ? { ...c, title, updatedAt: /* @__PURE__ */ new Date() } : c
115
+ );
116
+ }
117
+ beginRequest();
118
+ try {
119
+ const raw = await http.post(`${path}/${id}`, {
120
+ metadata: { title }
121
+ });
122
+ const updated = mapConversation(raw);
123
+ conversations.value = conversations.value.map(
124
+ (c) => c.id === id ? updated : c
125
+ );
126
+ return updated;
127
+ } catch (e) {
128
+ if (opts?.optimistic) conversations.value = previous;
129
+ error.value = toError(e);
130
+ throw error.value;
131
+ } finally {
132
+ endRequest();
133
+ }
134
+ }
135
+ async function remove(id, opts) {
136
+ const previous = conversations.value;
137
+ if (opts?.optimistic) {
138
+ conversations.value = previous.filter((c) => c.id !== id);
139
+ }
140
+ beginRequest();
141
+ try {
142
+ await http.delete(`${path}/${id}`);
143
+ if (!opts?.optimistic) {
144
+ conversations.value = previous.filter((c) => c.id !== id);
145
+ }
146
+ } catch (e) {
147
+ if (opts?.optimistic) conversations.value = previous;
148
+ error.value = toError(e);
149
+ throw error.value;
150
+ } finally {
151
+ endRequest();
152
+ }
153
+ }
154
+ function getById(id) {
155
+ return conversations.value.find((c) => c.id === id);
156
+ }
157
+ async function fetchResponses(id, opts) {
158
+ beginRequest();
159
+ try {
160
+ const raw = await http.get(`${path}/${id}/responses`, {
161
+ params: { order: opts?.order ?? "asc" }
162
+ });
163
+ return raw.data;
164
+ } catch (e) {
165
+ error.value = toError(e);
166
+ throw error.value;
167
+ } finally {
168
+ endRequest();
169
+ }
170
+ }
171
+ return {
172
+ conversations: readonly(conversations),
173
+ isLoading,
174
+ hasMore: readonly(hasMore),
175
+ error: readonly(error),
176
+ fetch: fetch2,
177
+ fetchMore,
178
+ fetchById,
179
+ create,
180
+ rename,
181
+ remove,
182
+ getById,
183
+ fetchResponses
184
+ };
185
+ }
186
+ function mapConversation(raw) {
187
+ return {
188
+ id: raw.id,
189
+ title: raw.metadata?.title ?? "",
190
+ createdAt: secondsToDate(raw.created_at),
191
+ updatedAt: secondsToDate(raw.updated_at)
192
+ };
193
+ }
194
+ function mapConversationList(raw) {
195
+ return {
196
+ items: raw.data.map(mapConversation),
197
+ nextCursor: raw.has_more ? raw.last_id ?? null : null
198
+ };
199
+ }
200
+ function makePlaceholder(title) {
201
+ const now2 = /* @__PURE__ */ new Date();
202
+ return {
203
+ id: `pending-${generateId()}`,
204
+ title: title ?? "",
205
+ createdAt: now2,
206
+ updatedAt: now2
207
+ };
208
+ }
209
+ function useConversationSessions() {
210
+ const sessions = /* @__PURE__ */ new Map();
211
+ const focusedId = ref(null);
212
+ function createSession(conversationId) {
213
+ const existing = sessions.get(conversationId);
214
+ if (existing) return existing;
215
+ const session = {
216
+ id: conversationId,
217
+ messageHandler: useMessageHandler()
218
+ };
219
+ sessions.set(conversationId, session);
220
+ return session;
221
+ }
222
+ function getSession(conversationId) {
223
+ return sessions.get(conversationId);
224
+ }
225
+ function setActiveConversation(conversationId) {
226
+ if (conversationId === null) {
227
+ focusedId.value = null;
228
+ return;
229
+ }
230
+ createSession(conversationId);
231
+ focusedId.value = conversationId;
232
+ }
233
+ function removeSession(conversationId) {
234
+ sessions.delete(conversationId);
235
+ if (focusedId.value === conversationId) {
236
+ focusedId.value = null;
237
+ }
238
+ }
239
+ const activeSession = computed(() => {
240
+ const id = focusedId.value;
241
+ return id !== null ? sessions.get(id) ?? null : null;
242
+ });
243
+ const messageHandler = computed(
244
+ () => activeSession.value?.messageHandler ?? null
245
+ );
246
+ return {
247
+ activeSession,
248
+ messageHandler,
249
+ createSession,
250
+ getSession,
251
+ setActiveConversation,
252
+ removeSession
253
+ };
254
+ }
255
+ const defaultEndpoints = {
256
+ files: (storeId) => `search-stores/${storeId}/files`,
257
+ file: (storeId, fileId) => `search-stores/${storeId}/files/${fileId}`,
258
+ filePublicUrl: (storeId, fileId) => `search-stores/${storeId}/files/${fileId}/public-urls?ttl=1h`,
259
+ workflowRun: (storeId, workflowId) => `search-stores/${storeId}/workflows/${workflowId}/runs`,
260
+ workflowRunEventStream: (storeId, workflowId, runId) => `search-stores/${storeId}/workflows/${workflowId}/runs/${runId}/event-stream`
261
+ };
262
+ class SdkError extends Error {
263
+ constructor(code, message, details) {
264
+ super(message);
265
+ this.code = code;
266
+ this.details = details;
267
+ this.name = "SdkError";
268
+ }
269
+ }
270
+ const DEFAULT_CONSTRAINTS = {
271
+ allowedMimeTypes: [
272
+ "application/pdf",
273
+ "text/plain",
274
+ "text/csv",
275
+ "application/msword",
276
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
277
+ "application/vnd.ms-excel",
278
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
279
+ "application/vnd.ms-powerpoint",
280
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
281
+ "image/png",
282
+ "image/jpeg",
283
+ "image/gif",
284
+ "image/webp"
285
+ ],
286
+ allowedExtensions: [
287
+ ".pdf",
288
+ ".txt",
289
+ ".csv",
290
+ ".doc",
291
+ ".docx",
292
+ ".xls",
293
+ ".xlsx",
294
+ ".ppt",
295
+ ".pptx",
296
+ ".png",
297
+ ".jpg",
298
+ ".jpeg",
299
+ ".gif",
300
+ ".webp"
301
+ ],
302
+ maxFileSizeBytes: 100 * 1024 * 1024,
303
+ maxFilesPerUpload: 20
304
+ };
305
+ const DEFAULT_RETRY_POLICY = {
306
+ maxRetries: 2,
307
+ delayMs: 1e3,
308
+ backoffMultiplier: 2,
309
+ retryableStatusCodes: [408, 429, 500, 502, 503]
310
+ };
311
+ const FILE_ICON_SVG = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/%3E%3Cpath d='M14 2v4a2 2 0 0 0 2 2h4'/%3E%3C/svg%3E`;
312
+ const FILE_ICONS = {
313
+ _default: FILE_ICON_SVG,
314
+ "application/pdf": `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128' viewBox='0 0 24 24' fill='none' stroke='%23ef4444' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/%3E%3Cpath d='M14 2v4a2 2 0 0 0 2 2h4'/%3E%3Cpath d='M10 12h4'/%3E%3Cpath d='M10 16h4'/%3E%3C/svg%3E`,
315
+ "text/plain": `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/%3E%3Cpath d='M14 2v4a2 2 0 0 0 2 2h4'/%3E%3Cpath d='M10 12h4'/%3E%3Cpath d='M10 16h4'/%3E%3C/svg%3E`,
316
+ "text/csv": `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128' viewBox='0 0 24 24' fill='none' stroke='%2322c55e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/%3E%3Cpath d='M14 2v4a2 2 0 0 0 2 2h4'/%3E%3Cpath d='M8 15h2'/%3E%3Cpath d='M14 15h2'/%3E%3Cpath d='M8 11h2'/%3E%3Cpath d='M14 11h2'/%3E%3C/svg%3E`
317
+ };
318
+ const DB_VERSION = 1;
319
+ const ACTIVE_UPLOAD_STATUSES = /* @__PURE__ */ new Set(["queued", "validating", "uploading"]);
320
+ const ACTIVE_OR_PROCESSING = /* @__PURE__ */ new Set([
321
+ "queued",
322
+ "validating",
323
+ "uploading",
324
+ "processing"
325
+ ]);
326
+ function now() {
327
+ return (/* @__PURE__ */ new Date()).toISOString();
328
+ }
329
+ function extensionOf(name) {
330
+ const dot = name.lastIndexOf(".");
331
+ return dot === -1 ? "" : name.slice(dot).toLowerCase();
332
+ }
333
+ function debounce(fn, ms) {
334
+ let timer = null;
335
+ return () => {
336
+ if (timer) clearTimeout(timer);
337
+ timer = setTimeout(fn, ms);
338
+ };
339
+ }
340
+ class Semaphore {
341
+ permits;
342
+ queue = [];
343
+ constructor(permits) {
344
+ this.permits = permits;
345
+ }
346
+ acquire() {
347
+ if (this.permits > 0) {
348
+ this.permits--;
349
+ return Promise.resolve();
350
+ }
351
+ return new Promise((resolve) => {
352
+ this.queue.push(resolve);
353
+ });
354
+ }
355
+ release() {
356
+ if (this.queue.length > 0) {
357
+ this.queue.shift()();
358
+ } else {
359
+ this.permits++;
360
+ }
361
+ }
362
+ }
363
+ function iconFallback(file) {
364
+ const dataUrl = FILE_ICONS[file.type] || FILE_ICONS._default;
365
+ return {
366
+ dataUrl: dataUrl ?? "",
367
+ width: 128,
368
+ height: 128,
369
+ generatedAt: now(),
370
+ strategy: "icon-fallback"
371
+ };
372
+ }
373
+ function generateCanvasThumbnail(file, maxW = 128, maxH = 128) {
374
+ return new Promise((resolve, reject) => {
375
+ if (typeof document === "undefined") {
376
+ reject(new Error("Canvas is not available in this environment"));
377
+ return;
378
+ }
379
+ const url = URL.createObjectURL(file);
380
+ const img = new Image();
381
+ img.onload = () => {
382
+ let w = img.naturalWidth;
383
+ let h = img.naturalHeight;
384
+ const ratio = Math.min(maxW / w, maxH / h, 1);
385
+ w = Math.round(w * ratio);
386
+ h = Math.round(h * ratio);
387
+ const canvas = document.createElement("canvas");
388
+ canvas.width = w;
389
+ canvas.height = h;
390
+ const ctx = canvas.getContext("2d");
391
+ ctx.drawImage(img, 0, 0, w, h);
392
+ const dataUrl = canvas.toDataURL("image/png");
393
+ URL.revokeObjectURL(url);
394
+ resolve({
395
+ dataUrl,
396
+ width: w,
397
+ height: h,
398
+ generatedAt: now(),
399
+ strategy: "canvas"
400
+ });
401
+ };
402
+ img.onerror = () => {
403
+ URL.revokeObjectURL(url);
404
+ reject(new Error("Failed to load image for thumbnail generation"));
405
+ };
406
+ img.src = url;
407
+ });
408
+ }
409
+ async function generateThumbnail(file) {
410
+ if (file.type.startsWith("image/")) {
411
+ try {
412
+ return await generateCanvasThumbnail(file);
413
+ } catch {
414
+ return iconFallback(file);
415
+ }
416
+ }
417
+ return iconFallback(file);
418
+ }
419
+ function validateFileConstraints(file, constraints) {
420
+ const ext = extensionOf(file.name);
421
+ if (file.size > constraints.maxFileSizeBytes) {
422
+ return {
423
+ code: "FILE_TOO_LARGE",
424
+ message: `File "${file.name}" exceeds the maximum size of ${constraints.maxFileSizeBytes} bytes.`,
425
+ occurredAt: now()
426
+ };
427
+ }
428
+ if (constraints.allowedMimeTypes.length > 0 && !constraints.allowedMimeTypes.includes(file.type)) {
429
+ return {
430
+ code: "FILE_TYPE_NOT_ALLOWED",
431
+ message: `MIME type "${file.type}" is not allowed for file "${file.name}".`,
432
+ occurredAt: now()
433
+ };
434
+ }
435
+ if (constraints.allowedExtensions.length > 0 && ext !== "" && !constraints.allowedExtensions.includes(ext)) {
436
+ return {
437
+ code: "FILE_TYPE_NOT_ALLOWED",
438
+ message: `Extension "${ext}" is not allowed for file "${file.name}".`,
439
+ occurredAt: now()
440
+ };
441
+ }
442
+ return null;
443
+ }
444
+ async function runCustomValidators(file, constraints, validators) {
445
+ for (const validator of validators) {
446
+ const result = await validator(file, constraints);
447
+ if (!result.valid) {
448
+ return {
449
+ code: "VALIDATION_FAILED",
450
+ message: result.reason ?? `Validation failed for "${file.name}".`,
451
+ occurredAt: now()
452
+ };
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+ function isRetryable(err, policy) {
458
+ if (!policy.retryableStatusCodes?.length) return true;
459
+ const status = err?.httpStatus ?? err?.status ?? err?.response?.status;
460
+ return status != null && policy.retryableStatusCodes.includes(status);
461
+ }
462
+ async function withRetry(fn, policy, signal) {
463
+ let lastError;
464
+ for (let attempt = 0; attempt <= policy.maxRetries; attempt++) {
465
+ if (signal.aborted)
466
+ throw new SdkError("UPLOAD_ABORTED", "Upload cancelled.");
467
+ try {
468
+ return await fn();
469
+ } catch (err) {
470
+ lastError = err;
471
+ if (signal.aborted) throw err;
472
+ if (attempt < policy.maxRetries && isRetryable(err, policy)) {
473
+ const delay = policy.delayMs * Math.pow(policy.backoffMultiplier ?? 2, attempt);
474
+ await new Promise((r) => setTimeout(r, delay));
475
+ }
476
+ }
477
+ }
478
+ throw lastError;
479
+ }
480
+ function openDB(dbName, storeName) {
481
+ return new Promise((resolve, reject) => {
482
+ const request = indexedDB.open(dbName, DB_VERSION);
483
+ request.onupgradeneeded = () => {
484
+ const db = request.result;
485
+ if (!db.objectStoreNames.contains(storeName)) {
486
+ db.createObjectStore(storeName, { keyPath: "id" });
487
+ }
488
+ };
489
+ request.onsuccess = () => resolve(request.result);
490
+ request.onerror = () => reject(request.error);
491
+ });
492
+ }
493
+ function persistEntries(db, storeName, entries, filter) {
494
+ const filtered = filter ? entries.filter(filter) : entries;
495
+ const tx = db.transaction(storeName, "readwrite");
496
+ const store = tx.objectStore(storeName);
497
+ for (const entry of filtered) {
498
+ store.put(structuredClone(toRaw(entry)));
499
+ }
500
+ return new Promise((resolve, reject) => {
501
+ tx.oncomplete = () => resolve();
502
+ tx.onerror = () => reject(tx.error);
503
+ });
504
+ }
505
+ function deletePersistedEntry(db, storeName, id) {
506
+ const tx = db.transaction(storeName, "readwrite");
507
+ tx.objectStore(storeName).delete(id);
508
+ return new Promise((resolve, reject) => {
509
+ tx.oncomplete = () => resolve();
510
+ tx.onerror = () => reject(tx.error);
511
+ });
512
+ }
513
+ function restoreEntries(db, storeName, maxAge) {
514
+ const tx = db.transaction(storeName, "readonly");
515
+ const request = tx.objectStore(storeName).getAll();
516
+ return new Promise((resolve, reject) => {
517
+ request.onsuccess = () => {
518
+ let entries = request.result;
519
+ if (maxAge) {
520
+ const cutoff = Date.now() - maxAge;
521
+ entries = entries.filter(
522
+ (e) => new Date(e.queuedAt).getTime() > cutoff
523
+ );
524
+ }
525
+ resolve(entries.map(rehydrateEntry));
526
+ };
527
+ request.onerror = () => reject(request.error);
528
+ });
529
+ }
530
+ function purgeStore(db, storeName) {
531
+ const tx = db.transaction(storeName, "readwrite");
532
+ tx.objectStore(storeName).clear();
533
+ return new Promise((resolve, reject) => {
534
+ tx.oncomplete = () => resolve();
535
+ tx.onerror = () => reject(tx.error);
536
+ });
537
+ }
538
+ function rehydrateEntry(entry) {
539
+ if (entry.status === "uploading" || entry.status === "validating" || entry.status === "queued") {
540
+ return {
541
+ ...entry,
542
+ status: "failed",
543
+ error: {
544
+ code: "UPLOAD_INTERRUPTED",
545
+ message: "Upload was interrupted by a page reload or browser close.",
546
+ occurredAt: now()
547
+ }
548
+ };
549
+ }
550
+ if (entry.status === "processing") {
551
+ return {
552
+ ...entry,
553
+ status: "failed",
554
+ error: {
555
+ code: "PROCESSING_INTERRUPTED",
556
+ message: "Workflow processing was interrupted before completion.",
557
+ occurredAt: now()
558
+ }
559
+ };
560
+ }
561
+ if (entry.status === "removing") {
562
+ return {
563
+ ...entry,
564
+ status: "failed",
565
+ error: {
566
+ code: "REMOVE_INTERRUPTED",
567
+ message: "File removal was interrupted.",
568
+ occurredAt: now()
569
+ }
570
+ };
571
+ }
572
+ return entry;
573
+ }
574
+ function normalizePersistConfig(opt) {
575
+ if (!opt) return null;
576
+ if (opt === true) return {};
577
+ return opt;
578
+ }
579
+ function deriveStateKey(groupId, fileName, existingKeys) {
580
+ const base = `${groupId}::${fileName}`;
581
+ if (!existingKeys.has(base)) return base;
582
+ let suffix = 2;
583
+ while (existingKeys.has(`${base}::${suffix}`)) suffix++;
584
+ return `${base}::${suffix}`;
585
+ }
586
+ function useFileUpload(options) {
587
+ const { client, workflowId } = options;
588
+ let storeId = options.storeId ?? "";
589
+ const endpoints = options.endpoints ?? defaultEndpoints;
590
+ const retryPolicy = options.retryPolicy ?? DEFAULT_RETRY_POLICY;
591
+ const constraints = { ...DEFAULT_CONSTRAINTS };
592
+ const validators = options.validators ?? [];
593
+ const maxConcurrency = options.maxConcurrency ?? 3;
594
+ const files = ref(/* @__PURE__ */ new Map());
595
+ const workflowRun = ref(null);
596
+ const isRestoring = ref(false);
597
+ const uploadControllers = /* @__PURE__ */ new Map();
598
+ const semaphore = new Semaphore(maxConcurrency);
599
+ const persistConfig = normalizePersistConfig(options.persist);
600
+ let db = null;
601
+ const persistDbName = persistConfig?.dbName ?? "file-upload-sdk";
602
+ const persistStoreName = persistConfig?.storeName ?? "file-entries";
603
+ const persistDebounceMs = persistConfig?.debounceMs ?? 300;
604
+ const schedulePersist = debounce(async () => {
605
+ if (!db) return;
606
+ try {
607
+ const entries = [...files.value.values()];
608
+ await persistEntries(
609
+ db,
610
+ persistStoreName,
611
+ entries,
612
+ persistConfig?.filter
613
+ );
614
+ emit({ type: "state:persisted", entryCount: entries.length });
615
+ } catch (err) {
616
+ emit({ type: "state:persist-error", error: err });
617
+ }
618
+ }, persistDebounceMs);
619
+ function persistIfEnabled() {
620
+ if (persistConfig && db) schedulePersist();
621
+ }
622
+ function emit(event) {
623
+ options.onEvent?.(event);
624
+ }
625
+ function setEntry(entry) {
626
+ files.value.set(entry.id, entry);
627
+ persistIfEnabled();
628
+ }
629
+ function updateEntry(id, updates) {
630
+ const entry = files.value.get(id);
631
+ if (!entry) return;
632
+ Object.assign(entry, updates);
633
+ persistIfEnabled();
634
+ }
635
+ function getSupportedFileTypes() {
636
+ return { ...constraints };
637
+ }
638
+ async function validateAndQueueBatch(batch, groupId, mergedMeta) {
639
+ for (const { file, stateKey } of batch) {
640
+ const constraintErr = validateFileConstraints(file, constraints);
641
+ if (constraintErr) {
642
+ const entry2 = createEntry(stateKey, groupId, file, mergedMeta);
643
+ entry2.status = "failed";
644
+ entry2.error = constraintErr;
645
+ setEntry(entry2);
646
+ emit({ type: "file:failed", fileId: stateKey, error: constraintErr });
647
+ continue;
648
+ }
649
+ let thumbnail = null;
650
+ try {
651
+ thumbnail = await generateThumbnail(file);
652
+ } catch (err) {
653
+ emit({
654
+ type: "file:thumbnail-failed",
655
+ fileId: stateKey,
656
+ reason: err.message
657
+ });
658
+ }
659
+ const entry = createEntry(stateKey, groupId, file, mergedMeta, thumbnail);
660
+ setEntry(entry);
661
+ emit({ type: "file:queued", fileId: stateKey, entry });
662
+ }
663
+ }
664
+ async function runValidationPhase(validBatch) {
665
+ for (const { file, stateKey } of validBatch) {
666
+ updateEntry(stateKey, { status: "validating" });
667
+ emit({ type: "file:validating", fileId: stateKey });
668
+ const validationErr = await runCustomValidators(
669
+ file,
670
+ constraints,
671
+ validators
672
+ );
673
+ if (validationErr) {
674
+ updateEntry(stateKey, { status: "failed", error: validationErr });
675
+ emit({ type: "file:failed", fileId: stateKey, error: validationErr });
676
+ }
677
+ }
678
+ }
679
+ function handleUploadSuccess(uploadReady, response) {
680
+ const completedAt = now();
681
+ const results = Array.isArray(response) ? response : [];
682
+ for (let i = 0; i < uploadReady.length; i++) {
683
+ const { stateKey } = uploadReady[i];
684
+ const entry = files.value.get(stateKey);
685
+ const durationMs = new Date(completedAt).getTime() - new Date(entry.uploadStartedAt).getTime();
686
+ const remoteResult = results[i];
687
+ const remoteFileId = remoteResult?.id ?? null;
688
+ const documentId = remoteResult?.documentId ?? null;
689
+ if (remoteFileId) {
690
+ updateEntry(stateKey, {
691
+ status: "success",
692
+ progress: 100,
693
+ remoteFileId,
694
+ uploadCompletedAt: completedAt,
695
+ uploadDurationMs: durationMs
696
+ });
697
+ if (documentId) {
698
+ updateFileMetadata(stateKey, { documentId });
699
+ }
700
+ emit({
701
+ type: "file:success",
702
+ fileId: stateKey,
703
+ remoteFileId,
704
+ durationMs
705
+ });
706
+ } else {
707
+ const err = {
708
+ code: "UPLOAD_FAILED",
709
+ message: `No remote ID returned for "${entry.fileName}".`,
710
+ occurredAt: completedAt
711
+ };
712
+ updateEntry(stateKey, {
713
+ status: "failed",
714
+ error: err,
715
+ uploadCompletedAt: completedAt,
716
+ uploadDurationMs: durationMs
717
+ });
718
+ emit({ type: "file:failed", fileId: stateKey, error: err });
719
+ }
720
+ }
721
+ }
722
+ function handleUploadError(uploadReady, err, mergedSignal, batch, groupId) {
723
+ const completedAt = now();
724
+ if (mergedSignal.aborted) {
725
+ for (const { stateKey } of uploadReady) {
726
+ transitionToCancelled(stateKey);
727
+ }
728
+ uploadControllers.delete(groupId);
729
+ const result = buildUploadResult(groupId, batch);
730
+ emit({
731
+ type: "upload:complete",
732
+ groupId,
733
+ results: result.results
734
+ });
735
+ throw new SdkError(
736
+ "UPLOAD_ABORTED",
737
+ `Upload "${groupId}" was cancelled.`
738
+ );
739
+ }
740
+ const uploadErr = {
741
+ code: "UPLOAD_FAILED",
742
+ message: err.message ?? "Upload failed.",
743
+ httpStatus: err?.httpStatus ?? err?.status,
744
+ occurredAt: completedAt
745
+ };
746
+ for (const { stateKey } of uploadReady) {
747
+ const entry = files.value.get(stateKey);
748
+ const durationMs = entry.uploadStartedAt ? new Date(completedAt).getTime() - new Date(entry.uploadStartedAt).getTime() : null;
749
+ updateEntry(stateKey, {
750
+ status: "failed",
751
+ error: uploadErr,
752
+ uploadCompletedAt: completedAt,
753
+ uploadDurationMs: durationMs
754
+ });
755
+ emit({ type: "file:failed", fileId: stateKey, error: uploadErr });
756
+ }
757
+ }
758
+ async function upload(params) {
759
+ const fileList = Array.isArray(params.files) ? params.files : [params.files];
760
+ if (fileList.length > constraints.maxFilesPerUpload) {
761
+ throw new SdkError(
762
+ "MAX_FILES_EXCEEDED",
763
+ `Cannot upload ${fileList.length} files — maximum is ${constraints.maxFilesPerUpload}.`
764
+ );
765
+ }
766
+ const mergedMeta = {
767
+ ...params.metadata ?? {},
768
+ ...params.forwardedProps ?? {}
769
+ };
770
+ const existingKeys = new Set(files.value.keys());
771
+ const batch = [];
772
+ for (const file of fileList) {
773
+ const stateKey = deriveStateKey(params.id, file.name, existingKeys);
774
+ existingKeys.add(stateKey);
775
+ batch.push({ file, stateKey });
776
+ }
777
+ await validateAndQueueBatch(batch, params.id, mergedMeta);
778
+ const validBatch = batch.filter((b) => {
779
+ const entry = files.value.get(b.stateKey);
780
+ return entry && entry.status === "queued";
781
+ });
782
+ if (validBatch.length === 0) {
783
+ return buildUploadResult(params.id, batch);
784
+ }
785
+ await runValidationPhase(validBatch);
786
+ const uploadReady = validBatch.filter((b) => {
787
+ const entry = files.value.get(b.stateKey);
788
+ return entry && entry.status === "validating";
789
+ });
790
+ if (uploadReady.length === 0) {
791
+ return buildUploadResult(params.id, batch);
792
+ }
793
+ const formData = new FormData();
794
+ for (const { file } of uploadReady) {
795
+ formData.append("files", file, file.name);
796
+ }
797
+ const controller = new AbortController();
798
+ uploadControllers.set(params.id, controller);
799
+ const mergedSignal = params.signal ? AbortSignal.any([controller.signal, params.signal]) : controller.signal;
800
+ const uploadStarted = now();
801
+ for (const { stateKey } of uploadReady) {
802
+ updateEntry(stateKey, {
803
+ status: "uploading",
804
+ uploadStartedAt: uploadStarted,
805
+ attemptCount: (files.value.get(stateKey)?.attemptCount ?? 0) + 1,
806
+ lastAttemptAt: uploadStarted
807
+ });
808
+ emit({
809
+ type: "file:uploading",
810
+ fileId: stateKey,
811
+ attemptCount: files.value.get(stateKey).attemptCount
812
+ });
813
+ }
814
+ const totalBytes = uploadReady.reduce(
815
+ (sum, { file }) => sum + file.size,
816
+ 0
817
+ );
818
+ const onUploadProgress = (event) => {
819
+ const ratio = event.total > 0 ? event.loaded / event.total : 0;
820
+ const loadedBytes = ratio * totalBytes;
821
+ for (const { file, stateKey } of uploadReady) {
822
+ const fileShare = totalBytes > 0 ? loadedBytes * (file.size / totalBytes) : 0;
823
+ const progress = Math.min(
824
+ 100,
825
+ Math.round(fileShare / Math.max(file.size, 1) * 100)
826
+ );
827
+ updateEntry(stateKey, { progress });
828
+ emit({ type: "file:progress", fileId: stateKey, progress });
829
+ }
830
+ };
831
+ await semaphore.acquire();
832
+ try {
833
+ const response = await withRetry(
834
+ () => client.post(endpoints.files(storeId), formData, {
835
+ signal: mergedSignal,
836
+ onUploadProgress
837
+ }),
838
+ retryPolicy,
839
+ mergedSignal
840
+ );
841
+ handleUploadSuccess(uploadReady, response);
842
+ } catch (err) {
843
+ handleUploadError(uploadReady, err, mergedSignal, batch, params.id);
844
+ } finally {
845
+ semaphore.release();
846
+ uploadControllers.delete(params.id);
847
+ }
848
+ const result = buildUploadResult(params.id, batch);
849
+ emit({
850
+ type: "upload:complete",
851
+ groupId: params.id,
852
+ results: result.results
853
+ });
854
+ return result;
855
+ }
856
+ function createEntry(id, groupId, file, metadata, thumbnail = null) {
857
+ return {
858
+ id,
859
+ groupId,
860
+ fileName: file.name,
861
+ fileSize: file.size,
862
+ fileType: file.type,
863
+ status: "queued",
864
+ progress: 0,
865
+ queuedAt: now(),
866
+ uploadStartedAt: null,
867
+ uploadCompletedAt: null,
868
+ cancelledAt: null,
869
+ removedAt: null,
870
+ uploadDurationMs: null,
871
+ remoteFileId: null,
872
+ error: null,
873
+ attemptCount: 0,
874
+ lastAttemptAt: null,
875
+ thumbnail,
876
+ metadata: { ...metadata }
877
+ };
878
+ }
879
+ function buildUploadResult(groupId, batch) {
880
+ return {
881
+ id: groupId,
882
+ results: batch.map(({ stateKey }) => {
883
+ const entry = files.value.get(stateKey);
884
+ return {
885
+ fileId: stateKey,
886
+ success: entry.status === "success",
887
+ status: entry.status,
888
+ remoteFileId: entry.remoteFileId ?? void 0,
889
+ error: entry.error ?? void 0,
890
+ uploadDate: entry.uploadCompletedAt ?? entry.queuedAt,
891
+ metadata: entry.metadata
892
+ };
893
+ })
894
+ };
895
+ }
896
+ function cancel(groupId) {
897
+ const controller = uploadControllers.get(groupId);
898
+ if (controller) {
899
+ controller.abort();
900
+ uploadControllers.delete(groupId);
901
+ }
902
+ for (const [, entry] of files.value) {
903
+ if (entry.groupId === groupId) {
904
+ transitionToCancelled(entry.id);
905
+ }
906
+ }
907
+ }
908
+ function transitionToCancelled(id) {
909
+ const entry = files.value.get(id);
910
+ if (!entry || !ACTIVE_UPLOAD_STATUSES.has(entry.status)) return;
911
+ const ts = now();
912
+ updateEntry(id, {
913
+ status: "cancelled",
914
+ cancelledAt: ts,
915
+ error: {
916
+ code: "UPLOAD_ABORTED",
917
+ message: "Upload cancelled.",
918
+ occurredAt: ts
919
+ }
920
+ });
921
+ emit({ type: "file:cancelled", fileId: id });
922
+ }
923
+ async function removeFile(fileId, opts) {
924
+ const removeFromState = opts?.removeFromState ?? true;
925
+ const removeFromRemote = opts?.removeFromRemote ?? true;
926
+ const entry = files.value.get(fileId);
927
+ if (removeFromRemote && entry?.remoteFileId) {
928
+ updateEntry(fileId, { status: "removing" });
929
+ try {
930
+ await client.delete(endpoints.file(storeId, entry.remoteFileId));
931
+ } catch (err) {
932
+ const status = err?.httpStatus ?? err?.status ?? err?.response?.status;
933
+ if (status === 404) ;
934
+ else {
935
+ const removeErr = {
936
+ code: "REMOVE_FAILED",
937
+ message: err.message ?? "File removal failed.",
938
+ occurredAt: now()
939
+ };
940
+ updateEntry(fileId, { status: "failed", error: removeErr });
941
+ throw new SdkError("REMOVE_FAILED", removeErr.message);
942
+ }
943
+ }
944
+ }
945
+ if (removeFromState) {
946
+ files.value.delete(fileId);
947
+ if (db) {
948
+ try {
949
+ await deletePersistedEntry(db, persistStoreName, fileId);
950
+ } catch {
951
+ }
952
+ }
953
+ emit({ type: "file:removed", fileId });
954
+ } else if (entry) {
955
+ updateEntry(fileId, { status: "removed", removedAt: now() });
956
+ emit({ type: "file:removed", fileId });
957
+ }
958
+ }
959
+ function getFileState(id) {
960
+ return files.value.get(id);
961
+ }
962
+ function updateFileMetadata(id, meta) {
963
+ const entry = files.value.get(id);
964
+ if (!entry) return;
965
+ Object.assign(entry.metadata, meta);
966
+ persistIfEnabled();
967
+ }
968
+ function clearState() {
969
+ files.value.clear();
970
+ if (db) {
971
+ purgeStore(db, persistStoreName).catch(() => {
972
+ });
973
+ }
974
+ }
975
+ function scopeBy(metadataKey, value) {
976
+ const scopedFiles = computed(
977
+ () => [...files.value.values()].filter(
978
+ (f) => f.metadata[metadataKey] === value
979
+ )
980
+ );
981
+ const progress = computed(() => {
982
+ const active = scopedFiles.value.filter(
983
+ (f) => ACTIVE_OR_PROCESSING.has(f.status)
984
+ );
985
+ if (!active.length) return 100;
986
+ return Math.round(
987
+ active.reduce((sum, f) => sum + f.progress, 0) / active.length
988
+ );
989
+ });
990
+ const isUploading = computed(
991
+ () => scopedFiles.value.some((f) => ACTIVE_UPLOAD_STATUSES.has(f.status))
992
+ );
993
+ const hasErrors = computed(
994
+ () => scopedFiles.value.some((f) => f.status === "failed")
995
+ );
996
+ return {
997
+ files: scopedFiles,
998
+ count: computed(() => scopedFiles.value.length),
999
+ progress,
1000
+ isUploading,
1001
+ hasErrors,
1002
+ clear: () => {
1003
+ for (const entry of scopedFiles.value) {
1004
+ files.value.delete(entry.id);
1005
+ if (db) {
1006
+ deletePersistedEntry(db, persistStoreName, entry.id).catch(
1007
+ () => {
1008
+ }
1009
+ );
1010
+ }
1011
+ }
1012
+ },
1013
+ cancel: () => {
1014
+ const groupIds = new Set(
1015
+ scopedFiles.value.map((e) => e.groupId)
1016
+ );
1017
+ for (const gid of groupIds) cancel(gid);
1018
+ },
1019
+ retry: (fileId) => {
1020
+ const entry = files.value.get(fileId);
1021
+ if (entry && entry.status === "failed") {
1022
+ updateEntry(fileId, { status: "queued", error: null, progress: 0 });
1023
+ }
1024
+ }
1025
+ };
1026
+ }
1027
+ async function runWorkflow(fileIds) {
1028
+ const source = fileIds.map((id) => `files://${id}`);
1029
+ const input = { source };
1030
+ const controller = new AbortController();
1031
+ const involvedStateKeys = [...files.value.values()].filter((e) => e.remoteFileId && fileIds.includes(e.remoteFileId)).map((e) => e.id);
1032
+ let sseEventCount = 0;
1033
+ for (const key of involvedStateKeys) {
1034
+ updateEntry(key, { status: "processing", progress: 0 });
1035
+ }
1036
+ const runResponse = await client.post(
1037
+ endpoints.workflowRun(storeId, workflowId),
1038
+ { input },
1039
+ { signal: controller.signal }
1040
+ );
1041
+ const runId = runResponse.id;
1042
+ workflowRun.value = {
1043
+ runId,
1044
+ workflowId,
1045
+ storeId,
1046
+ status: "connecting",
1047
+ events: [],
1048
+ error: null,
1049
+ startedAt: now(),
1050
+ completedAt: null
1051
+ };
1052
+ emit({ type: "workflow:started", runId });
1053
+ const sseEndpoint = endpoints.workflowRunEventStream(
1054
+ storeId,
1055
+ workflowId,
1056
+ runId
1057
+ );
1058
+ const sseStream = client.stream(sseEndpoint, {
1059
+ signal: controller.signal
1060
+ });
1061
+ sseStream.subscribe((event) => {
1062
+ if (!workflowRun.value) return;
1063
+ let data;
1064
+ try {
1065
+ data = JSON.parse(event.data);
1066
+ } catch {
1067
+ data = {};
1068
+ }
1069
+ const runEvent = {
1070
+ id: data.id ?? "",
1071
+ status: data.status ?? event.event
1072
+ };
1073
+ workflowRun.value.events.push(runEvent);
1074
+ emit({ type: "workflow:event", event: runEvent });
1075
+ if (data.status === "completed") {
1076
+ workflowRun.value.status = "completed";
1077
+ workflowRun.value.completedAt = now();
1078
+ for (const key of involvedStateKeys) {
1079
+ updateEntry(key, { status: "success", progress: 100 });
1080
+ }
1081
+ emit({ type: "workflow:completed", runId });
1082
+ } else if (data.status === "failed" || data.status === "error") {
1083
+ workflowRun.value.status = "failed";
1084
+ workflowRun.value.completedAt = now();
1085
+ workflowRun.value.error = {
1086
+ code: "WORKFLOW_RUN_FAILED",
1087
+ message: `Workflow run ${runId} failed.`
1088
+ };
1089
+ const workflowErr = {
1090
+ code: "UPLOAD_FAILED",
1091
+ message: `Workflow run ${runId} failed.`,
1092
+ occurredAt: now()
1093
+ };
1094
+ for (const key of involvedStateKeys) {
1095
+ updateEntry(key, { status: "failed", error: workflowErr });
1096
+ }
1097
+ emit({
1098
+ type: "workflow:failed",
1099
+ runId,
1100
+ error: workflowRun.value.error
1101
+ });
1102
+ } else {
1103
+ workflowRun.value.status = "running";
1104
+ sseEventCount++;
1105
+ const estimated = Math.round(100 * (1 - 1 / (1 + sseEventCount * 0.3)));
1106
+ for (const key of involvedStateKeys) {
1107
+ updateEntry(key, { progress: estimated });
1108
+ emit({ type: "file:progress", fileId: key, progress: estimated });
1109
+ }
1110
+ }
1111
+ });
1112
+ return {
1113
+ close() {
1114
+ controller.abort();
1115
+ sseStream.close();
1116
+ if (workflowRun.value && !["completed", "failed", "cancelled"].includes(
1117
+ workflowRun.value.status
1118
+ )) {
1119
+ workflowRun.value.status = "cancelled";
1120
+ workflowRun.value.completedAt = now();
1121
+ }
1122
+ }
1123
+ };
1124
+ }
1125
+ async function restoreState() {
1126
+ if (!persistConfig) return;
1127
+ isRestoring.value = true;
1128
+ try {
1129
+ if (!db) db = await openDB(persistDbName, persistStoreName);
1130
+ const entries = await restoreEntries(
1131
+ db,
1132
+ persistStoreName,
1133
+ persistConfig.maxAge
1134
+ );
1135
+ for (const entry of entries) {
1136
+ files.value.set(entry.id, entry);
1137
+ }
1138
+ emit({ type: "state:restored", entryCount: entries.length });
1139
+ } catch (err) {
1140
+ emit({ type: "state:persist-error", error: err });
1141
+ } finally {
1142
+ isRestoring.value = false;
1143
+ }
1144
+ }
1145
+ async function purgePersistedState() {
1146
+ if (!db) return;
1147
+ try {
1148
+ await purgeStore(db, persistStoreName);
1149
+ } catch (err) {
1150
+ emit({ type: "state:persist-error", error: err });
1151
+ }
1152
+ }
1153
+ if (persistConfig) {
1154
+ openDB(persistDbName, persistStoreName).then((database) => {
1155
+ db = database;
1156
+ return restoreState();
1157
+ }).catch((err) => {
1158
+ emit({ type: "state:persist-error", error: err });
1159
+ });
1160
+ }
1161
+ function setStoreId(id) {
1162
+ storeId = id;
1163
+ }
1164
+ async function getPublicUrl(fileId) {
1165
+ const res = await client.post(
1166
+ endpoints.filePublicUrl(storeId, fileId),
1167
+ {}
1168
+ );
1169
+ return res.url;
1170
+ }
1171
+ return {
1172
+ getSupportedFileTypes,
1173
+ upload,
1174
+ cancel,
1175
+ removeFile,
1176
+ files,
1177
+ getFileState,
1178
+ updateFileMetadata,
1179
+ clearState,
1180
+ setStoreId,
1181
+ getPublicUrl,
1182
+ scopeBy,
1183
+ workflowRun,
1184
+ runWorkflow,
1185
+ isRestoring,
1186
+ restoreState,
1187
+ purgePersistedState
1188
+ };
1189
+ }
1190
+ function buildHttpClient(baseUrl, defaultHeaders) {
1191
+ return {
1192
+ async get(url, config) {
1193
+ const fullUrl = `${baseUrl}/${url}`;
1194
+ const res = await fetch(fullUrl, {
1195
+ method: "GET",
1196
+ signal: config?.signal,
1197
+ headers: defaultHeaders,
1198
+ credentials: "include"
1199
+ });
1200
+ if (!res.ok) throw new Error(`GET ${fullUrl} failed: ${res.status}`);
1201
+ return res.json();
1202
+ },
1203
+ async post(url, body, config) {
1204
+ const fullUrl = `${baseUrl}/${url}`;
1205
+ const headers = { ...defaultHeaders };
1206
+ let resolvedBody;
1207
+ if (body instanceof FormData) {
1208
+ resolvedBody = body;
1209
+ } else {
1210
+ headers["Content-Type"] = "application/json";
1211
+ resolvedBody = JSON.stringify(body);
1212
+ }
1213
+ const res = await fetch(fullUrl, {
1214
+ method: "POST",
1215
+ signal: config?.signal,
1216
+ headers,
1217
+ body: resolvedBody,
1218
+ credentials: "include"
1219
+ });
1220
+ if (!res.ok) throw new Error(`POST ${fullUrl} failed: ${res.status}`);
1221
+ return res.json();
1222
+ },
1223
+ async delete(url, config) {
1224
+ const fullUrl = `${baseUrl}/${url}`;
1225
+ const res = await fetch(fullUrl, {
1226
+ method: "DELETE",
1227
+ signal: config?.signal,
1228
+ headers: defaultHeaders,
1229
+ credentials: "include"
1230
+ });
1231
+ if (!res.ok) throw new Error(`DELETE ${fullUrl} failed: ${res.status}`);
1232
+ return res.json();
1233
+ },
1234
+ stream(url, config) {
1235
+ const controller = new AbortController();
1236
+ const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
1237
+ const listeners = [];
1238
+ let closed = false;
1239
+ const fullUrl = `${baseUrl}/${url}`;
1240
+ const headers = {
1241
+ Accept: "text/event-stream",
1242
+ ...defaultHeaders
1243
+ };
1244
+ function parseSseLines(lines, notify) {
1245
+ let currentEvent = "";
1246
+ let currentData = "";
1247
+ for (const line of lines) {
1248
+ if (line.startsWith("event:")) {
1249
+ currentEvent = line.slice(6).trim();
1250
+ } else if (line.startsWith("data:")) {
1251
+ currentData = line.slice(5).trim();
1252
+ } else if (line === "") {
1253
+ if (currentData) {
1254
+ notify({
1255
+ event: currentEvent || "message",
1256
+ data: currentData
1257
+ });
1258
+ }
1259
+ currentEvent = "";
1260
+ currentData = "";
1261
+ }
1262
+ }
1263
+ }
1264
+ (async () => {
1265
+ try {
1266
+ const response = await fetch(fullUrl, {
1267
+ signal,
1268
+ headers,
1269
+ credentials: "include"
1270
+ });
1271
+ if (!response.ok || !response.body) return;
1272
+ const reader = response.body.getReader();
1273
+ const decoder = new TextDecoder();
1274
+ let buffer = "";
1275
+ while (!closed) {
1276
+ const { done, value } = await reader.read();
1277
+ if (done) break;
1278
+ buffer += decoder.decode(value, { stream: true });
1279
+ const lines = buffer.split("\n");
1280
+ buffer = lines.pop() ?? "";
1281
+ parseSseLines(lines, (event) => {
1282
+ for (const cb of listeners) cb(event);
1283
+ });
1284
+ }
1285
+ } catch {
1286
+ }
1287
+ })();
1288
+ return {
1289
+ subscribe(callback) {
1290
+ listeners.push(callback);
1291
+ },
1292
+ close() {
1293
+ closed = true;
1294
+ controller.abort();
1295
+ }
1296
+ };
1297
+ }
1298
+ };
1299
+ }
1300
+ function createDocumentUploadProvider(config) {
1301
+ const {
1302
+ baseUrl,
1303
+ userId,
1304
+ workflowId,
1305
+ searchStoreConfig,
1306
+ workflowConfig,
1307
+ persist = true,
1308
+ maxConcurrency = 3
1309
+ } = config;
1310
+ const defaultHeaders = {
1311
+ "x-user-id": userId,
1312
+ ...config.headers ?? {}
1313
+ };
1314
+ const httpClient = buildHttpClient(baseUrl, defaultHeaders);
1315
+ let resolvedStoreId = null;
1316
+ let resolvePromise = null;
1317
+ async function createSearchStoreWithWorkflow() {
1318
+ const name = `${userId}-store-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1319
+ const storeRes = await httpClient.post("search-stores", {
1320
+ name,
1321
+ ...searchStoreConfig,
1322
+ metadata: { created_by: userId }
1323
+ });
1324
+ const newStoreId = storeRes.id;
1325
+ await httpClient.post(
1326
+ `search-stores/${newStoreId}/workflows`,
1327
+ workflowConfig
1328
+ );
1329
+ return newStoreId;
1330
+ }
1331
+ async function resolveSearchStore() {
1332
+ if (resolvedStoreId) return resolvedStoreId;
1333
+ if (resolvePromise) return resolvePromise;
1334
+ resolvePromise = (async () => {
1335
+ try {
1336
+ const stores = await httpClient.get("search-stores");
1337
+ if (stores.length > 0 && stores[0]) {
1338
+ resolvedStoreId = stores[0].id;
1339
+ return resolvedStoreId;
1340
+ }
1341
+ resolvedStoreId = await createSearchStoreWithWorkflow();
1342
+ return resolvedStoreId;
1343
+ } catch (err) {
1344
+ resolvePromise = null;
1345
+ throw err;
1346
+ }
1347
+ })();
1348
+ return resolvePromise;
1349
+ }
1350
+ let instance = null;
1351
+ function purgeInterruptedFiles() {
1352
+ if (!instance) return;
1353
+ const allFiles = [...instance.files.value.values()];
1354
+ for (const entry of allFiles) {
1355
+ if (entry.status === "failed" && !entry.metadata.messageId) {
1356
+ instance.removeFile(entry.id, { removeFromRemote: false });
1357
+ }
1358
+ }
1359
+ }
1360
+ function getUploadInstance() {
1361
+ if (!instance) {
1362
+ const onEvent = (event) => {
1363
+ config.onEvent?.(event);
1364
+ if (event.type === "state:restored") {
1365
+ purgeInterruptedFiles();
1366
+ }
1367
+ };
1368
+ instance = useFileUpload({
1369
+ client: httpClient,
1370
+ workflowId,
1371
+ persist,
1372
+ maxConcurrency,
1373
+ onEvent
1374
+ });
1375
+ }
1376
+ return instance;
1377
+ }
1378
+ function useDocumentUpload(conversationIdRef) {
1379
+ const fileInput = ref(null);
1380
+ const upload = getUploadInstance();
1381
+ const storeId = ref(resolvedStoreId ?? "");
1382
+ const currentScope = shallowRef(
1383
+ upload.scopeBy("conversationId", toValue(conversationIdRef))
1384
+ );
1385
+ watch(
1386
+ () => toValue(conversationIdRef),
1387
+ (newId, oldId) => {
1388
+ if (oldId && oldId !== newId) {
1389
+ const oldScope = upload.scopeBy("conversationId", oldId);
1390
+ for (const entry of oldScope.files.value) {
1391
+ upload.updateFileMetadata(entry.id, { conversationId: newId });
1392
+ }
1393
+ }
1394
+ currentScope.value = upload.scopeBy("conversationId", newId);
1395
+ }
1396
+ );
1397
+ function scopedFiles() {
1398
+ return currentScope.value?.files.value ?? [];
1399
+ }
1400
+ async function uploadFiles(files) {
1401
+ if (!files.length) return;
1402
+ const userStoreId = await resolveSearchStore();
1403
+ upload.setStoreId(userStoreId);
1404
+ storeId.value = userStoreId;
1405
+ const conversationId = toValue(conversationIdRef);
1406
+ const result = await upload.upload({
1407
+ id: `${conversationId}::${Date.now()}`,
1408
+ files,
1409
+ forwardedProps: { conversationId }
1410
+ });
1411
+ for (const r of result.results) {
1412
+ if (r.success && r.remoteFileId) {
1413
+ upload.updateFileMetadata(r.fileId, {
1414
+ data_platform_id: r.remoteFileId,
1415
+ searchStoreId: storeId.value
1416
+ });
1417
+ }
1418
+ }
1419
+ const successFileIds = result.results.filter((r) => r.success && r.remoteFileId).map((r) => r.remoteFileId);
1420
+ if (successFileIds.length > 0) {
1421
+ await upload.runWorkflow(successFileIds);
1422
+ purgeCompletedFiles();
1423
+ }
1424
+ return result;
1425
+ }
1426
+ const unassignedFiles = computed(
1427
+ () => scopedFiles().filter((entry) => !entry.metadata.messageId)
1428
+ );
1429
+ function assignFilesToMessage(messageId) {
1430
+ for (const entry of scopedFiles()) {
1431
+ if (!entry.metadata.messageId && entry.status === "success") {
1432
+ upload.updateFileMetadata(entry.id, { messageId });
1433
+ }
1434
+ }
1435
+ }
1436
+ function getFilesForMessage(messageId) {
1437
+ return scopedFiles().filter(
1438
+ (entry) => entry.metadata.messageId === messageId || entry.metadata.responseId && messageId === `user-${entry.metadata.responseId}-0`
1439
+ );
1440
+ }
1441
+ function stampResponseId(responseId, localMessageId) {
1442
+ for (const entry of scopedFiles()) {
1443
+ if (entry.metadata.messageId === localMessageId) {
1444
+ upload.updateFileMetadata(entry.id, { responseId });
1445
+ }
1446
+ }
1447
+ }
1448
+ function purgeCompletedFiles() {
1449
+ const toPurge = scopedFiles().filter(
1450
+ (entry) => entry.metadata.messageId && (entry.status === "success" || entry.status === "failed")
1451
+ );
1452
+ for (const entry of toPurge) {
1453
+ upload.removeFile(entry.id, { removeFromRemote: false });
1454
+ }
1455
+ }
1456
+ function handleFilesSelected(event) {
1457
+ const input = event.target;
1458
+ const fileList = Array.from(input.files ?? []);
1459
+ uploadFiles(fileList);
1460
+ input.value = "";
1461
+ }
1462
+ return {
1463
+ upload,
1464
+ storeId,
1465
+ fileInput,
1466
+ uploadFiles,
1467
+ unassignedFiles,
1468
+ assignFilesToMessage,
1469
+ getFilesForMessage,
1470
+ stampResponseId,
1471
+ purgeCompletedFiles,
1472
+ handleFilesSelected
1473
+ };
1474
+ }
1475
+ return { useDocumentUpload };
1476
+ }
1477
+ const WORKFLOW_ID = "my-doc-ai";
1478
+ const defaultDocumentUploadConfig = {
1479
+ baseUrl: "/file-upload",
1480
+ userId: "chat-user",
1481
+ workflowId: WORKFLOW_ID,
1482
+ searchStoreConfig: {
1483
+ models: [
1484
+ {
1485
+ type: "dense",
1486
+ dimension: 1024,
1487
+ distance: "cosine",
1488
+ modality: "text",
1489
+ embedding_model: "baai/bge-m3"
1490
+ }
1491
+ ],
1492
+ fusion_strategy: { type: "rrf" }
1493
+ },
1494
+ workflowConfig: {
1495
+ name: WORKFLOW_ID,
1496
+ workflow_name: "DocumentAI",
1497
+ definition: { workflow_name: "DocumentAI" },
1498
+ parameters: {
1499
+ initialization: {
1500
+ store: { batch_size: 10, pages_per_part: 25 }
1501
+ },
1502
+ document_extraction: {
1503
+ pdf_reader: { kind: "pypdf", batch_size: 10 },
1504
+ docx_reader: {
1505
+ kind: "docx",
1506
+ batch_size: 10,
1507
+ split_by_sections: false
1508
+ },
1509
+ pptx_reader: { kind: "pptx", batch_size: 10, include_notes: true },
1510
+ xlsx_reader: {
1511
+ kind: "xlsx",
1512
+ batch_size: 10,
1513
+ max_rows: 1e4,
1514
+ max_cols: 100,
1515
+ include_empty_sheets: false
1516
+ },
1517
+ markdown_reader: { kind: "markdown", batch_size: 10 },
1518
+ plaintext_reader: {
1519
+ kind: "plaintext",
1520
+ batch_size: 10,
1521
+ max_content_size: 5e5,
1522
+ min_space_ratio: 0.02
1523
+ }
1524
+ },
1525
+ chunk_extraction: {
1526
+ chunker: {
1527
+ kind: "naive",
1528
+ batch_size: 1,
1529
+ include_image_descriptions: false,
1530
+ chunk_size: 200
1531
+ }
1532
+ }
1533
+ },
1534
+ schema: {
1535
+ type: "object",
1536
+ properties: {
1537
+ source: {
1538
+ description: "Source path, connector URI, or list thereof",
1539
+ anyOf: [
1540
+ { type: "string" },
1541
+ { type: "array", items: { type: "string" } }
1542
+ ]
1543
+ }
1544
+ },
1545
+ additionalProperties: true
1546
+ }
1547
+ }
1548
+ };
1549
+ const { useDocumentUpload: useDocumentUploadWithDefaultConfig } = createDocumentUploadProvider(defaultDocumentUploadConfig);
1550
+ export {
1551
+ useConversationSessions as a,
1552
+ useConversations as b,
1553
+ createDocumentUploadProvider as c,
1554
+ defaultDocumentUploadConfig as d,
1555
+ useDocumentUploadWithDefaultConfig as e,
1556
+ useChat as u
1557
+ };