@aleph-alpha/chat-kit 2.1.0 → 2.2.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 (47) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DPZedAQS.js → CkInput.vue_vue_type_script_setup_true_lang-CjfKRuZs.js} +291 -61
  2. package/dist/chat-kit.css +40 -5
  3. package/dist/components/base/CkDropZoneOverlay/CkDropZoneIllustration.vue.d.ts +4 -0
  4. package/dist/components/base/CkDropZoneOverlay/CkDropZoneIllustration.vue.d.ts.map +1 -0
  5. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.stories.d.ts +17 -0
  6. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.stories.d.ts.map +1 -0
  7. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +31 -0
  8. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts.map +1 -0
  9. package/dist/components/base/CkDropZoneOverlay/index.d.ts +3 -0
  10. package/dist/components/base/CkDropZoneOverlay/index.d.ts.map +1 -0
  11. package/dist/components/base/CkDropZoneOverlay/types.d.ts +88 -0
  12. package/dist/components/base/CkDropZoneOverlay/types.d.ts.map +1 -0
  13. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -1
  14. package/dist/components/base/index.d.ts +2 -0
  15. package/dist/components/base/index.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +2 -2
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/components/index.js +19 -18
  19. package/dist/composables/fileUpload/index.d.ts +1 -1
  20. package/dist/composables/fileUpload/index.d.ts.map +1 -1
  21. package/dist/composables/fileUpload/types.d.ts +11 -1
  22. package/dist/composables/fileUpload/types.d.ts.map +1 -1
  23. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
  24. package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
  25. package/dist/composables/index.js +1 -1
  26. package/dist/{index-DrtHeZpl.js → index-CfXEGjc7.js} +73 -27
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +21 -20
  30. package/package.json +1 -1
  31. package/src/components/base/CkDropZoneOverlay/CkDropZoneIllustration.vue +125 -0
  32. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.spec.ts +104 -0
  33. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.stories.ts +130 -0
  34. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue +272 -0
  35. package/src/components/base/CkDropZoneOverlay/index.ts +2 -0
  36. package/src/components/base/CkDropZoneOverlay/types.ts +93 -0
  37. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +22 -1
  38. package/src/components/base/index.ts +7 -0
  39. package/src/components/index.ts +5 -0
  40. package/src/composables/fileUpload/index.spec.ts +39 -7
  41. package/src/composables/fileUpload/index.ts +41 -19
  42. package/src/composables/fileUpload/types.ts +15 -1
  43. package/src/composables/fileUpload/useFileUpload.spec.ts +147 -0
  44. package/src/composables/fileUpload/useFileUpload.ts +18 -4
  45. package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +202 -0
  46. package/src/composables/fileUpload/useFileUploadWrapper.ts +25 -3
  47. package/src/index.ts +5 -0
