@aleph-alpha/chat-kit 3.2.0 → 4.0.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 (164) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-B-4xnH2c.js → CkInput.vue_vue_type_script_setup_true_lang-DJN-nZ2-.js} +391 -113
  2. package/dist/adapters/responses-api.d.ts +11 -5
  3. package/dist/adapters/responses-api.d.ts.map +1 -1
  4. package/dist/adapters/responses-api.js +9 -3
  5. package/dist/chat-kit.css +14 -8
  6. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +7 -0
  7. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
  8. package/dist/components/base/CkAgentMessage/types.d.ts +1 -1
  9. package/dist/components/base/CkCitation/CkCitation.stories.d.ts +47 -0
  10. package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -0
  11. package/dist/components/base/CkCitation/CkCitation.vue.d.ts +15 -0
  12. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -0
  13. package/dist/components/base/CkCitation/index.d.ts +3 -0
  14. package/dist/components/base/CkCitation/index.d.ts.map +1 -0
  15. package/dist/components/base/CkCitation/types.d.ts +79 -0
  16. package/dist/components/base/CkCitation/types.d.ts.map +1 -0
  17. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +51 -0
  18. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -0
  19. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +13 -0
  20. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -0
  21. package/dist/components/base/CkCitationCard/index.d.ts +3 -0
  22. package/dist/components/base/CkCitationCard/index.d.ts.map +1 -0
  23. package/dist/components/base/CkCitationCard/types.d.ts +51 -0
  24. package/dist/components/base/CkCitationCard/types.d.ts.map +1 -0
  25. package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts → CkCopyMessageButton/CkCopyMessageButton.stories.d.ts} +3 -3
  26. package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.stories.d.ts.map +1 -0
  27. package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts → CkCopyMessageButton/CkCopyMessageButton.vue.d.ts} +4 -4
  28. package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.vue.d.ts.map +1 -0
  29. package/dist/components/base/CkCopyMessageButton/index.d.ts +3 -0
  30. package/dist/components/base/CkCopyMessageButton/index.d.ts.map +1 -0
  31. package/dist/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.d.ts +3 -3
  32. package/dist/components/base/CkCopyMessageButton/types.d.ts.map +1 -0
  33. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +1 -1
  34. package/dist/components/base/CkFeedbackButton/types.d.ts +1 -1
  35. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
  36. package/dist/components/base/CkFeedbackDialog/types.d.ts +1 -1
  37. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
  38. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +15 -0
  39. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
  40. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts +16 -2
  41. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
  42. package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
  43. package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
  44. package/dist/components/base/CkMarkdownRenderer/types.d.ts +25 -0
  45. package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
  46. package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts +17 -0
  47. package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts.map +1 -0
  48. package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.stories.d.ts → CkMessageButton/CkMessageButton.stories.d.ts} +3 -3
  49. package/dist/components/base/CkMessageButton/CkMessageButton.stories.d.ts.map +1 -0
  50. package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.vue.d.ts → CkMessageButton/CkMessageButton.vue.d.ts} +3 -3
  51. package/dist/components/base/CkMessageButton/CkMessageButton.vue.d.ts.map +1 -0
  52. package/dist/components/base/CkMessageButton/index.d.ts +3 -0
  53. package/dist/components/base/CkMessageButton/index.d.ts.map +1 -0
  54. package/dist/components/base/{CkMessageActionButton → CkMessageButton}/types.d.ts +2 -2
  55. package/dist/components/base/CkMessageButton/types.d.ts.map +1 -0
  56. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
  57. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts +16 -0
  58. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts.map +1 -0
  59. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts +12 -0
  60. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts.map +1 -0
  61. package/dist/components/base/CkRegenerateMessageButton/index.d.ts +3 -0
  62. package/dist/components/base/{CkCopyMessageActionButton → CkRegenerateMessageButton}/index.d.ts.map +1 -1
  63. package/dist/components/base/CkRegenerateMessageButton/types.d.ts +27 -0
  64. package/dist/components/base/CkRegenerateMessageButton/types.d.ts.map +1 -0
  65. package/dist/components/base/index.d.ts +8 -3
  66. package/dist/components/base/index.d.ts.map +1 -1
  67. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +1 -1
  68. package/dist/components/composed/CkConversation/types.d.ts +1 -1
  69. package/dist/components/composed/CkInput/CkInput.vue.d.ts +1 -1
  70. package/dist/components/index.d.ts +2 -2
  71. package/dist/components/index.d.ts.map +1 -1
  72. package/dist/components/index.js +26 -23
  73. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
  74. package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
  75. package/dist/composables/index.js +1 -1
  76. package/dist/handlers/index.d.ts +2 -0
  77. package/dist/handlers/index.d.ts.map +1 -1
  78. package/dist/handlers/index.js +4 -3
  79. package/dist/handlers/useChatKitLabels.d.ts +13 -0
  80. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  81. package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
  82. package/dist/handlers/useMessageHandler.d.ts +37 -0
  83. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  84. package/dist/helpers/conversationTree.d.ts +15 -0
  85. package/dist/helpers/conversationTree.d.ts.map +1 -1
  86. package/dist/helpers/unified/remarkCitations.d.ts +17 -0
  87. package/dist/helpers/unified/remarkCitations.d.ts.map +1 -0
  88. package/dist/{index-DAmWGYNH.js → index-CyrVnbS9.js} +86 -71
  89. package/dist/index.d.ts +2 -2
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +30 -27
  92. package/dist/{useChatKitLabels-D5gNv8se.js → useChatKitLabels-zMFV7aEz.js} +31 -3
  93. package/dist/{useMessageHandler-CMdy-lm0.js → useMessageHandler-N50Yj-NU.js} +45 -7
  94. package/package.json +5 -3
  95. package/src/adapters/responses-api.spec.ts +52 -5
  96. package/src/adapters/responses-api.ts +26 -7
  97. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +112 -0
  98. package/src/components/base/CkAgentMessage/types.ts +1 -1
  99. package/src/components/base/CkCitation/CkCitation.spec.ts +79 -0
  100. package/src/components/base/CkCitation/CkCitation.stories.ts +136 -0
  101. package/src/components/base/CkCitation/CkCitation.vue +145 -0
  102. package/src/components/base/CkCitation/index.ts +2 -0
  103. package/src/components/base/CkCitation/types.ts +80 -0
  104. package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +77 -0
  105. package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +144 -0
  106. package/src/components/base/CkCitationCard/CkCitationCard.vue +76 -0
  107. package/src/components/base/CkCitationCard/index.ts +2 -0
  108. package/src/components/base/CkCitationCard/types.ts +51 -0
  109. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts → CkCopyMessageButton/CkCopyMessageButton.spec.ts} +6 -6
  110. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts → CkCopyMessageButton/CkCopyMessageButton.stories.ts} +8 -8
  111. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue → CkCopyMessageButton/CkCopyMessageButton.vue} +6 -6
  112. package/src/components/base/CkCopyMessageButton/index.ts +2 -0
  113. package/src/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.ts +3 -3
  114. package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +5 -5
  115. package/src/components/base/CkFeedbackButton/types.ts +1 -1
  116. package/src/components/base/CkFeedbackDialog/types.ts +1 -1
  117. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +124 -0
  118. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +118 -0
  119. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +70 -28
  120. package/src/components/base/CkMarkdownRenderer/index.ts +1 -0
  121. package/src/components/base/CkMarkdownRenderer/types.ts +26 -0
  122. package/src/components/base/CkMarkdownRenderer/useCitationRendering.spec.ts +94 -0
  123. package/src/components/base/CkMarkdownRenderer/useCitationRendering.ts +52 -0
  124. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.spec.ts → CkMessageButton/CkMessageButton.spec.ts} +6 -6
  125. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.stories.ts → CkMessageButton/CkMessageButton.stories.ts} +10 -10
  126. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.vue → CkMessageButton/CkMessageButton.vue} +3 -3
  127. package/src/components/base/CkMessageButton/index.ts +2 -0
  128. package/src/components/base/{CkMessageActionButton → CkMessageButton}/types.ts +2 -2
  129. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.spec.ts +49 -0
  130. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.ts +63 -0
  131. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue +43 -0
  132. package/src/components/base/CkRegenerateMessageButton/index.ts +2 -0
  133. package/src/components/base/CkRegenerateMessageButton/types.ts +27 -0
  134. package/src/components/base/index.ts +15 -2
  135. package/src/components/composed/CkConversation/CkConversation.stories.ts +6 -6
  136. package/src/components/composed/CkConversation/CkConversation.vue +2 -2
  137. package/src/components/composed/CkConversation/types.ts +1 -1
  138. package/src/components/index.ts +11 -2
  139. package/src/composables/fileUpload/useFileUpload.spec.ts +2 -61
  140. package/src/composables/fileUpload/useFileUpload.ts +2 -9
  141. package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +195 -14
  142. package/src/composables/fileUpload/useFileUploadWrapper.ts +114 -81
  143. package/src/handlers/index.ts +2 -0
  144. package/src/handlers/useChatKitLabels.spec.ts +1 -0
  145. package/src/handlers/useChatKitLabels.ts +16 -0
  146. package/src/handlers/useMarkdownParser.ts +5 -2
  147. package/src/handlers/useMessageHandler.spec.ts +79 -0
  148. package/src/handlers/useMessageHandler.ts +50 -2
  149. package/src/helpers/conversationTree.spec.ts +120 -0
  150. package/src/helpers/conversationTree.ts +57 -8
  151. package/src/helpers/unified/remarkCitations.spec.ts +112 -0
  152. package/src/helpers/unified/remarkCitations.ts +55 -0
  153. package/src/index.ts +11 -2
  154. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +0 -1
  155. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +0 -1
  156. package/dist/components/base/CkCopyMessageActionButton/index.d.ts +0 -3
  157. package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +0 -1
  158. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +0 -1
  159. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +0 -1
  160. package/dist/components/base/CkMessageActionButton/index.d.ts +0 -3
  161. package/dist/components/base/CkMessageActionButton/index.d.ts.map +0 -1
  162. package/dist/components/base/CkMessageActionButton/types.d.ts.map +0 -1
  163. package/src/components/base/CkCopyMessageActionButton/index.ts +0 -2
  164. package/src/components/base/CkMessageActionButton/index.ts +0 -2
