@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.
- 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
- package/dist/adapters/responses-api.d.ts +11 -5
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +9 -3
- package/dist/chat-kit.css +14 -8
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +7 -0
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
- package/dist/components/base/CkAgentMessage/types.d.ts +1 -1
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts +47 -0
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -0
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts +15 -0
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -0
- package/dist/components/base/CkCitation/index.d.ts +3 -0
- package/dist/components/base/CkCitation/index.d.ts.map +1 -0
- package/dist/components/base/CkCitation/types.d.ts +79 -0
- package/dist/components/base/CkCitation/types.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +51 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +13 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/index.d.ts +3 -0
- package/dist/components/base/CkCitationCard/index.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/types.d.ts +51 -0
- package/dist/components/base/CkCitationCard/types.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts → CkCopyMessageButton/CkCopyMessageButton.stories.d.ts} +3 -3
- package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts → CkCopyMessageButton/CkCopyMessageButton.vue.d.ts} +4 -4
- package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkCopyMessageButton/index.d.ts +3 -0
- package/dist/components/base/CkCopyMessageButton/index.d.ts.map +1 -0
- package/dist/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.d.ts +3 -3
- package/dist/components/base/CkCopyMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +1 -1
- package/dist/components/base/CkFeedbackButton/types.d.ts +1 -1
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
- package/dist/components/base/CkFeedbackDialog/types.d.ts +1 -1
- package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +15 -0
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts +16 -2
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
- package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/types.d.ts +25 -0
- package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts +17 -0
- package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.stories.d.ts → CkMessageButton/CkMessageButton.stories.d.ts} +3 -3
- package/dist/components/base/CkMessageButton/CkMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.vue.d.ts → CkMessageButton/CkMessageButton.vue.d.ts} +3 -3
- package/dist/components/base/CkMessageButton/CkMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkMessageButton/index.d.ts +3 -0
- package/dist/components/base/CkMessageButton/index.d.ts.map +1 -0
- package/dist/components/base/{CkMessageActionButton → CkMessageButton}/types.d.ts +2 -2
- package/dist/components/base/CkMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts +16 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts +12 -0
- package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkRegenerateMessageButton/index.d.ts +3 -0
- package/dist/components/base/{CkCopyMessageActionButton → CkRegenerateMessageButton}/index.d.ts.map +1 -1
- package/dist/components/base/CkRegenerateMessageButton/types.d.ts +27 -0
- package/dist/components/base/CkRegenerateMessageButton/types.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +8 -3
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +1 -1
- package/dist/components/composed/CkConversation/types.d.ts +1 -1
- package/dist/components/composed/CkInput/CkInput.vue.d.ts +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +26 -23
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +4 -3
- package/dist/handlers/useChatKitLabels.d.ts +13 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
- package/dist/handlers/useMessageHandler.d.ts +37 -0
- package/dist/handlers/useMessageHandler.d.ts.map +1 -1
- package/dist/helpers/conversationTree.d.ts +15 -0
- package/dist/helpers/conversationTree.d.ts.map +1 -1
- package/dist/helpers/unified/remarkCitations.d.ts +17 -0
- package/dist/helpers/unified/remarkCitations.d.ts.map +1 -0
- package/dist/{index-DAmWGYNH.js → index-CyrVnbS9.js} +86 -71
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -27
- package/dist/{useChatKitLabels-D5gNv8se.js → useChatKitLabels-zMFV7aEz.js} +31 -3
- package/dist/{useMessageHandler-CMdy-lm0.js → useMessageHandler-N50Yj-NU.js} +45 -7
- package/package.json +5 -3
- package/src/adapters/responses-api.spec.ts +52 -5
- package/src/adapters/responses-api.ts +26 -7
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +112 -0
- package/src/components/base/CkAgentMessage/types.ts +1 -1
- package/src/components/base/CkCitation/CkCitation.spec.ts +79 -0
- package/src/components/base/CkCitation/CkCitation.stories.ts +136 -0
- package/src/components/base/CkCitation/CkCitation.vue +145 -0
- package/src/components/base/CkCitation/index.ts +2 -0
- package/src/components/base/CkCitation/types.ts +80 -0
- package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +77 -0
- package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +144 -0
- package/src/components/base/CkCitationCard/CkCitationCard.vue +76 -0
- package/src/components/base/CkCitationCard/index.ts +2 -0
- package/src/components/base/CkCitationCard/types.ts +51 -0
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts → CkCopyMessageButton/CkCopyMessageButton.spec.ts} +6 -6
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts → CkCopyMessageButton/CkCopyMessageButton.stories.ts} +8 -8
- package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue → CkCopyMessageButton/CkCopyMessageButton.vue} +6 -6
- package/src/components/base/CkCopyMessageButton/index.ts +2 -0
- package/src/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.ts +3 -3
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +5 -5
- package/src/components/base/CkFeedbackButton/types.ts +1 -1
- package/src/components/base/CkFeedbackDialog/types.ts +1 -1
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +124 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +118 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +70 -28
- package/src/components/base/CkMarkdownRenderer/index.ts +1 -0
- package/src/components/base/CkMarkdownRenderer/types.ts +26 -0
- package/src/components/base/CkMarkdownRenderer/useCitationRendering.spec.ts +94 -0
- package/src/components/base/CkMarkdownRenderer/useCitationRendering.ts +52 -0
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.spec.ts → CkMessageButton/CkMessageButton.spec.ts} +6 -6
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.stories.ts → CkMessageButton/CkMessageButton.stories.ts} +10 -10
- package/src/components/base/{CkMessageActionButton/CkMessageActionButton.vue → CkMessageButton/CkMessageButton.vue} +3 -3
- package/src/components/base/CkMessageButton/index.ts +2 -0
- package/src/components/base/{CkMessageActionButton → CkMessageButton}/types.ts +2 -2
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.spec.ts +49 -0
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.ts +63 -0
- package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue +43 -0
- package/src/components/base/CkRegenerateMessageButton/index.ts +2 -0
- package/src/components/base/CkRegenerateMessageButton/types.ts +27 -0
- package/src/components/base/index.ts +15 -2
- package/src/components/composed/CkConversation/CkConversation.stories.ts +6 -6
- package/src/components/composed/CkConversation/CkConversation.vue +2 -2
- package/src/components/composed/CkConversation/types.ts +1 -1
- package/src/components/index.ts +11 -2
- package/src/composables/fileUpload/useFileUpload.spec.ts +2 -61
- package/src/composables/fileUpload/useFileUpload.ts +2 -9
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +195 -14
- package/src/composables/fileUpload/useFileUploadWrapper.ts +114 -81
- package/src/handlers/index.ts +2 -0
- package/src/handlers/useChatKitLabels.spec.ts +1 -0
- package/src/handlers/useChatKitLabels.ts +16 -0
- package/src/handlers/useMarkdownParser.ts +5 -2
- package/src/handlers/useMessageHandler.spec.ts +79 -0
- package/src/handlers/useMessageHandler.ts +50 -2
- package/src/helpers/conversationTree.spec.ts +120 -0
- package/src/helpers/conversationTree.ts +57 -8
- package/src/helpers/unified/remarkCitations.spec.ts +112 -0
- package/src/helpers/unified/remarkCitations.ts +55 -0
- package/src/index.ts +11 -2
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkCopyMessageActionButton/index.d.ts +0 -3
- package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/index.d.ts +0 -3
- package/dist/components/base/CkMessageActionButton/index.d.ts.map +0 -1
- package/dist/components/base/CkMessageActionButton/types.d.ts.map +0 -1
- package/src/components/base/CkCopyMessageActionButton/index.ts +0 -2
- 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.
|
|
97
|
+
if (method === 'GET' && url.endsWith('search-stores')) {
|
|
98
98
|
return createMockFetchResponse({
|
|
99
99
|
items: [
|
|
100
100
|
{
|
|
101
|
-
id: '
|
|
102
|
-
name: '
|
|
103
|
-
|
|
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('
|
|
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
|
|
180
|
+
// ---- Search store resolution (one per conversation) ----
|
|
183
181
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
219
|
+
`search-stores/${storeId}/workflows`,
|
|
234
220
|
);
|
|
221
|
+
const existing = findConfiguredWorkflow(response.items);
|
|
235
222
|
|
|
236
|
-
if (
|
|
237
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
240
|
+
await httpClient.post(
|
|
241
|
+
`search-stores/${storeRes.id}/workflows`,
|
|
242
|
+
workflowConfig,
|
|
243
|
+
);
|
|
257
244
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return resolvedStoreId;
|
|
261
|
-
}
|
|
245
|
+
return storeRes.id;
|
|
246
|
+
}
|
|
262
247
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 (
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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>(
|
|
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
|
|
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
|
|
349
|
-
|
|
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 >
|
|
431
|
-
const kept = Math.max(0,
|
|
463
|
+
if (fileList.length > resolvedMaxFiles) {
|
|
464
|
+
const kept = Math.max(0, resolvedMaxFiles);
|
|
432
465
|
options?.onFilesExceeded?.({
|
|
433
466
|
selected: fileList.length,
|
|
434
|
-
allowed:
|
|
467
|
+
allowed: resolvedMaxFiles,
|
|
435
468
|
kept,
|
|
436
469
|
});
|
|
437
470
|
fileList = fileList.slice(0, kept);
|
package/src/handlers/index.ts
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
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', () => {
|