@aleph-alpha/chat-kit 3.2.0 → 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-B-4xnH2c.js → CkInput.vue_vue_type_script_setup_true_lang-BAcBFs6I.js} +302 -71
- 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/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 +3 -2
- 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/{index-DAmWGYNH.js → index-C6njJ8EB.js} +85 -70
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -26
- package/dist/{useChatKitLabels-D5gNv8se.js → useChatKitLabels-BM2I7-9o.js} +30 -3
- package/package.json +4 -2
- 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/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.ts +13 -0
- package/src/handlers/useMarkdownParser.ts +5 -2
- package/src/helpers/unified/remarkCitations.spec.ts +112 -0
- package/src/helpers/unified/remarkCitations.ts +55 -0
- package/src/index.ts +8 -0
|
@@ -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,
|
|
@@ -81,6 +81,17 @@ export type ChatKitLabels = {
|
|
|
81
81
|
* can scroll wide tables). Falls back to `defaultChatKitLabels` when omitted.
|
|
82
82
|
*/
|
|
83
83
|
markdownTableLabel?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Accessible label for the external-link action in a citation card
|
|
86
|
+
* (`CkCitation` / `CkCitationCard`). Falls back to `defaultChatKitLabels`.
|
|
87
|
+
*/
|
|
88
|
+
citationOpenSource?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Prefix for a citation marker's accessible name in `CkCitation`, composed
|
|
91
|
+
* as `"{citationSourceLabel} {index}"` (e.g. "Source 1"). Falls back to
|
|
92
|
+
* `defaultChatKitLabels`.
|
|
93
|
+
*/
|
|
94
|
+
citationSourceLabel?: string;
|
|
84
95
|
/** Fallback title used by `CkHistory` when a conversation has no `title`. */
|
|
85
96
|
historyItemUntitled: string;
|
|
86
97
|
/** Accessible name for the trailing kebab trigger on each `CkHistory` row. */
|
|
@@ -123,6 +134,8 @@ export const defaultChatKitLabels: ChatKitLabels = {
|
|
|
123
134
|
modelSelectorListAriaLabel: 'Models',
|
|
124
135
|
modelSelectorPlaceholder: 'Select model',
|
|
125
136
|
markdownTableLabel: 'Table',
|
|
137
|
+
citationOpenSource: 'Open source',
|
|
138
|
+
citationSourceLabel: 'Source',
|
|
126
139
|
historyItemUntitled: 'Untitled conversation',
|
|
127
140
|
historyItemActionAriaLabel: 'Conversation options',
|
|
128
141
|
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
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { remarkCitations } from './remarkCitations';
|
|
2
|
+
|
|
3
|
+
// The plugin is a pure mdast transformer, so we can exercise it directly on a
|
|
4
|
+
// hand-built tree without spinning up the full unified/remark parser.
|
|
5
|
+
interface MdNode {
|
|
6
|
+
type: string;
|
|
7
|
+
value?: string;
|
|
8
|
+
children?: MdNode[];
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function rootWithText(text: string): MdNode {
|
|
13
|
+
return { type: 'root', children: [{ type: 'text', value: text }] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// `remarkCitations` is typed as a unified `Plugin` (it expects a `Processor`
|
|
17
|
+
// `this`). In these unit tests we call it as a plain factory, so cast away the
|
|
18
|
+
// `this` context to invoke it directly on a hand-built tree.
|
|
19
|
+
const makeTransformer = remarkCitations as unknown as (options: {
|
|
20
|
+
indices?: ReadonlySet<number>;
|
|
21
|
+
}) => (tree: MdNode) => void;
|
|
22
|
+
|
|
23
|
+
function run(text: string, indices?: ReadonlySet<number>): MdNode[] {
|
|
24
|
+
const tree = rootWithText(text);
|
|
25
|
+
makeTransformer({ indices })(tree);
|
|
26
|
+
return tree.children!;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('remarkCitations', () => {
|
|
30
|
+
it('replaces a known [n] marker with a citation placeholder node', () => {
|
|
31
|
+
const out = run('see this [1] here', new Set([1]));
|
|
32
|
+
expect(out.map((n) => n.type)).toEqual([
|
|
33
|
+
'text',
|
|
34
|
+
'citationReference',
|
|
35
|
+
'text',
|
|
36
|
+
]);
|
|
37
|
+
expect(out[0]).toEqual({ type: 'text', value: 'see this ' });
|
|
38
|
+
expect(out[2]).toEqual({ type: 'text', value: ' here' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('emits a span placeholder with the index as a data attribute', () => {
|
|
42
|
+
const [citation] = run('[1]', new Set([1]));
|
|
43
|
+
expect(citation.data).toEqual({
|
|
44
|
+
hName: 'span',
|
|
45
|
+
hProperties: { 'data-citation-index': 1 },
|
|
46
|
+
hChildren: [{ type: 'text', value: '1' }],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles adjacent markers', () => {
|
|
51
|
+
const out = run('claim [1][2] end', new Set([1, 2]));
|
|
52
|
+
expect(out.map((n) => n.type)).toEqual([
|
|
53
|
+
'text',
|
|
54
|
+
'citationReference',
|
|
55
|
+
'citationReference',
|
|
56
|
+
'text',
|
|
57
|
+
]);
|
|
58
|
+
expect(
|
|
59
|
+
(out[1].data!.hProperties as Record<string, number>)[
|
|
60
|
+
'data-citation-index'
|
|
61
|
+
],
|
|
62
|
+
).toBe(1);
|
|
63
|
+
expect(
|
|
64
|
+
(out[2].data!.hProperties as Record<string, number>)[
|
|
65
|
+
'data-citation-index'
|
|
66
|
+
],
|
|
67
|
+
).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('leaves unsupported markers (no matching source) as literal text', () => {
|
|
71
|
+
const out = run('real [1] but [9] is not', new Set([1]));
|
|
72
|
+
expect(out.map((n) => n.type)).toEqual([
|
|
73
|
+
'text',
|
|
74
|
+
'citationReference',
|
|
75
|
+
'text',
|
|
76
|
+
]);
|
|
77
|
+
// The [9] survives verbatim in the trailing text node.
|
|
78
|
+
expect(out[2].value).toBe(' but [9] is not');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('treats every [n] as a citation when no index set is given', () => {
|
|
82
|
+
const out = run('a [1] b [2]');
|
|
83
|
+
expect(out.filter((n) => n.type === 'citationReference')).toHaveLength(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('never converts [0] — citations are 1-based', () => {
|
|
87
|
+
const out = run('the value arr[0] here'); // no allow-list: every [n] except 0
|
|
88
|
+
expect(out.some((n) => n.type === 'citationReference')).toBe(false);
|
|
89
|
+
expect(out.map((n) => n.value ?? '').join('')).toContain('[0]');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('leaves text without markers untouched', () => {
|
|
93
|
+
const out = run('no citations here', new Set([1]));
|
|
94
|
+
expect(out).toEqual([{ type: 'text', value: 'no citations here' }]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not descend into code/inlineCode value nodes', () => {
|
|
98
|
+
const tree: MdNode = {
|
|
99
|
+
type: 'root',
|
|
100
|
+
children: [
|
|
101
|
+
{ type: 'inlineCode', value: 'arr[1]' },
|
|
102
|
+
{ type: 'text', value: ' and [1]' },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
makeTransformer({ indices: new Set([1]) })(tree);
|
|
106
|
+
// inlineCode is untouched; only the text node's [1] becomes a citation.
|
|
107
|
+
expect(tree.children![0]).toEqual({ type: 'inlineCode', value: 'arr[1]' });
|
|
108
|
+
expect(tree.children!.some((n) => n.type === 'citationReference')).toBe(
|
|
109
|
+
true,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Plugin } from 'unified';
|
|
2
|
+
import type { PhrasingContent } from 'mdast';
|
|
3
|
+
import { findAndReplace } from 'mdast-util-find-and-replace';
|
|
4
|
+
|
|
5
|
+
export interface RemarkCitationsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* When provided, only `[n]` markers whose number is in this set are turned
|
|
8
|
+
* into citation placeholders; any other `[n]` is left as literal text
|
|
9
|
+
* (an "unsupported" marker with no backing source). When omitted, every
|
|
10
|
+
* `[n]` is treated as a citation.
|
|
11
|
+
*/
|
|
12
|
+
indices?: ReadonlySet<number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Matches a bare `[n]` reference, e.g. `[1]`. Because this runs on the parsed
|
|
16
|
+
// mdast tree (via `findAndReplace`, which only visits `text` nodes), `[n]`
|
|
17
|
+
// inside code spans/blocks and bracketed links never reaches it — only a
|
|
18
|
+
// literal `[n]` typed in prose does.
|
|
19
|
+
const CITATION_RE = /\[(\d+)\]/g;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Replaces inline `[n]` citation references in the parsed markdown with a
|
|
23
|
+
* placeholder element (`<span data-citation-index="n">n</span>`) that survives
|
|
24
|
+
* stringify + sanitisation (`data-*` attributes are allowed by default).
|
|
25
|
+
*/
|
|
26
|
+
// The tree type is left as unified's default (a unist `Node`) so the public
|
|
27
|
+
// type signature — and therefore the published `.d.ts` — doesn't depend on
|
|
28
|
+
// `mdast` types; the cast below keeps the internal `findAndReplace` call typed.
|
|
29
|
+
export const remarkCitations: Plugin<[RemarkCitationsOptions?]> = (
|
|
30
|
+
options = {},
|
|
31
|
+
) => {
|
|
32
|
+
const { indices } = options;
|
|
33
|
+
|
|
34
|
+
return (tree) => {
|
|
35
|
+
findAndReplace(tree as Parameters<typeof findAndReplace>[0], [
|
|
36
|
+
CITATION_RE,
|
|
37
|
+
(_match: string, digits: string) => {
|
|
38
|
+
const index = Number(digits);
|
|
39
|
+
if (index < 1) return false; // citations are 1-based
|
|
40
|
+
// Leave unsupported markers (no backing source) as literal text.
|
|
41
|
+
if (indices && !indices.has(index)) return false;
|
|
42
|
+
return {
|
|
43
|
+
type: 'citationReference',
|
|
44
|
+
data: {
|
|
45
|
+
hName: 'span',
|
|
46
|
+
// Use the literal attribute name so the placeholder contract matches
|
|
47
|
+
// the renderer's `getAttribute('data-citation-index')` lookup exactly.
|
|
48
|
+
hProperties: { 'data-citation-index': index },
|
|
49
|
+
hChildren: [{ type: 'text', value: String(index) }],
|
|
50
|
+
},
|
|
51
|
+
} as unknown as PhrasingContent;
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
};
|
|
55
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,8 @@ export type {
|
|
|
63
63
|
// Components
|
|
64
64
|
export {
|
|
65
65
|
CkAgentMessage,
|
|
66
|
+
CkCitation,
|
|
67
|
+
CkCitationCard,
|
|
66
68
|
CkConversation,
|
|
67
69
|
CkConversationLayout,
|
|
68
70
|
CkCopyMessageActionButton,
|
|
@@ -87,6 +89,11 @@ export {
|
|
|
87
89
|
CkUserMessage,
|
|
88
90
|
} from './components';
|
|
89
91
|
export type {
|
|
92
|
+
CkCitationProps,
|
|
93
|
+
CkCitationEmits,
|
|
94
|
+
CkCitationSource,
|
|
95
|
+
CkCitationCardProps,
|
|
96
|
+
CkCitationCardEmits,
|
|
90
97
|
CkConversationLayoutProps,
|
|
91
98
|
CkConversationProps,
|
|
92
99
|
CkDropZoneOverlayProps,
|
|
@@ -110,6 +117,7 @@ export type {
|
|
|
110
117
|
CkHistoryWrapperProps,
|
|
111
118
|
CkMarkdownElementClasses,
|
|
112
119
|
CkMarkdownRendererProps,
|
|
120
|
+
CkMarkdownRendererEmits,
|
|
113
121
|
CkModelOption,
|
|
114
122
|
CkModelSelectorProps,
|
|
115
123
|
CkModelSelectorEmits,
|