@@ -94,30 +94,42 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
94
94
  fetchMock.mockImplementation((url: string, init?: RequestInit) => {
95
95
  const method = init?.method ?? 'GET';
96
96
 
97
- if (method === 'GET' && url.includes('/workflows')) {
97
+ if (method === 'GET' && url.endsWith('search-stores')) {
98
98
  return createMockFetchResponse({
99
99
  items: [
100
100
  {
101
- id: 'workflow-api-1',
102
- name: 'wf-1',
103
- created_at: '2024-01-01T00:00:00Z',
101
+ id: 'store-1',
102
+ name: 'conv-1',
103
+ models: [{ embedding_model: 'test-model' }],
104
104
  },
105
105
  ],
106
106
  total: 1,
107
107
  });
108
108
  }
109
- if (method === 'GET' && url.includes('search-stores')) {
109
+ if (method === 'GET' && url.includes('/workflows')) {
110
110
  return createMockFetchResponse({
111
- items: [
112
- {
113
- id: 'store-1',
114
- name: 'test-store',
115
- models: [{ embedding_model: 'test-model' }],
116
- },
117
- ],
118
- total: 1,
111
+ items: [{ id: 'wf-1', name: 'wf-1' }],
119
112
  });
120
113
  }
114
+ if (method === 'POST' && url.endsWith('search-stores')) {
115
+ return createMockFetchResponse({ id: 'new-store-id' });
116
+ }
117
+ if (method === 'POST' && url.includes('/files')) {
118
+ return createMockFetchResponse([{ id: 'remote-1' }]);
119
+ }
120
+ if (method === 'POST' && url.includes('/workflows')) {
121
+ return createMockFetchResponse({ id: 'run-1' });
122
+ }
123
+ if (url.includes('/event-stream')) {
124
+ return createMockFetchResponse(null, 200).then((res) => ({
125
+ ...res,
126
+ body: new ReadableStream({
127
+ start(controller) {
128
+ controller.close();
129
+ },
130
+ }),
131
+ }));
132
+ }
121
133
  return createMockFetchResponse({});
122
134
  });
123
135
  });