@@ -0,0 +1,202 @@
1
+ import { createDocumentUploadProvider } from './useFileUploadWrapper';
2
+ import type { DocumentUploadConfig } from './types';
3
+
4
+ function createFile(name: string, size = 100): File {
5
+ return new File(['x'.repeat(size)], name, { type: 'application/pdf' });
6
+ }
7
+
8
+ function createMockFetchResponse(body: unknown, status = 200) {
9
+ return Promise.resolve({
10
+ ok: status >= 200 && status < 300,
11
+ status,
12
+ headers: new Headers({ 'content-length': '1' }),
13
+ json: () => Promise.resolve(body),
14
+ body: null,
15
+ });
16
+ }
17
+
18
+ function makeInputEvent(files: File[]): Event {
19
+ const input = document.createElement('input');
20
+ input.type = 'file';
21
+ Object.defineProperty(input, 'files', {
22
+ value: files,
23
+ writable: true,
24
+ });
25
+ return { target: input } as unknown as Event;
26
+ }
27
+
28
+ function buildConfig(
29
+ overrides?: Partial<DocumentUploadConfig>,
30
+ ): DocumentUploadConfig {
31
+ return {
32
+ baseUrl: 'https://test.example.com',
33
+ userId: 'user-1',
34
+ workflowId: 'wf-1',
35
+ searchStoreConfig: {
36
+ models: [
37
+ {
38
+ type: 'dense',
39
+ dimension: 1024,
40
+ distance: 'cosine',
41
+ modality: 'text',
42
+ embedding_model: 'test-model',
43
+ },
44
+ ],
45
+ fusion_strategy: { type: 'rrf' },
46
+ },
47
+ workflowConfig: { name: 'wf-1' },
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ describe('useFileUploadWrapper handleFilesSelected', () => {
53
+ let fetchMock: ReturnType<typeof vi.fn>;
54
+
55
+ beforeEach(() => {
56
+ fetchMock = vi.fn();
57
+ vi.stubGlobal('fetch', fetchMock);
58
+
59
+ fetchMock.mockImplementation((url: string, init?: RequestInit) => {
60
+ const method = init?.method ?? 'GET';
61
+
62
+ if (method === 'GET' && url.includes('search-stores')) {
63
+ return createMockFetchResponse([{ id: 'store-1' }]);
64
+ }
65
+ if (method === 'POST' && url.includes('/files')) {
66
+ return createMockFetchResponse([{ id: 'remote-1' }]);
67
+ }
68
+ if (method === 'POST' && url.includes('/workflows')) {
69
+ return createMockFetchResponse({ id: 'run-1' });
70
+ }
71
+ if (url.includes('/event-stream')) {
72
+ return createMockFetchResponse(null, 200).then((res) => ({
73
+ ...res,
74
+ body: new ReadableStream({
75
+ start(controller) {
76
+ controller.close();
77
+ },
78
+ }),
79
+ }));
80
+ }
81
+ return createMockFetchResponse({});
82
+ });
83
+ });
84
+
85
+ afterEach(() => {
86
+ vi.unstubAllGlobals();
87
+ });
88
+
89
+ it('exposes maxFilesPerUpload from config', () => {
90
+ const provider = createDocumentUploadProvider(
91
+ buildConfig({ maxFilesPerUpload: 7 }),
92
+ );
93
+ const result = provider.useDocumentUpload('conv-1');
94
+ expect(result.maxFilesPerUpload).toBe(7);
95
+ });
96
+
97
+ it('defaults maxFilesPerUpload to 20 when not configured', () => {
98
+ const provider = createDocumentUploadProvider(buildConfig());
99
+ const result = provider.useDocumentUpload('conv-1');
100
+ expect(result.maxFilesPerUpload).toBe(20);
101
+ });
102
+
103
+ it('uploads all files when within the limit', async () => {
104
+ const provider = createDocumentUploadProvider(
105
+ buildConfig({ maxFilesPerUpload: 5 }),
106
+ );
107
+ const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
108
+
109
+ const onFilesExceeded = vi.fn();
110
+ const event = makeInputEvent([createFile('a.pdf'), createFile('b.pdf')]);
111
+
112
+ handleFilesSelected(event, { onFilesExceeded });
113
+
114
+ expect(onFilesExceeded).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('truncates files to available slots and calls onFilesExceeded', () => {
118
+ const provider = createDocumentUploadProvider(
119
+ buildConfig({ maxFilesPerUpload: 2 }),
120
+ );
121
+ const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
122
+
123
+ const onFilesExceeded = vi.fn();
124
+ const event = makeInputEvent([
125
+ createFile('a.pdf'),
126
+ createFile('b.pdf'),
127
+ createFile('c.pdf'),
128
+ createFile('d.pdf'),
129
+ ]);
130
+
131
+ handleFilesSelected(event, { onFilesExceeded });
132
+
133
+ expect(onFilesExceeded).toHaveBeenCalledWith({
134
+ selected: 4,
135
+ allowed: 2,
136
+ kept: 2,
137
+ });
138
+ });
139
+
140
+ it('resets input value after handling', () => {
141
+ const provider = createDocumentUploadProvider(
142
+ buildConfig({ maxFilesPerUpload: 5 }),
143
+ );
144
+ const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
145
+
146
+ const event = makeInputEvent([createFile('a.pdf')]);
147
+ const input = (event as any).target as HTMLInputElement;
148
+
149
+ const valueSetter = vi.spyOn(input, 'value', 'set');
150
+
151
+ handleFilesSelected(event);
152
+
153
+ expect(valueSetter).toHaveBeenCalledWith('');
154
+ });
155
+
156
+ it('does not upload when no slots are available', async () => {
157
+ const provider = createDocumentUploadProvider(
158
+ buildConfig({ maxFilesPerUpload: 0 }),
159
+ );
160
+ const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
161
+
162
+ const onFilesExceeded = vi.fn();
163
+ const event = makeInputEvent([createFile('a.pdf')]);
164
+
165
+ handleFilesSelected(event, { onFilesExceeded });
166
+
167
+ expect(onFilesExceeded).toHaveBeenCalledWith({
168
+ selected: 1,
169
+ allowed: 0,
170
+ kept: 0,
171
+ });
172
+ });
173
+
174
+ it('does not call onFilesExceeded when exactly at the limit', () => {
175
+ const provider = createDocumentUploadProvider(
176
+ buildConfig({ maxFilesPerUpload: 3 }),
177
+ );
178
+ const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
179
+
180
+ const onFilesExceeded = vi.fn();
181
+ const event = makeInputEvent([
182
+ createFile('a.pdf'),
183
+ createFile('b.pdf'),
184
+ createFile('c.pdf'),
185
+ ]);
186
+
187
+ handleFilesSelected(event, { onFilesExceeded });
188
+
189
+ expect(onFilesExceeded).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it('works without onFilesExceeded callback (no crash)', () => {
193
+ const provider = createDocumentUploadProvider(
194
+ buildConfig({ maxFilesPerUpload: 1 }),
195
+ );
196
+ const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
197
+
198
+ const event = makeInputEvent([createFile('a.pdf'), createFile('b.pdf')]);
199
+
200
+ expect(() => handleFilesSelected(event)).not.toThrow();
201
+ });
202
+ });
@@ -9,6 +9,7 @@ import type {
9
9
  DocumentUploadConfig,
10
10
  DocumentUploadProvider,
11
11
  DocumentUploadReturn,
12
+ HandleFilesSelectedOptions,
12
13
  UploadEvent,
13
14
  } from './types';
14
15
  import { useFileUpload } from './useFileUpload';
@@ -166,6 +167,7 @@ export function createDocumentUploadProvider(
166
167
  workflowConfig,
167
168
  persist = true,
168
169
  maxConcurrency = 3,
170
+ maxFilesPerUpload,
169
171
  } = config;
170
172
 
171
173
  const defaultHeaders: Record<string, string> = {
@@ -249,6 +251,7 @@ export function createDocumentUploadProvider(
249
251
  workflowId,
250
252
  persist,
251
253
  maxConcurrency,
254
+ maxFilesPerUpload,
252
255
  onEvent,
253
256
  });
254
257
  }
@@ -262,6 +265,7 @@ export function createDocumentUploadProvider(
262
265
  ): DocumentUploadReturn {
263
266
  const fileInput = ref<HTMLInputElement | null>(null);
264
267
  const upload = getUploadInstance();
268
+ const resolvedMaxFiles = upload.getSupportedFileTypes().maxFilesPerUpload;
265
269
  const storeId = ref<string>(resolvedStoreId ?? '');
266
270
 
267
271
  const currentScope = shallowRef<ScopedFileView | null>(
@@ -360,10 +364,27 @@ export function createDocumentUploadProvider(
360
364
  }
361
365
  }
362
366
 
363
- function handleFilesSelected(event: Event) {
367
+ function handleFilesSelected(
368
+ event: Event,
369
+ options?: HandleFilesSelectedOptions,
370
+ ) {
364
371
  const input = event.target as HTMLInputElement;
365
- const fileList = Array.from(input.files ?? []);
366
- uploadFiles(fileList);
372
+ let fileList = Array.from(input.files ?? []);
373
+ const availableSlots = resolvedMaxFiles - unassignedFiles.value.length;
374
+
375
+ if (fileList.length > availableSlots) {
376
+ const kept = Math.max(0, availableSlots);
377
+ options?.onFilesExceeded?.({
378
+ selected: fileList.length,
379
+ allowed: availableSlots,
380
+ kept,
381
+ });
382
+ fileList = fileList.slice(0, kept);
383
+ }
384
+
385
+ if (fileList.length > 0) {
386
+ uploadFiles(fileList);
387
+ }
367
388
  input.value = '';
368
389
  }
369
390
 
@@ -371,6 +392,7 @@ export function createDocumentUploadProvider(
371
392
  upload,
372
393
  storeId,
373
394
  fileInput,
395
+ maxFilesPerUpload: resolvedMaxFiles,
374
396
  uploadFiles,
375
397
  unassignedFiles,
376
398
  assignFilesToMessage,
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export {
66
66
  CkConversation,
67
67
  CkConversationLayout,
68
68
  CkCopyMessageActionButton,
69
+ CkDropZoneOverlay,
69
70
  CkFeedbackButton,
70
71
  CkFeedbackDialog,
71
72
  CkFileItemsList,
@@ -87,6 +88,10 @@ export {
87
88
  export type {
88
89
  CkConversationLayoutProps,
89
90
  CkConversationProps,
91
+ CkDropZoneOverlayProps,
92
+ CkDropZoneOverlayEmits,
93
+ CkDropZoneRejection,
94
+ CkDropZoneRejectionReason,
90
95
  CkFeedbackButtonProps,
91
96
  CkFeedbackDialogEmits,
92
97
  CkFeedbackDialogProps,