@aleph-alpha/chat-kit 3.1.3 → 3.3.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-LPrW7CSQ.js → CkInput.vue_vue_type_script_setup_true_lang-BAcBFs6I.js} +302 -71
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +63 -37
- package/dist/chat-kit.css +6 -0
- 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/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/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +1 -1
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
- 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/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
- package/dist/components/base/index.d.ts +5 -1
- package/dist/components/base/index.d.ts.map +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 +25 -23
- package/dist/composables/fileUpload/types.d.ts +3 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -1
- 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 +3 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +4 -1
- package/dist/handlers/useChatKitLabels.d.ts +11 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMarkdownParser.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/helpers/unified/remarkNormalizeMath.d.ts +24 -0
- package/dist/helpers/unified/remarkNormalizeMath.d.ts.map +1 -0
- package/dist/{index-DVxCNAs3.js → index-C6njJ8EB.js} +96 -49
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -26
- package/dist/{useChatKitLabels-Ctz1dHpx.js → useChatKitLabels-BM2I7-9o.js} +126 -3
- package/package.json +6 -3
- package/src/adapters/responses-api.spec.ts +156 -0
- package/src/adapters/responses-api.ts +125 -58
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +112 -0
- 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/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/index.ts +12 -0
- package/src/components/index.ts +8 -0
- package/src/composables/fileUpload/types.ts +6 -0
- package/src/composables/fileUpload/useFileUpload.spec.ts +2 -61
- package/src/composables/fileUpload/useFileUpload.ts +9 -10
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +241 -10
- package/src/composables/fileUpload/useFileUploadWrapper.ts +126 -53
- package/src/handlers/index.ts +6 -0
- package/src/handlers/useChatKitLabels.ts +13 -0
- package/src/handlers/useMarkdownParser.spec.ts +34 -0
- package/src/handlers/useMarkdownParser.ts +15 -3
- package/src/helpers/unified/remarkCitations.spec.ts +112 -0
- package/src/helpers/unified/remarkCitations.ts +55 -0
- package/src/helpers/unified/remarkNormalizeMath.spec.ts +84 -0
- package/src/helpers/unified/remarkNormalizeMath.ts +134 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { computed, h, toValue, type MaybeRefOrGetter, type VNode } from 'vue';
|
|
2
|
+
import type { PluggableList } from 'unified';
|
|
3
|
+
import { remarkCitations } from '../../../helpers/unified/remarkCitations';
|
|
4
|
+
import { CkCitation } from '../CkCitation';
|
|
5
|
+
import type { CkCitationSource } from '../CkCitation';
|
|
6
|
+
|
|
7
|
+
/** Scope passed to the `citation` slot when overriding how a source renders. */
|
|
8
|
+
export type CkCitationSlot = (props: {
|
|
9
|
+
source: CkCitationSource;
|
|
10
|
+
openSource: (url: string) => void;
|
|
11
|
+
}) => VNode[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The citation concern of `CkMarkdownRenderer`, extracted so the renderer stays
|
|
15
|
+
* markdown-focused and this logic is testable on its own.
|
|
16
|
+
*/
|
|
17
|
+
export function useCitationRendering(
|
|
18
|
+
citations: MaybeRefOrGetter<CkCitationSource[] | undefined>,
|
|
19
|
+
onOpenSource: (url: string) => void,
|
|
20
|
+
) {
|
|
21
|
+
const sourcesByIndex = computed(
|
|
22
|
+
() =>
|
|
23
|
+
new Map(
|
|
24
|
+
(toValue(citations) ?? []).map((source) => [source.index, source]),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const citationPlugins = computed<PluggableList>(() => {
|
|
29
|
+
const list = toValue(citations);
|
|
30
|
+
if (!list?.length) return [];
|
|
31
|
+
const indices = new Set(list.map((source) => source.index));
|
|
32
|
+
return [[remarkCitations, { indices }]];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Returns null for non-citation elements so the renderer handles them normally.
|
|
36
|
+
function renderCitationNode(
|
|
37
|
+
el: Element,
|
|
38
|
+
slot?: CkCitationSlot,
|
|
39
|
+
): VNode | VNode[] | null {
|
|
40
|
+
if (!el.hasAttribute('data-citation-index')) return null;
|
|
41
|
+
|
|
42
|
+
const index = Number(el.getAttribute('data-citation-index'));
|
|
43
|
+
const source = sourcesByIndex.value.get(index);
|
|
44
|
+
if (!source) return null;
|
|
45
|
+
|
|
46
|
+
// Expose the same handler so slot content can forward `open-source`.
|
|
47
|
+
if (slot) return slot({ source, openSource: onOpenSource });
|
|
48
|
+
return h(CkCitation, { ...source, onOpenSource });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { citationPlugins, renderCitationNode };
|
|
52
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
export { CkSendIcon, CkStopIcon } from './icons';
|
|
2
2
|
export { CkAgentMessage } from './CkAgentMessage';
|
|
3
|
+
export { CkCitation } from './CkCitation';
|
|
4
|
+
export type {
|
|
5
|
+
CkCitationProps,
|
|
6
|
+
CkCitationEmits,
|
|
7
|
+
CkCitationSource,
|
|
8
|
+
} from './CkCitation';
|
|
9
|
+
export { CkCitationCard } from './CkCitationCard';
|
|
10
|
+
export type {
|
|
11
|
+
CkCitationCardProps,
|
|
12
|
+
CkCitationCardEmits,
|
|
13
|
+
} from './CkCitationCard';
|
|
3
14
|
export { CkConversationLayout } from './CkConversationLayout';
|
|
4
15
|
export type { CkConversationLayoutProps } from './CkConversationLayout';
|
|
5
16
|
export { CkCopyMessageActionButton } from './CkCopyMessageActionButton';
|
|
@@ -36,6 +47,7 @@ export type { CkHistoryWrapperProps } from './CkHistoryWrapper';
|
|
|
36
47
|
export { CkMarkdownRenderer } from './CkMarkdownRenderer';
|
|
37
48
|
export type {
|
|
38
49
|
CkMarkdownRendererProps,
|
|
50
|
+
CkMarkdownRendererEmits,
|
|
39
51
|
CkMarkdownElementClasses,
|
|
40
52
|
} from './CkMarkdownRenderer';
|
|
41
53
|
export { CkMessageActionButton } from './CkMessageActionButton';
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Base (presentational) components
|
|
2
2
|
export {
|
|
3
3
|
CkAgentMessage,
|
|
4
|
+
CkCitation,
|
|
5
|
+
CkCitationCard,
|
|
4
6
|
CkConversationLayout,
|
|
5
7
|
CkCopyMessageActionButton,
|
|
6
8
|
CkDropZoneOverlay,
|
|
@@ -22,6 +24,11 @@ export {
|
|
|
22
24
|
CkUserMessage,
|
|
23
25
|
} from './base';
|
|
24
26
|
export type {
|
|
27
|
+
CkCitationProps,
|
|
28
|
+
CkCitationEmits,
|
|
29
|
+
CkCitationSource,
|
|
30
|
+
CkCitationCardProps,
|
|
31
|
+
CkCitationCardEmits,
|
|
25
32
|
CkConversationLayoutProps,
|
|
26
33
|
CkDropZoneOverlayProps,
|
|
27
34
|
CkDropZoneOverlayEmits,
|
|
@@ -42,6 +49,7 @@ export type {
|
|
|
42
49
|
CkHistoryWrapperProps,
|
|
43
50
|
CkMarkdownElementClasses,
|
|
44
51
|
CkMarkdownRendererProps,
|
|
52
|
+
CkMarkdownRendererEmits,
|
|
45
53
|
CkModelOption,
|
|
46
54
|
CkModelSelectorProps,
|
|
47
55
|
CkModelSelectorEmits,
|
|
@@ -34,6 +34,8 @@ export interface EndpointResolver {
|
|
|
34
34
|
files: (storeId: string) => string;
|
|
35
35
|
file: (storeId: string, fileId: string) => string;
|
|
36
36
|
filePublicUrl: (storeId: string, fileId: string) => string;
|
|
37
|
+
workflows: (storeId: string) => string;
|
|
38
|
+
createWorkflow: (storeId: string) => string;
|
|
37
39
|
workflowRun: (storeId: string, workflowId: string) => string;
|
|
38
40
|
workflowRunEventStream: (
|
|
39
41
|
storeId: string,
|
|
@@ -47,6 +49,9 @@ export const defaultEndpoints: EndpointResolver = {
|
|
|
47
49
|
file: (storeId, fileId) => `search-stores/${storeId}/files/${fileId}`,
|
|
48
50
|
filePublicUrl: (storeId, fileId) =>
|
|
49
51
|
`search-stores/${storeId}/files/${fileId}/public-urls?ttl=1h`,
|
|
52
|
+
workflows: (storeId) =>
|
|
53
|
+
`search-stores/${storeId}/workflows?limit=20&offset=0&created_after=&created_before=`,
|
|
54
|
+
createWorkflow: (storeId) => `search-stores/${storeId}/workflows`,
|
|
50
55
|
workflowRun: (storeId, workflowId) =>
|
|
51
56
|
`search-stores/${storeId}/workflows/${workflowId}/runs`,
|
|
52
57
|
workflowRunEventStream: (storeId, workflowId, runId) =>
|
|
@@ -410,6 +415,7 @@ export interface UseFileUploadReturn {
|
|
|
410
415
|
updateFileMetadata: (id: string, meta: Record<string, unknown>) => void;
|
|
411
416
|
clearState: () => void;
|
|
412
417
|
setStoreId: (id: string) => void;
|
|
418
|
+
setWorkflowId: (id: string) => void;
|
|
413
419
|
getPublicUrl: (fileId: string) => Promise<string>;
|
|
414
420
|
|
|
415
421
|
scopeBy: (metadataKey: string, value: string) => ScopedFileView;
|
|
@@ -199,33 +199,7 @@ describe('useFileUpload maxFilesPerUpload', () => {
|
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
it('
|
|
203
|
-
const client = createUploadMockClient();
|
|
204
|
-
(client.post as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
205
|
-
{ id: 'remote-1' },
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
const upload = useFileUpload({
|
|
209
|
-
client,
|
|
210
|
-
workflowId: 'wf',
|
|
211
|
-
storeId: 'ss-1',
|
|
212
|
-
maxFilesPerUpload: 3,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
upload.files.value.set('existing-1', makeSuccessEntry('existing-1', 'r-1'));
|
|
216
|
-
upload.files.value.set('existing-2', makeSuccessEntry('existing-2', 'r-2'));
|
|
217
|
-
|
|
218
|
-
await expect(
|
|
219
|
-
upload.upload({
|
|
220
|
-
id: 'g2',
|
|
221
|
-
files: [createFile('a.pdf'), createFile('b.pdf')],
|
|
222
|
-
}),
|
|
223
|
-
).rejects.toMatchObject({
|
|
224
|
-
code: 'MAX_FILES_EXCEEDED',
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('allows upload when active files plus new batch is within limit', async () => {
|
|
202
|
+
it('allows upload when batch is within limit regardless of existing active files', async () => {
|
|
229
203
|
const client = createUploadMockClient();
|
|
230
204
|
(client.post as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
231
205
|
{ id: 'remote-new' },
|
|
@@ -239,6 +213,7 @@ describe('useFileUpload maxFilesPerUpload', () => {
|
|
|
239
213
|
});
|
|
240
214
|
|
|
241
215
|
upload.files.value.set('existing-1', makeSuccessEntry('existing-1', 'r-1'));
|
|
216
|
+
upload.files.value.set('existing-2', makeSuccessEntry('existing-2', 'r-2'));
|
|
242
217
|
|
|
243
218
|
const result = await upload.upload({
|
|
244
219
|
id: 'g3',
|
|
@@ -247,38 +222,4 @@ describe('useFileUpload maxFilesPerUpload', () => {
|
|
|
247
222
|
|
|
248
223
|
expect(result.results).toHaveLength(2);
|
|
249
224
|
});
|
|
250
|
-
|
|
251
|
-
it('does not count failed/cancelled/removed files towards active count', async () => {
|
|
252
|
-
const client = createUploadMockClient();
|
|
253
|
-
(client.post as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
254
|
-
{ id: 'remote-new' },
|
|
255
|
-
]);
|
|
256
|
-
|
|
257
|
-
const upload = useFileUpload({
|
|
258
|
-
client,
|
|
259
|
-
workflowId: 'wf',
|
|
260
|
-
storeId: 'ss-1',
|
|
261
|
-
maxFilesPerUpload: 2,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const failedEntry: FileEntry = {
|
|
265
|
-
...makeEntry('failed-1', 'r-f1'),
|
|
266
|
-
status: 'failed',
|
|
267
|
-
error: { code: 'UPLOAD_FAILED', message: 'err', occurredAt: 'now' },
|
|
268
|
-
};
|
|
269
|
-
const cancelledEntry: FileEntry = {
|
|
270
|
-
...makeEntry('cancelled-1', 'r-c1'),
|
|
271
|
-
status: 'cancelled',
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
upload.files.value.set('failed-1', failedEntry);
|
|
275
|
-
upload.files.value.set('cancelled-1', cancelledEntry);
|
|
276
|
-
|
|
277
|
-
const result = await upload.upload({
|
|
278
|
-
id: 'g4',
|
|
279
|
-
files: [createFile('a.pdf'), createFile('b.pdf')],
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
expect(result.results).toHaveLength(2);
|
|
283
|
-
});
|
|
284
225
|
});
|
|
@@ -469,8 +469,9 @@ function deriveStateKey(
|
|
|
469
469
|
export function useFileUpload(
|
|
470
470
|
options: UseFileUploadOptions,
|
|
471
471
|
): UseFileUploadReturn {
|
|
472
|
-
const { client
|
|
472
|
+
const { client } = options;
|
|
473
473
|
let storeId = options.storeId ?? '';
|
|
474
|
+
let workflowId = options.workflowId;
|
|
474
475
|
const endpoints: EndpointResolver = options.endpoints ?? defaultEndpoints;
|
|
475
476
|
const retryPolicy: RetryPolicy = options.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
476
477
|
const constraints: FileConstraints = {
|
|
@@ -706,17 +707,10 @@ export function useFileUpload(
|
|
|
706
707
|
? params.files
|
|
707
708
|
: [params.files];
|
|
708
709
|
|
|
709
|
-
|
|
710
|
-
(e) =>
|
|
711
|
-
e.status !== 'failed' &&
|
|
712
|
-
e.status !== 'cancelled' &&
|
|
713
|
-
e.status !== 'removed',
|
|
714
|
-
).length;
|
|
715
|
-
|
|
716
|
-
if (activeFileCount + fileList.length > constraints.maxFilesPerUpload) {
|
|
710
|
+
if (fileList.length > constraints.maxFilesPerUpload) {
|
|
717
711
|
throw new SdkError(
|
|
718
712
|
'MAX_FILES_EXCEEDED',
|
|
719
|
-
`Cannot upload ${fileList.length} file(s) —
|
|
713
|
+
`Cannot upload ${fileList.length} file(s) — maximum per selection is ${constraints.maxFilesPerUpload}.`,
|
|
720
714
|
);
|
|
721
715
|
}
|
|
722
716
|
|
|
@@ -1257,6 +1251,10 @@ export function useFileUpload(
|
|
|
1257
1251
|
storeId = id;
|
|
1258
1252
|
}
|
|
1259
1253
|
|
|
1254
|
+
function setWorkflowId(id: string) {
|
|
1255
|
+
workflowId = id;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1260
1258
|
async function getPublicUrl(fileId: string): Promise<string> {
|
|
1261
1259
|
const res = await client.post<{ url: string; expires_at: string }>(
|
|
1262
1260
|
endpoints.filePublicUrl(storeId, fileId),
|
|
@@ -1277,6 +1275,7 @@ export function useFileUpload(
|
|
|
1277
1275
|
updateFileMetadata,
|
|
1278
1276
|
clearState,
|
|
1279
1277
|
setStoreId,
|
|
1278
|
+
setWorkflowId,
|
|
1280
1279
|
getPublicUrl,
|
|
1281
1280
|
|
|
1282
1281
|
scopeBy,
|
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils';
|
|
2
|
+
import { ref } from 'vue';
|
|
1
3
|
import { createDocumentUploadProvider } from './useFileUploadWrapper';
|
|
2
4
|
import type { DocumentUploadConfig } from './types';
|
|
3
5
|
|
|
6
|
+
const uploadMock = vi.hoisted(() => ({
|
|
7
|
+
upload: vi.fn().mockResolvedValue({ results: [] }),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('./useFileUpload', () => ({
|
|
11
|
+
useFileUpload: vi.fn((options) => {
|
|
12
|
+
const scopedFiles = ref([]);
|
|
13
|
+
return {
|
|
14
|
+
files: ref(new Map()),
|
|
15
|
+
getSupportedFileTypes: () => ({
|
|
16
|
+
maxFilesPerUpload: options.maxFilesPerUpload ?? 20,
|
|
17
|
+
allowedMimeTypes: ['application/pdf'],
|
|
18
|
+
allowedExtensions: ['.pdf'],
|
|
19
|
+
maxFileSizeBytes: 100 * 1024 * 1024,
|
|
20
|
+
}),
|
|
21
|
+
scopeBy: () => ({ files: scopedFiles }),
|
|
22
|
+
upload: uploadMock.upload,
|
|
23
|
+
runWorkflow: vi.fn().mockResolvedValue({ close: vi.fn() }),
|
|
24
|
+
setStoreId: vi.fn(),
|
|
25
|
+
setWorkflowId: vi.fn(),
|
|
26
|
+
updateFileMetadata: vi.fn(),
|
|
27
|
+
removeFile: vi.fn(),
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
4
32
|
function createFile(name: string, size = 100): File {
|
|
5
33
|
return new File(['x'.repeat(size)], name, { type: 'application/pdf' });
|
|
6
34
|
}
|
|
@@ -31,6 +59,7 @@ function buildConfig(
|
|
|
31
59
|
return {
|
|
32
60
|
baseUrl: 'https://test.example.com',
|
|
33
61
|
workflowId: 'wf-1',
|
|
62
|
+
persist: false,
|
|
34
63
|
searchStoreConfig: {
|
|
35
64
|
models: [
|
|
36
65
|
{
|
|
@@ -48,28 +77,43 @@ function buildConfig(
|
|
|
48
77
|
};
|
|
49
78
|
}
|
|
50
79
|
|
|
80
|
+
async function settleAsyncUploads() {
|
|
81
|
+
await flushPromises();
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
52
85
|
let fetchMock: ReturnType<typeof vi.fn>;
|
|
53
86
|
|
|
54
87
|
beforeEach(() => {
|
|
88
|
+
uploadMock.upload.mockClear();
|
|
89
|
+
uploadMock.upload.mockResolvedValue({ results: [] });
|
|
90
|
+
|
|
55
91
|
fetchMock = vi.fn();
|
|
56
92
|
vi.stubGlobal('fetch', fetchMock);
|
|
57
93
|
|
|
58
94
|
fetchMock.mockImplementation((url: string, init?: RequestInit) => {
|
|
59
95
|
const method = init?.method ?? 'GET';
|
|
60
96
|
|
|
61
|
-
if (method === 'GET' && url.
|
|
97
|
+
if (method === 'GET' && url.endsWith('search-stores')) {
|
|
62
98
|
return createMockFetchResponse({
|
|
63
99
|
items: [
|
|
64
100
|
{
|
|
65
101
|
id: 'store-1',
|
|
66
|
-
name: '
|
|
102
|
+
name: 'conv-1',
|
|
67
103
|
models: [{ embedding_model: 'test-model' }],
|
|
68
104
|
},
|
|
69
105
|
],
|
|
70
106
|
total: 1,
|
|
71
107
|
});
|
|
72
108
|
}
|
|
109
|
+
if (method === 'GET' && url.includes('/workflows')) {
|
|
110
|
+
return createMockFetchResponse({
|
|
111
|
+
items: [{ id: 'wf-1', name: 'wf-1' }],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (method === 'POST' && url.endsWith('search-stores')) {
|
|
115
|
+
return createMockFetchResponse({ id: 'new-store-id' });
|
|
116
|
+
}
|
|
73
117
|
if (method === 'POST' && url.includes('/files')) {
|
|
74
118
|
return createMockFetchResponse([{ id: 'remote-1' }]);
|
|
75
119
|
}
|
|
@@ -90,7 +134,8 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
90
134
|
});
|
|
91
135
|
});
|
|
92
136
|
|
|
93
|
-
afterEach(() => {
|
|
137
|
+
afterEach(async () => {
|
|
138
|
+
await settleAsyncUploads();
|
|
94
139
|
vi.unstubAllGlobals();
|
|
95
140
|
});
|
|
96
141
|
|
|
@@ -115,26 +160,31 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
115
160
|
const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
|
|
116
161
|
|
|
117
162
|
const onFilesExceeded = vi.fn();
|
|
118
|
-
const
|
|
163
|
+
const files = [createFile('a.pdf'), createFile('b.pdf')];
|
|
164
|
+
const event = makeInputEvent(files);
|
|
119
165
|
|
|
120
166
|
handleFilesSelected(event, { onFilesExceeded });
|
|
121
167
|
|
|
122
168
|
expect(onFilesExceeded).not.toHaveBeenCalled();
|
|
169
|
+
await settleAsyncUploads();
|
|
170
|
+
expect(uploadMock.upload).toHaveBeenCalledOnce();
|
|
171
|
+
expect(uploadMock.upload.mock.calls[0]?.[0].files).toEqual(files);
|
|
123
172
|
});
|
|
124
173
|
|
|
125
|
-
it('truncates files to available slots and calls onFilesExceeded', () => {
|
|
174
|
+
it('truncates files to available slots and calls onFilesExceeded', async () => {
|
|
126
175
|
const provider = createDocumentUploadProvider(
|
|
127
176
|
buildConfig({ maxFilesPerUpload: 2 }),
|
|
128
177
|
);
|
|
129
178
|
const { handleFilesSelected } = provider.useDocumentUpload('conv-1');
|
|
130
179
|
|
|
131
180
|
const onFilesExceeded = vi.fn();
|
|
132
|
-
const
|
|
181
|
+
const files = [
|
|
133
182
|
createFile('a.pdf'),
|
|
134
183
|
createFile('b.pdf'),
|
|
135
184
|
createFile('c.pdf'),
|
|
136
185
|
createFile('d.pdf'),
|
|
137
|
-
]
|
|
186
|
+
];
|
|
187
|
+
const event = makeInputEvent(files);
|
|
138
188
|
|
|
139
189
|
handleFilesSelected(event, { onFilesExceeded });
|
|
140
190
|
|
|
@@ -143,9 +193,13 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
143
193
|
allowed: 2,
|
|
144
194
|
kept: 2,
|
|
145
195
|
});
|
|
196
|
+
await settleAsyncUploads();
|
|
197
|
+
expect(uploadMock.upload.mock.calls[0]?.[0].files).toEqual(
|
|
198
|
+
files.slice(0, 2),
|
|
199
|
+
);
|
|
146
200
|
});
|
|
147
201
|
|
|
148
|
-
it('resets input value after handling', () => {
|
|
202
|
+
it('resets input value after handling', async () => {
|
|
149
203
|
const provider = createDocumentUploadProvider(
|
|
150
204
|
buildConfig({ maxFilesPerUpload: 5 }),
|
|
151
205
|
);
|
|
@@ -159,6 +213,7 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
159
213
|
handleFilesSelected(event);
|
|
160
214
|
|
|
161
215
|
expect(valueSetter).toHaveBeenCalledWith('');
|
|
216
|
+
await settleAsyncUploads();
|
|
162
217
|
});
|
|
163
218
|
|
|
164
219
|
it('does not upload when no slots are available', async () => {
|
|
@@ -177,9 +232,11 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
177
232
|
allowed: 0,
|
|
178
233
|
kept: 0,
|
|
179
234
|
});
|
|
235
|
+
await settleAsyncUploads();
|
|
236
|
+
expect(uploadMock.upload).not.toHaveBeenCalled();
|
|
180
237
|
});
|
|
181
238
|
|
|
182
|
-
it('does not call onFilesExceeded when exactly at the limit', () => {
|
|
239
|
+
it('does not call onFilesExceeded when exactly at the limit', async () => {
|
|
183
240
|
const provider = createDocumentUploadProvider(
|
|
184
241
|
buildConfig({ maxFilesPerUpload: 3 }),
|
|
185
242
|
);
|
|
@@ -195,9 +252,11 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
195
252
|
handleFilesSelected(event, { onFilesExceeded });
|
|
196
253
|
|
|
197
254
|
expect(onFilesExceeded).not.toHaveBeenCalled();
|
|
255
|
+
await settleAsyncUploads();
|
|
256
|
+
expect(uploadMock.upload).toHaveBeenCalledOnce();
|
|
198
257
|
});
|
|
199
258
|
|
|
200
|
-
it('works without onFilesExceeded callback (no crash)', () => {
|
|
259
|
+
it('works without onFilesExceeded callback (no crash)', async () => {
|
|
201
260
|
const provider = createDocumentUploadProvider(
|
|
202
261
|
buildConfig({ maxFilesPerUpload: 1 }),
|
|
203
262
|
);
|
|
@@ -206,5 +265,177 @@ describe('useFileUploadWrapper handleFilesSelected', () => {
|
|
|
206
265
|
const event = makeInputEvent([createFile('a.pdf'), createFile('b.pdf')]);
|
|
207
266
|
|
|
208
267
|
expect(() => handleFilesSelected(event)).not.toThrow();
|
|
268
|
+
await settleAsyncUploads();
|
|
269
|
+
expect(uploadMock.upload).toHaveBeenCalledOnce();
|
|
270
|
+
});
|
|
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
|
+
});
|
|
209
440
|
});
|
|
210
441
|
});
|