@@ -222,7 +234,6 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
222
234
  });
223
235
  await settleAsyncUploads();
224
236
  expect(uploadMock.upload).not.toHaveBeenCalled();
225
- expect(fetchMock).not.toHaveBeenCalled();
226
237
  });
227
238
 
228
239
  it('does not call onFilesExceeded when exactly at the limit', async () => {
@@ -258,3 +269,173 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
258
269
  expect(uploadMock.upload).toHaveBeenCalledOnce();
259
270
  });
260
271
  });
272
+
273
+ describe('useFileUploadWrapper search store resolution', () => {
274
+ let fetchMock: ReturnType<typeof vi.fn>;
275
+
276
+ beforeEach(() => {
277
+ fetchMock = vi.fn();
278
+ vi.stubGlobal('fetch', fetchMock);
279
+ });
280
+
281
+ afterEach(() => {
282
+ vi.unstubAllGlobals();
283
+ });
284
+
285
+ it('reuses an existing search store named after the conversation id', async () => {
286
+ fetchMock.mockImplementation((url: string, init?: RequestInit) => {
287
+ const method = init?.method ?? 'GET';
288
+
289
+ if (method === 'GET' && url.endsWith('search-stores')) {
290
+ return createMockFetchResponse({
291
+ items: [
292
+ {
293
+ id: 'existing-store',
294
+ name: 'conv-1',
295
+ models: [{ embedding_model: 'test-model' }],
296
+ },
297
+ ],
298
+ total: 1,
299
+ });
300
+ }
301
+ if (method === 'GET' && url.includes('/workflows')) {
302
+ return createMockFetchResponse({
303
+ items: [{ id: 'wf-1', name: 'wf-1' }],
304
+ });
305
+ }
306
+ return createMockFetchResponse({});
307
+ });
308
+
309
+ const provider = createDocumentUploadProvider(buildConfig());
310
+ const { storeId } = provider.useDocumentUpload('conv-1');
311
+
312
+ await vi.waitFor(() => {
313
+ expect(storeId.value).toBe('existing-store');
314
+ });
315
+
316
+ expect(fetchMock).toHaveBeenCalledWith(
317
+ 'https://test.example.com/search-stores',
318
+ expect.objectContaining({ method: 'GET' }),
319
+ );
320
+ expect(
321
+ fetchMock.mock.calls.some(
322
+ ([url, init]) =>
323
+ init?.method === 'POST' && String(url).endsWith('search-stores'),
324
+ ),
325
+ ).toBe(false);
326
+ });
327
+
328
+ it('creates a search store and workflow when none exists for the conversation', async () => {
329
+ fetchMock.mockImplementation((url: string, init?: RequestInit) => {
330
+ const method = init?.method ?? 'GET';
331
+
332
+ if (method === 'GET' && url.endsWith('search-stores')) {
333
+ return createMockFetchResponse({ items: [], total: 0 });
334
+ }
335
+ if (method === 'POST' && url.endsWith('search-stores')) {
336
+ const body = JSON.parse(String(init?.body));
337
+ expect(body.name).toBe('conv-2');
338
+ return createMockFetchResponse({ id: 'created-store' });
339
+ }
340
+ if (method === 'POST' && url.includes('/workflows')) {
341
+ return createMockFetchResponse({ id: 'wf-run-1' });
342
+ }
343
+ return createMockFetchResponse({});
344
+ });
345
+
346
+ const provider = createDocumentUploadProvider(buildConfig());
347
+ const { storeId } = provider.useDocumentUpload('conv-2');
348
+
349
+ await vi.waitFor(() => {
350
+ expect(storeId.value).toBe('created-store');
351
+ });
352
+
353
+ expect(
354
+ fetchMock.mock.calls.some(
355
+ ([url, init]) =>
356
+ init?.method === 'POST' &&
357
+ String(url).endsWith('search-stores/created-store/workflows'),
358
+ ),
359
+ ).toBe(true);
360
+ });
361
+
362
+ it('creates a workflow when reusing a store that has none', async () => {
363
+ fetchMock.mockImplementation((url: string, init?: RequestInit) => {
364
+ const method = init?.method ?? 'GET';
365
+
366
+ if (method === 'GET' && url.endsWith('search-stores')) {
367
+ return createMockFetchResponse({
368
+ items: [
369
+ {
370
+ id: 'orphan-store',
371
+ name: 'conv-3',
372
+ models: [{ embedding_model: 'test-model' }],
373
+ },
374
+ ],
375
+ total: 1,
376
+ });
377
+ }
378
+ if (method === 'GET' && url.includes('/workflows')) {
379
+ return createMockFetchResponse({ items: [] });
380
+ }
381
+ if (method === 'POST' && url.includes('/workflows')) {
382
+ return createMockFetchResponse({ id: 'wf-1' });
383
+ }
384
+ return createMockFetchResponse({});
385
+ });
386
+
387
+ const provider = createDocumentUploadProvider(buildConfig());
388
+ const { storeId } = provider.useDocumentUpload('conv-3');
389
+
390
+ await vi.waitFor(() => {
391
+ expect(storeId.value).toBe('orphan-store');
392
+ });
393
+
394
+ expect(
395
+ fetchMock.mock.calls.some(
396
+ ([url, init]) =>
397
+ init?.method === 'POST' &&
398
+ String(url).endsWith('search-stores/orphan-store/workflows'),
399
+ ),
400
+ ).toBe(true);
401
+ });
402
+
403
+ it('resolves a different store per conversation id', async () => {
404
+ fetchMock.mockImplementation((url: string, init?: RequestInit) => {
405
+ const method = init?.method ?? 'GET';
406
+
407
+ if (method === 'GET' && url.endsWith('search-stores')) {
408
+ return createMockFetchResponse({
409
+ items: [
410
+ {
411
+ id: 'store-a',
412
+ name: 'conv-a',
413
+ models: [{ embedding_model: 'test-model' }],
414
+ },
415
+ {
416
+ id: 'store-b',
417
+ name: 'conv-b',
418
+ models: [{ embedding_model: 'test-model' }],
419
+ },
420
+ ],
421
+ total: 2,
422
+ });
423
+ }
424
+ if (method === 'GET' && url.includes('/workflows')) {
425
+ return createMockFetchResponse({
426
+ items: [{ id: 'wf-1', name: 'wf-1' }],
427
+ });
428
+ }
429
+ return createMockFetchResponse({});
430
+ });
431
+
432
+ const provider = createDocumentUploadProvider(buildConfig());
433
+ const uploadA = provider.useDocumentUpload('conv-a');
434
+ const uploadB = provider.useDocumentUpload('conv-b');
435
+
436
+ await vi.waitFor(() => {
437
+ expect(uploadA.storeId.value).toBe('store-a');
438
+ expect(uploadB.storeId.value).toBe('store-b');
439
+ });
440
+ });
441
+ });
@@ -13,7 +13,6 @@ import type {
13
13
  UploadEvent,
14
14
  } from './types';
15
15
  import { useFileUpload } from './useFileUpload';
16
- import { defaultEndpoints } from './types';
17
16
 
18
17
  function buildHttpClient(
19
18
  baseUrl: string,
@@ -177,34 +176,14 @@ export function createDocumentUploadProvider(
177
176
  };
178
177
 
179
178
  const httpClient = buildHttpClient(baseUrl, defaultHeaders);
180
- const endpoints = defaultEndpoints;
181
179
 
182
- // ---- Search store resolution (one per user) ----
180
+ // ---- Search store resolution (one per conversation) ----
183
181
 
184
- let resolvedStoreId: string | null = null;
185
- let resolvePromise: Promise<string> | null = null;
186
- const resolvedWorkflowIdByStore = new Map<string, string>();
182
+ const storeIdByConversation = new Map<string, string>();
183
+ const resolvePromises = new Map<string, Promise<string>>();
187
184
 
188
- async function createWorkflowForStore(storeId: string): Promise<string> {
189
- const workflowRes = await httpClient.post<{ id: string }>(
190
- endpoints.createWorkflow(storeId),
191
- workflowConfig,
192
- );
193
- return workflowRes.id;
194
- }
195
-
196
- async function createSearchStoreWithWorkflow(): Promise<string> {
197
- const name = `store-${new Date().toISOString().replace(/[:.]/g, '-')}`;
198
- const storeRes = await httpClient.post<{ id: string }>('search-stores', {
199
- name,
200
- ...searchStoreConfig,
201
- });
202
- const newStoreId = storeRes.id;
203
-
204
- const workflowApiId = await createWorkflowForStore(newStoreId);
205
- resolvedWorkflowIdByStore.set(newStoreId, workflowApiId);
206
-
207
- return newStoreId;
185
+ function searchStoreNameForConversation(conversationId: string): string {
186
+ return conversationId;
208
187
  }
209
188
 
210
189
  interface SearchStoreListResponse {
@@ -217,63 +196,109 @@ export function createDocumentUploadProvider(
217
196
  }
218
197
 
219
198
  interface WorkflowListResponse {
220
- items: Array<{
221
- id: string;
222
- name: string;
223
- created_at: string;
224
- }>;
225
- total: number;
199
+ items: Array<{ id: string; name?: string }>;
226
200
  }
227
201
 
228
- async function fetchOrCreateWorkflow(storeId: string): Promise<string> {
229
- const cached = resolvedWorkflowIdByStore.get(storeId);
230
- if (cached) return cached;
202
+ function findConfiguredWorkflow(
203
+ items: WorkflowListResponse['items'],
204
+ ): { id: string } | undefined {
205
+ const configuredWorkflowName =
206
+ typeof workflowConfig.name === 'string' ? workflowConfig.name : undefined;
207
+
208
+ return items?.find(
209
+ (workflow) =>
210
+ workflow.id === workflowId ||
211
+ workflow.name === workflowId ||
212
+ (configuredWorkflowName != null &&
213
+ workflow.name === configuredWorkflowName),
214
+ );
215
+ }
231
216
 
217
+ async function resolveWorkflowId(storeId: string): Promise<string> {
232
218
  const response = await httpClient.get<WorkflowListResponse>(
233
- endpoints.workflows(storeId),
219
+ `search-stores/${storeId}/workflows`,
234
220
  );
221
+ const existing = findConfiguredWorkflow(response.items);
235
222
 
236
- if (response.items.length > 0) {
237
- const match = response.items.find((item) => item.name === workflowId);
238
- const selected = match ?? response.items[0]!;
239
- resolvedWorkflowIdByStore.set(storeId, selected.id);
240
- return selected.id;
223
+ if (existing) {
224
+ return existing.id;
241
225
  }
242
226
 
243
- const workflowApiId = await createWorkflowForStore(storeId);
244
- resolvedWorkflowIdByStore.set(storeId, workflowApiId);
245
- return workflowApiId;
227
+ const created = await httpClient.post<{ id: string }>(
228
+ `search-stores/${storeId}/workflows`,
229
+ workflowConfig,
230
+ );
231
+ return created.id;
246
232
  }
247
233
 
248
- async function fetchOrCreateStore(): Promise<string> {
249
- try {
250
- const response =
251
- await httpClient.get<SearchStoreListResponse>('search-stores');
234
+ async function createSearchStore(conversationId: string): Promise<string> {
235
+ const storeRes = await httpClient.post<{ id: string }>('search-stores', {
236
+ name: searchStoreNameForConversation(conversationId),
237
+ ...searchStoreConfig,
238
+ });
252
239
 
253
- const expectedModel = searchStoreConfig.models[0]?.embedding_model;
254
- const match = response.items.find((store) =>
255
- store.models.some((m) => m.embedding_model === expectedModel),
256
- );
240
+ await httpClient.post(
241
+ `search-stores/${storeRes.id}/workflows`,
242
+ workflowConfig,
243
+ );
257
244
 
258
- if (match) {
259
- resolvedStoreId = match.id;
260
- return resolvedStoreId;
261
- }
245
+ return storeRes.id;
246
+ }
262
247
 
263
- resolvedStoreId = await createSearchStoreWithWorkflow();
264
- return resolvedStoreId;
265
- } catch (err) {
266
- resolvePromise = null;
267
- throw err;
248
+ function findStoreForConversation(
249
+ items: SearchStoreListResponse['items'],
250
+ conversationId: string,
251
+ ) {
252
+ const expectedName = searchStoreNameForConversation(conversationId);
253
+ const expectedModel = searchStoreConfig.models[0]?.embedding_model;
254
+
255
+ return items.find(
256
+ (store) =>
257
+ store.name === expectedName &&
258
+ (!expectedModel ||
259
+ store.models.some((m) => m.embedding_model === expectedModel)),
260
+ );
261
+ }
262
+
263
+ async function fetchOrCreateStore(conversationId: string): Promise<string> {
264
+ const response =
265
+ await httpClient.get<SearchStoreListResponse>('search-stores');
266
+
267
+ const existing = findStoreForConversation(response.items, conversationId);
268
+
269
+ if (existing) {
270
+ await resolveWorkflowId(existing.id);
271
+ storeIdByConversation.set(conversationId, existing.id);
272
+ return existing.id;
268
273
  }
274
+
275
+ const newStoreId = await createSearchStore(conversationId);
276
+ storeIdByConversation.set(conversationId, newStoreId);
277
+ return newStoreId;
269
278
  }
270
279
 
271
- async function resolveSearchStore(): Promise<string> {
272
- if (resolvedStoreId) return resolvedStoreId;
273
- if (resolvePromise) return resolvePromise;
280
+ async function resolveSearchStore(conversationId: string): Promise<string> {
281
+ if (!conversationId) {
282
+ throw new Error('conversationId is required to resolve a search store.');
283
+ }
284
+
285
+ const cached = storeIdByConversation.get(conversationId);
286
+ if (cached) return cached;
287
+
288
+ const pending = resolvePromises.get(conversationId);
289
+ if (pending) return pending;
290
+
291
+ const promise = fetchOrCreateStore(conversationId).catch((err) => {
292
+ storeIdByConversation.delete(conversationId);
293
+ throw err;
294
+ });
295
+ resolvePromises.set(conversationId, promise);
274
296
 
275
- resolvePromise = fetchOrCreateStore();
276
- return resolvePromise;
297
+ try {
298
+ return await promise;
299
+ } finally {
300
+ resolvePromises.delete(conversationId);
301
+ }
277
302
  }
278
303
 
279
304
  // ---- Upload SDK singleton ----
@@ -319,7 +344,9 @@ export function createDocumentUploadProvider(
319
344
  const fileInput = ref<HTMLInputElement | null>(null);
320
345
  const upload = getUploadInstance();
321
346
  const resolvedMaxFiles = upload.getSupportedFileTypes().maxFilesPerUpload;
322
- const storeId = ref<string>(resolvedStoreId ?? '');
347
+ const storeId = ref<string>(
348
+ storeIdByConversation.get(toValue(conversationIdRef)) ?? '',
349
+ );
323
350
 
324
351
  const currentScope = shallowRef<ScopedFileView | null>(
325
352
  upload.scopeBy('conversationId', toValue(conversationIdRef)),
@@ -327,15 +354,20 @@ export function createDocumentUploadProvider(
327
354
 
328
355
  watch(
329
356
  () => toValue(conversationIdRef),
330
- (newId: string, oldId: string) => {
331
- if (oldId && oldId !== newId) {
332
- const oldScope = upload.scopeBy('conversationId', oldId);
333
- for (const entry of oldScope.files.value) {
334
- upload.updateFileMetadata(entry.id, { conversationId: newId });
335
- }
336
- }
357
+ async (newId: string) => {
337
358
  currentScope.value = upload.scopeBy('conversationId', newId);
359
+ if (!newId) {
360
+ storeId.value = '';
361
+ return;
362
+ }
363
+
364
+ try {
365
+ storeId.value = await resolveSearchStore(newId);
366
+ } catch {
367
+ storeId.value = '';
368
+ }
338
369
  },
370
+ { immediate: true },
339
371
  );
340
372
 
341
373
  function scopedFiles(): FileEntry[] {
@@ -345,13 +377,15 @@ export function createDocumentUploadProvider(
345
377
  async function uploadFiles(files: File[]) {
346
378
  if (!files.length) return;
347
379
 
348
- const userStoreId = await resolveSearchStore();
349
- const workflowApiId = await fetchOrCreateWorkflow(userStoreId);
380
+ const conversationId = toValue(conversationIdRef);
381
+ if (!conversationId) return;
382
+
383
+ const userStoreId = await resolveSearchStore(conversationId);
384
+ const workflowApiId = await resolveWorkflowId(userStoreId);
350
385
  upload.setStoreId(userStoreId);
351
386
  upload.setWorkflowId(workflowApiId);
352
387
  storeId.value = userStoreId;
353
388
 
354
- const conversationId = toValue(conversationIdRef);
355
389
  const result = await upload.upload({
356
390
  id: `${conversationId}::${Date.now()}`,
357
391
  files,
@@ -425,13 +459,12 @@ export function createDocumentUploadProvider(
425
459
  ) {
426
460
  const input = event.target as HTMLInputElement;
427
461
  let fileList = Array.from(input.files ?? []);
428
- const availableSlots = resolvedMaxFiles - unassignedFiles.value.length;
429
462
 
430
- if (fileList.length > availableSlots) {
431
- const kept = Math.max(0, availableSlots);
463
+ if (fileList.length > resolvedMaxFiles) {
464
+ const kept = Math.max(0, resolvedMaxFiles);
432
465
  options?.onFilesExceeded?.({
433
466
  selected: fileList.length,
434
- allowed: availableSlots,
467
+ allowed: resolvedMaxFiles,
435
468
  kept,
436
469
  });
437
470
  fileList = fileList.slice(0, kept);
@@ -4,6 +4,8 @@ export {
4
4
  remarkNormalizeMath,
5
5
  normalizeMathDelimiters,
6
6
  } from '../helpers/unified/remarkNormalizeMath';
7
+ export { remarkCitations } from '../helpers/unified/remarkCitations';
8
+ export type { RemarkCitationsOptions } from '../helpers/unified/remarkCitations';
7
9
  export {
8
10
  provideChatKitLabels,
9
11
  useChatKitLabels,
@@ -102,6 +102,7 @@ describe('useChatKitLabels', () => {
102
102
  inputPlaceholder: 'Custom placeholder',
103
103
  copyMessageAction: 'Custom copy',
104
104
  copyMessageActionCopied: 'Custom copied',
105
+ regenerateMessageAction: 'Custom regenerate',
105
106
  feedbackThumbsUp: 'Custom up',
106
107
  feedbackThumbsDown: 'Custom down',
107
108
  feedbackGroup: 'Custom group',
@@ -29,6 +29,8 @@ export type ChatKitLabels = {
29
29
  copyMessageAction: string;
30
30
  /** Accessible label / tooltip for the copy-message action button after a successful copy */
31
31
  copyMessageActionCopied: string;
32
+ /** Accessible label / tooltip for the regenerate-message action button */
33
+ regenerateMessageAction: string;
32
34
  /**
33
35
  * Accessible label / tooltip for the thumbs-up button in `CkFeedbackButton`.
34
36
  * Optional for backward compatibility; falls back to
@@ -81,6 +83,17 @@ export type ChatKitLabels = {
81
83
  * can scroll wide tables). Falls back to `defaultChatKitLabels` when omitted.
82
84
  */
83
85
  markdownTableLabel?: string;
86
+ /**
87
+ * Accessible label for the external-link action in a citation card
88
+ * (`CkCitation` / `CkCitationCard`). Falls back to `defaultChatKitLabels`.
89
+ */
90
+ citationOpenSource?: string;
91
+ /**
92
+ * Prefix for a citation marker's accessible name in `CkCitation`, composed
93
+ * as `"{citationSourceLabel} {index}"` (e.g. "Source 1"). Falls back to
94
+ * `defaultChatKitLabels`.
95
+ */
96
+ citationSourceLabel?: string;
84
97
  /** Fallback title used by `CkHistory` when a conversation has no `title`. */
85
98
  historyItemUntitled: string;
86
99
  /** Accessible name for the trailing kebab trigger on each `CkHistory` row. */
@@ -103,6 +116,7 @@ export const defaultChatKitLabels: ChatKitLabels = {
103
116
  inputPlaceholder: 'Type a message...',
104
117
  copyMessageAction: 'Copy response',
105
118
  copyMessageActionCopied: 'Copied!',
119
+ regenerateMessageAction: 'Regenerate response',
106
120
  feedbackThumbsUp: 'Good response',
107
121
  feedbackThumbsDown: 'Bad response',
108
122
  feedbackGroup: 'Rate this response',
@@ -123,6 +137,8 @@ export const defaultChatKitLabels: ChatKitLabels = {
123
137
  modelSelectorListAriaLabel: 'Models',
124
138
  modelSelectorPlaceholder: 'Select model',
125
139
  markdownTableLabel: 'Table',
140
+ citationOpenSource: 'Open source',
141
+ citationSourceLabel: 'Source',
126
142
  historyItemUntitled: 'Untitled conversation',
127
143
  historyItemActionAriaLabel: 'Conversation options',
128
144
  historyItemEditAction: 'Edit',
@@ -83,8 +83,11 @@ function buildProcessor(plugins: PluggableList): SyncProcessor {
83
83
  .use(remarkNormalizeMath)
84
84
  .use(remarkGfm)
85
85
  .use(remarkMath);
86
- for (const plugin of plugins) {
87
- proc = proc.use(plugin as any);
86
+ // Apply the whole list at once. unified resolves `[plugin, options]` tuples
87
+ // when they're entries of a list; calling `.use(tuple)` per entry instead
88
+ // misreads the options object as a preset and throws.
89
+ if (plugins.length > 0) {
90
+ proc = proc.use(plugins);
88
91
  }
89
92
  return proc.use(remarkRehype).use(rehypeKatex).use(rehypeStringify);
90
93
  }
@@ -146,6 +146,36 @@ describe('useMessageHandler', () => {
146
146
  expect('metadata' in msg).toBe(false);
147
147
  expect('metadata' in handler.messages.value[0]).toBe(false);
148
148
  });
149
+
150
+ it('branches off an explicit parentId, updating the parent and activating the new branch', () => {
151
+ const handler = useMessageHandler();
152
+
153
+ handler.setCurrentUserMessage('First');
154
+ const first = handler.commitUserMessage();
155
+ handler.setCurrentUserMessage('Second');
156
+ const second = handler.commitUserMessage();
157
+
158
+ // Branch a sibling of `second` off their shared parent `first`.
159
+ handler.setCurrentUserMessage('Sibling');
160
+ const sibling = handler.commitUserMessage({ parentId: first.id });
161
+
162
+ const parent = handler.getTree().nodes[first.id];
163
+ expect(parent.childrenIds).toEqual([second.id, sibling.id]);
164
+ // The freshly created branch becomes the active path.
165
+ expect(parent.activeChildIndex).toBe(1);
166
+ expect(handler.messages.value.map((m) => m.id)).toEqual([
167
+ first.id,
168
+ sibling.id,
169
+ ]);
170
+ });
171
+
172
+ it('throws when branching off an unknown parentId', () => {
173
+ const handler = useMessageHandler();
174
+ handler.setCurrentUserMessage('Hello');
175
+ expect(() =>
176
+ handler.commitUserMessage({ parentId: 'does-not-exist' }),
177
+ ).toThrow('Unknown parent message id: does-not-exist');
178
+ });
149
179
  });
150
180
 
151
181
  describe('updateMessage', () => {
@@ -342,6 +372,55 @@ describe('useMessageHandler', () => {
342
372
  expect(handler.messages.value).toHaveLength(2);
343
373
  expect(getText(handler.messages.value[1].content)).toBe('Streaming...');
344
374
  });
375
+
376
+ it('branches a new node off an explicit parentId on create', () => {
377
+ const handler = useMessageHandler();
378
+ handler.setTree(
379
+ makeTree(
380
+ {
381
+ u1: makeTreeMessage({ id: 'u1', content: [text('Hello')] }),
382
+ a1: makeTreeMessage({
383
+ id: 'a1',
384
+ parentId: 'u1',
385
+ role: 'assistant',
386
+ content: [text('Hi')],
387
+ }),
388
+ },
389
+ 'u1',
390
+ ),
391
+ );
392
+
393
+ // Regenerate: add a sibling assistant answer under the same user turn.
394
+ handler.updateMessage({
395
+ id: 'a2',
396
+ content: [text('Hi again')],
397
+ role: 'assistant',
398
+ status: 'completed',
399
+ parentId: 'u1',
400
+ });
401
+
402
+ const parent = handler.getTree().nodes.u1;
403
+ expect(parent.childrenIds).toEqual(['a1', 'a2']);
404
+ expect(parent.activeChildIndex).toBe(1);
405
+ expect(handler.messages.value.map((m) => m.id)).toEqual(['u1', 'a2']);
406
+ });
407
+
408
+ it('ignores parentId when the node already exists', () => {
409
+ const handler = useMessageHandler();
410
+ handler.setCurrentUserMessage('Hello');
411
+ const msg = handler.commitUserMessage();
412
+
413
+ handler.updateMessage({
414
+ id: msg.id,
415
+ content: [text('Updated')],
416
+ role: 'user',
417
+ status: 'completed',
418
+ parentId: 'does-not-exist',
419
+ });
420
+
421
+ expect(getText(handler.getMessageById(msg.id)!.content)).toBe('Updated');
422
+ expect(handler.getTree().nodes[msg.id].parentId).toBeNull();
423
+ });
345
424
  });
346
425
 
347
426
  describe('getMessageById', () => {