@aleph-alpha/chat-kit 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-B-4xnH2c.js → CkInput.vue_vue_type_script_setup_true_lang-DJN-nZ2-.js} +391 -113
  2. package/dist/adapters/responses-api.d.ts +11 -5
  3. package/dist/adapters/responses-api.d.ts.map +1 -1
  4. package/dist/adapters/responses-api.js +9 -3
  5. package/dist/chat-kit.css +14 -8
  6. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +7 -0
  7. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
  8. package/dist/components/base/CkAgentMessage/types.d.ts +1 -1
  9. package/dist/components/base/CkCitation/CkCitation.stories.d.ts +47 -0
  10. package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -0
  11. package/dist/components/base/CkCitation/CkCitation.vue.d.ts +15 -0
  12. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -0
  13. package/dist/components/base/CkCitation/index.d.ts +3 -0
  14. package/dist/components/base/CkCitation/index.d.ts.map +1 -0
  15. package/dist/components/base/CkCitation/types.d.ts +79 -0
  16. package/dist/components/base/CkCitation/types.d.ts.map +1 -0
  17. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +51 -0
  18. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -0
  19. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +13 -0
  20. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -0
  21. package/dist/components/base/CkCitationCard/index.d.ts +3 -0
  22. package/dist/components/base/CkCitationCard/index.d.ts.map +1 -0
  23. package/dist/components/base/CkCitationCard/types.d.ts +51 -0
  24. package/dist/components/base/CkCitationCard/types.d.ts.map +1 -0
  25. package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts → CkCopyMessageButton/CkCopyMessageButton.stories.d.ts} +3 -3
  26. package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.stories.d.ts.map +1 -0
  27. package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts → CkCopyMessageButton/CkCopyMessageButton.vue.d.ts} +4 -4
  28. package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.vue.d.ts.map +1 -0
  29. package/dist/components/base/CkCopyMessageButton/index.d.ts +3 -0
  30. package/dist/components/base/CkCopyMessageButton/index.d.ts.map +1 -0
  31. package/dist/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.d.ts +3 -3
  32. package/dist/components/base/CkCopyMessageButton/types.d.ts.map +1 -0
  33. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +1 -1
  34. package/dist/components/base/CkFeedbackButton/types.d.ts +1 -1
  35. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
  36. package/dist/components/base/CkFeedbackDialog/types.d.ts +1 -1
  37. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
  38. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +15 -0
  39. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
  40. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts +16 -2
  41. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
  42. package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
  43. package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
  44. package/dist/components/base/CkMarkdownRenderer/types.d.ts +25 -0
  45. package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
  46. package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts +17 -0
  47. package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts.map +1 -0
  48. package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.stories.d.ts → CkMessageButton/CkMessageButton.stories.d.ts} +3 -3
  49. package/dist/components/base/CkMessageButton/CkMessageButton.stories.d.ts.map +1 -0
  50. package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.vue.d.ts → CkMessageButton/CkMessageButton.vue.d.ts} +3 -3
  51. package/dist/components/base/CkMessageButton/CkMessageButton.vue.d.ts.map +1 -0
  52. package/dist/components/base/CkMessageButton/index.d.ts +3 -0
  53. package/dist/components/base/CkMessageButton/index.d.ts.map +1 -0
  54. package/dist/components/base/{CkMessageActionButton → CkMessageButton}/types.d.ts +2 -2
  55. package/dist/components/base/CkMessageButton/types.d.ts.map +1 -0
  56. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
  57. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts +16 -0
  58. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts.map +1 -0
  59. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts +12 -0
  60. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts.map +1 -0
  61. package/dist/components/base/CkRegenerateMessageButton/index.d.ts +3 -0
  62. package/dist/components/base/{CkCopyMessageActionButton → CkRegenerateMessageButton}/index.d.ts.map +1 -1
  63. package/dist/components/base/CkRegenerateMessageButton/types.d.ts +27 -0
  64. package/dist/components/base/CkRegenerateMessageButton/types.d.ts.map +1 -0
  65. package/dist/components/base/index.d.ts +8 -3
  66. package/dist/components/base/index.d.ts.map +1 -1
  67. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +1 -1
  68. package/dist/components/composed/CkConversation/types.d.ts +1 -1
  69. package/dist/components/composed/CkInput/CkInput.vue.d.ts +1 -1
  70. package/dist/components/index.d.ts +2 -2
  71. package/dist/components/index.d.ts.map +1 -1
  72. package/dist/components/index.js +26 -23
  73. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
  74. package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
  75. package/dist/composables/index.js +1 -1
  76. package/dist/handlers/index.d.ts +2 -0
  77. package/dist/handlers/index.d.ts.map +1 -1
  78. package/dist/handlers/index.js +4 -3
  79. package/dist/handlers/useChatKitLabels.d.ts +13 -0
  80. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  81. package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
  82. package/dist/handlers/useMessageHandler.d.ts +37 -0
  83. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  84. package/dist/helpers/conversationTree.d.ts +15 -0
  85. package/dist/helpers/conversationTree.d.ts.map +1 -1
  86. package/dist/helpers/unified/remarkCitations.d.ts +17 -0
  87. package/dist/helpers/unified/remarkCitations.d.ts.map +1 -0
  88. package/dist/{index-DAmWGYNH.js → index-CyrVnbS9.js} +86 -71
  89. package/dist/index.d.ts +2 -2
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +30 -27
  92. package/dist/{useChatKitLabels-D5gNv8se.js → useChatKitLabels-zMFV7aEz.js} +31 -3
  93. package/dist/{useMessageHandler-CMdy-lm0.js → useMessageHandler-N50Yj-NU.js} +45 -7
  94. package/package.json +5 -3
  95. package/src/adapters/responses-api.spec.ts +52 -5
  96. package/src/adapters/responses-api.ts +26 -7
  97. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +112 -0
  98. package/src/components/base/CkAgentMessage/types.ts +1 -1
  99. package/src/components/base/CkCitation/CkCitation.spec.ts +79 -0
  100. package/src/components/base/CkCitation/CkCitation.stories.ts +136 -0
  101. package/src/components/base/CkCitation/CkCitation.vue +145 -0
  102. package/src/components/base/CkCitation/index.ts +2 -0
  103. package/src/components/base/CkCitation/types.ts +80 -0
  104. package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +77 -0
  105. package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +144 -0
  106. package/src/components/base/CkCitationCard/CkCitationCard.vue +76 -0
  107. package/src/components/base/CkCitationCard/index.ts +2 -0
  108. package/src/components/base/CkCitationCard/types.ts +51 -0
  109. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts → CkCopyMessageButton/CkCopyMessageButton.spec.ts} +6 -6
  110. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts → CkCopyMessageButton/CkCopyMessageButton.stories.ts} +8 -8
  111. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue → CkCopyMessageButton/CkCopyMessageButton.vue} +6 -6
  112. package/src/components/base/CkCopyMessageButton/index.ts +2 -0
  113. package/src/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.ts +3 -3
  114. package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +5 -5
  115. package/src/components/base/CkFeedbackButton/types.ts +1 -1
  116. package/src/components/base/CkFeedbackDialog/types.ts +1 -1
  117. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +124 -0
  118. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +118 -0
  119. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +70 -28
  120. package/src/components/base/CkMarkdownRenderer/index.ts +1 -0
  121. package/src/components/base/CkMarkdownRenderer/types.ts +26 -0
  122. package/src/components/base/CkMarkdownRenderer/useCitationRendering.spec.ts +94 -0
  123. package/src/components/base/CkMarkdownRenderer/useCitationRendering.ts +52 -0
  124. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.spec.ts → CkMessageButton/CkMessageButton.spec.ts} +6 -6
  125. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.stories.ts → CkMessageButton/CkMessageButton.stories.ts} +10 -10
  126. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.vue → CkMessageButton/CkMessageButton.vue} +3 -3
  127. package/src/components/base/CkMessageButton/index.ts +2 -0
  128. package/src/components/base/{CkMessageActionButton → CkMessageButton}/types.ts +2 -2
  129. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.spec.ts +49 -0
  130. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.ts +63 -0
  131. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue +43 -0
  132. package/src/components/base/CkRegenerateMessageButton/index.ts +2 -0
  133. package/src/components/base/CkRegenerateMessageButton/types.ts +27 -0
  134. package/src/components/base/index.ts +15 -2
  135. package/src/components/composed/CkConversation/CkConversation.stories.ts +6 -6
  136. package/src/components/composed/CkConversation/CkConversation.vue +2 -2
  137. package/src/components/composed/CkConversation/types.ts +1 -1
  138. package/src/components/index.ts +11 -2
  139. package/src/composables/fileUpload/useFileUpload.spec.ts +2 -61
  140. package/src/composables/fileUpload/useFileUpload.ts +2 -9
  141. package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +195 -14
  142. package/src/composables/fileUpload/useFileUploadWrapper.ts +114 -81
  143. package/src/handlers/index.ts +2 -0
  144. package/src/handlers/useChatKitLabels.spec.ts +1 -0
  145. package/src/handlers/useChatKitLabels.ts +16 -0
  146. package/src/handlers/useMarkdownParser.ts +5 -2
  147. package/src/handlers/useMessageHandler.spec.ts +79 -0
  148. package/src/handlers/useMessageHandler.ts +50 -2
  149. package/src/helpers/conversationTree.spec.ts +120 -0
  150. package/src/helpers/conversationTree.ts +57 -8
  151. package/src/helpers/unified/remarkCitations.spec.ts +112 -0
  152. package/src/helpers/unified/remarkCitations.ts +55 -0
  153. package/src/index.ts +11 -2
  154. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +0 -1
  155. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +0 -1
  156. package/dist/components/base/CkCopyMessageActionButton/index.d.ts +0 -3
  157. package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +0 -1
  158. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +0 -1
  159. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +0 -1
  160. package/dist/components/base/CkMessageActionButton/index.d.ts +0 -3
  161. package/dist/components/base/CkMessageActionButton/index.d.ts.map +0 -1
  162. package/dist/components/base/CkMessageActionButton/types.d.ts.map +0 -1
  163. package/src/components/base/CkCopyMessageActionButton/index.ts +0 -2
  164. package/src/components/base/CkMessageActionButton/index.ts +0 -2
@@ -212,6 +212,7 @@ export interface ResponseAttachment {
212
212
  fileId: string;
213
213
  fileName: string;
214
214
  searchStoreId: string;
215
+ documentId?: string;
215
216
  }
216
217
 
217
218
  /**
@@ -219,15 +220,17 @@ export interface ResponseAttachment {
219
220
  * `metadata.attachments` on the user `TreeMessage` (chat-kit's `BaseMessage`
220
221
  * /`TreeMessage` carry `metadata` as an opaque `Record<string, unknown>`).
221
222
  *
222
- * The shape is intentionally minimal — a human-readable `name` plus the
223
- * optional ids needed to resolve a public URL for the file. Consumers
224
- * using a different attachment shape can either skip this adapter or
225
- * project this onto their own type at the read site.
223
+ * Carries a human-readable `name` plus every id needed to resolve a public
224
+ * URL for the file or to re-submit it on a later turn (e.g. regenerate).
225
+ * Consumers using a different attachment shape can either skip this adapter
226
+ * or project this onto their own type at the read site.
226
227
  */
227
228
  export interface ResponseMessageAttachment {
228
229
  name: string;
229
230
  fileId?: string;
231
+ fileName?: string;
230
232
  searchStoreId?: string;
233
+ documentId?: string;
231
234
  }
232
235
 
233
236
  /**
@@ -280,6 +283,7 @@ export interface ResponseList {
280
283
  */
281
284
  export type ResponseTreeMessageMetadata = MessageMetadata & {
282
285
  responseId: string;
286
+ model?: string;
283
287
  attachments?: ResponseMessageAttachment[];
284
288
  };
285
289
 
@@ -478,13 +482,18 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
478
482
  const userContent = text || responseAttachments?.length;
479
483
  if (role !== 'user' || !userContent) continue;
480
484
 
481
- const metadata: ResponseTreeMessageMetadata = { responseId: resp.id };
485
+ const metadata: ResponseTreeMessageMetadata = {
486
+ responseId: resp.id,
487
+ ...(resp.model ? { model: resp.model } : {}),
488
+ };
482
489
  if (responseAttachments && userNodesPushed === 0) {
483
490
  metadata.attachments = responseAttachments.map(
484
491
  (a): ResponseMessageAttachment => ({
485
492
  name: a.fileName,
486
493
  fileId: a.fileId,
494
+ fileName: a.fileName,
487
495
  searchStoreId: a.searchStoreId,
496
+ ...(a.documentId ? { documentId: a.documentId } : {}),
488
497
  }),
489
498
  );
490
499
  }
@@ -607,6 +616,12 @@ function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
607
616
  parentResponseId !== null
608
617
  ? (tailMessageByResponse.get(parentResponseId) ?? null)
609
618
  : null;
619
+ // Whether this response opens a new root-level branch (it chains onto
620
+ // nothing). The first turn can be branched/regenerated, producing several
621
+ // root responses; since the tree has a single `rootId` with no parent node
622
+ // to track sibling navigation, we keep the latest such branch as the active
623
+ // root — mirroring the "latest sibling wins" rule used for deeper branches.
624
+ const isRootChain = chainParent === null;
610
625
 
611
626
  const messageNodes = responseToMessageNodes(resp);
612
627
 
@@ -624,7 +639,9 @@ function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
624
639
  chainParent = node.id;
625
640
  }
626
641
 
627
- if (state.rootMessageId === null && firstId !== null) {
642
+ // Favour the latest root branch: responses arrive in ascending `created_at`
643
+ // order, so a later root chain overwrites an earlier one.
644
+ if (firstId !== null && (state.rootMessageId === null || isRootChain)) {
628
645
  state.rootMessageId = firstId;
629
646
  }
630
647
 
@@ -639,7 +656,9 @@ function linkResponse(state: TreeBuildState, resp: StoredResponse): void {
639
656
  * pass: each response's `previous_response_id` always points to a response
640
657
  * we have already processed, so we can wire its messages onto the parent's
641
658
  * last message immediately. Sibling responses (same `previous_response_id`)
642
- * surface as branches; the latest one wins as the active branch.
659
+ * surface as branches; the latest one wins as the active branch. The same
660
+ * preference applies at the top level: when the first turn is branched into
661
+ * several root responses, the latest becomes the active `rootId`.
643
662
  *
644
663
  * Every emitted node carries `metadata.responseId` pointing back to the
645
664
  * originating `StoredResponse.id` (see {@link ResponseTreeMessage}). The
@@ -1,4 +1,6 @@
1
1
  import CkAgentMessage from './CkAgentMessage.vue';
2
+ import { CkMarkdownRenderer } from '../CkMarkdownRenderer';
3
+ import type { CkCitationSource } from '../CkCitation';
2
4
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
5
 
4
6
  const meta: Meta<typeof CkAgentMessage> = {
@@ -213,3 +215,113 @@ export const WithoutIcon: Story = {
213
215
  }),
214
216
  parameters: { docs: { source: { code: withoutIconTemplateSource } } },
215
217
  };
218
+
219
+ const CITATIONS: CkCitationSource[] = [
220
+ {
221
+ index: 1,
222
+ fileName: 'Q2_Business_Plan.pdf',
223
+ snippet:
224
+ 'Departments that received customized training saw a 25% higher adoption rate of the new tools compared to those with generic training.',
225
+ locationLabel: 'Page 3',
226
+ url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
227
+ },
228
+ {
229
+ index: 2,
230
+ fileName: 'Support-Metrics-Q1.csv',
231
+ snippet:
232
+ 'Ticket volume fell 18% quarter-over-quarter for the tailored-training cohorts versus the control group.',
233
+ locationLabel: 'Row 42',
234
+ url: '/api/v1/search-stores/ss_abc/files/file_456/public-urls',
235
+ },
236
+ ];
237
+
238
+ const CITED_MARKDOWN = [
239
+ 'Based on the internal documents, customized training drove measurably better outcomes.',
240
+ '',
241
+ 'Departments with tailored sessions saw a **25% higher adoption rate** [1],',
242
+ 'and reported markedly fewer support tickets in the first quarter [2].',
243
+ ].join('\n');
244
+
245
+ const withCitationsTemplateSource = `<script setup lang="ts">
246
+ import { CkAgentMessage, CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
247
+ import type { CkCitationSource } from '@aleph-alpha/chat-kit';
248
+
249
+ // The reply body is markdown that contains inline [n] references.
250
+ const content = [
251
+ 'Based on the internal documents, customized training drove measurably better outcomes.',
252
+ '',
253
+ 'Departments with tailored sessions saw a **25% higher adoption rate** [1],',
254
+ 'and reported markedly fewer support tickets in the first quarter [2].',
255
+ ].join('\\n');
256
+
257
+ // The numbered sources, mapped from the backend citations manifest.
258
+ const citations: CkCitationSource[] = [
259
+ {
260
+ index: 1,
261
+ fileName: 'Q2_Business_Plan.pdf',
262
+ snippet: 'Departments that received customized training saw a 25% higher adoption rate…',
263
+ locationLabel: 'Page 3',
264
+ url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
265
+ },
266
+ {
267
+ index: 2,
268
+ fileName: 'Support-Metrics-Q1.csv',
269
+ snippet: 'Ticket volume fell 18% quarter-over-quarter for the tailored-training cohorts.',
270
+ locationLabel: 'Row 42',
271
+ url: '/api/v1/search-stores/ss_abc/files/file_456/public-urls',
272
+ },
273
+ ];
274
+
275
+ function openSource(url: string) {
276
+ // Source URLs are auth-proxied; the host performs the navigation.
277
+ window.open(url, '_blank', 'noopener');
278
+ }
279
+ </script>
280
+
281
+ <template>
282
+ <CkAgentMessage>
283
+ <template #icon>
284
+ <div class="bg-core-bg-tertiary flex size-6 items-center justify-center rounded-full text-xs">
285
+ AI
286
+ </div>
287
+ </template>
288
+ <CkMarkdownRenderer
289
+ :content="content"
290
+ :citations="citations"
291
+ @open-source="openSource"
292
+ />
293
+ </CkAgentMessage>
294
+ </template>`;
295
+
296
+ /**
297
+ * An assistant reply whose body is markdown containing inline `[n]` citations,
298
+ * rendered through `CkMarkdownRenderer` inside the message. Each `[n]` becomes
299
+ * an interactive `CkCitation` — hover a marker to preview its source. This is
300
+ * the end-to-end shape of a cited answer in a conversation.
301
+ */
302
+ export const WithCitations: Story = {
303
+ render: () => ({
304
+ components: { CkAgentMessage, CkMarkdownRenderer },
305
+ setup() {
306
+ function openSource(url: string) {
307
+ window.open(url, '_blank', 'noopener');
308
+ }
309
+ return { content: CITED_MARKDOWN, citations: CITATIONS, openSource };
310
+ },
311
+ template: `
312
+ <CkAgentMessage>
313
+ <template #icon>
314
+ <div class="bg-core-bg-tertiary flex size-6 items-center justify-center rounded-full text-xs">
315
+ AI
316
+ </div>
317
+ </template>
318
+ <CkMarkdownRenderer
319
+ :content="content"
320
+ :citations="citations"
321
+ @open-source="openSource"
322
+ />
323
+ </CkAgentMessage>
324
+ `,
325
+ }),
326
+ parameters: { docs: { source: { code: withCitationsTemplateSource } } },
327
+ };
@@ -6,6 +6,6 @@
6
6
  * @category Messages
7
7
  * @useCases render assistant reply, AI message layout, conversation turn, streaming message container
8
8
  * @keywords agent, assistant, message, bubble, reply, ai, conversation
9
- * @related CkUserMessage, CkConversation, CkMarkdownRenderer, CkCopyMessageActionButton
9
+ * @related CkUserMessage, CkConversation, CkMarkdownRenderer, CkCopyMessageButton
10
10
  */
11
11
  export type CkAgentMessageProps = Record<string, never>;
@@ -0,0 +1,79 @@
1
+ import CkCitation from './CkCitation.vue';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/vue';
4
+
5
+ const base = {
6
+ index: 1,
7
+ fileName: 'q3-report.pdf',
8
+ snippet: 'The cited chunk text.',
9
+ locationLabel: 'Page 3',
10
+ url: '/api/v1/.../public-urls',
11
+ };
12
+
13
+ describe('CkCitation', () => {
14
+ it('renders a marker button showing the source number', () => {
15
+ render(CkCitation, { props: base });
16
+
17
+ const marker = screen.getByRole('button', { name: 'Source 1' });
18
+ expect(marker).toHaveTextContent('1');
19
+ });
20
+
21
+ it('derives the accessible name from the index by default', () => {
22
+ render(CkCitation, { props: { ...base, index: 7 } });
23
+
24
+ expect(
25
+ screen.getByRole('button', { name: 'Source 7' }),
26
+ ).toBeInTheDocument();
27
+ });
28
+
29
+ it('uses a custom ariaLabel when provided', () => {
30
+ render(CkCitation, { props: { ...base, ariaLabel: 'Quelle 1' } });
31
+
32
+ expect(
33
+ screen.getByRole('button', { name: 'Quelle 1' }),
34
+ ).toBeInTheDocument();
35
+ });
36
+
37
+ it('reveals the source card on activation and re-emits open-source', async () => {
38
+ const user = userEvent.setup();
39
+ const { emitted } = render(CkCitation, { props: base });
40
+
41
+ // The marker reveals its card on hover, matching the design.
42
+ await user.hover(screen.getByRole('button', { name: 'Source 1' }));
43
+
44
+ // The card content is rendered (in a popover portal) once open.
45
+ expect(await screen.findByText('q3-report.pdf')).toBeInTheDocument();
46
+ expect(screen.getByText('The cited chunk text.')).toBeInTheDocument();
47
+
48
+ await user.click(screen.getByRole('button', { name: 'Open source' }));
49
+
50
+ expect(emitted('open-source')).toEqual([['/api/v1/.../public-urls']]);
51
+ });
52
+
53
+ it('opens the card on keyboard activation of the marker', async () => {
54
+ const user = userEvent.setup();
55
+ render(CkCitation, { props: base });
56
+
57
+ const marker = screen.getByRole('button', { name: 'Source 1' });
58
+ marker.focus();
59
+ await user.keyboard('{Enter}');
60
+
61
+ expect(await screen.findByText('q3-report.pdf')).toBeInTheDocument();
62
+ });
63
+
64
+ it('renders the default (amber) marker state by default', () => {
65
+ render(CkCitation, { props: base });
66
+
67
+ const marker = screen.getByRole('button', { name: 'Source 1' });
68
+ expect(marker.className).toContain('bg-background-accent-yellow');
69
+ expect(marker.className).not.toContain('bg-background-accent-soft');
70
+ });
71
+
72
+ it('renders the selected (teal) marker state when `selected`', () => {
73
+ render(CkCitation, { props: { ...base, selected: true } });
74
+
75
+ const marker = screen.getByRole('button', { name: 'Source 1' });
76
+ expect(marker.className).toContain('bg-background-accent-soft');
77
+ expect(marker.className).not.toContain('bg-background-accent-yellow');
78
+ });
79
+ });
@@ -0,0 +1,136 @@
1
+ import CkCitation from './CkCitation.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const SNIPPET =
5
+ 'Departments that received customized training saw a 25% higher adoption rate of the new tools compared to those with generic training.';
6
+
7
+ /**
8
+ * CkCitation is the inline marker rendered in place of a `[n]` reference in an
9
+ * assistant answer — a numbered chip that reveals a `CkCitationCard` describing
10
+ * the source on hover or keyboard activation (Enter/Space). Drive it through `CkMarkdownRenderer`'s
11
+ * `citations` prop to render cited answers; use `CkCitationCard` on its own for
12
+ * a standalone source preview.
13
+ */
14
+ const meta: Meta<typeof CkCitation> = {
15
+ title: 'Base/CkCitation',
16
+ component: CkCitation,
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ index: { control: 'number' },
20
+ selected: { control: 'boolean' },
21
+ fileName: { control: 'text' },
22
+ snippet: { control: 'text' },
23
+ locationLabel: { control: 'text' },
24
+ url: { control: 'text' },
25
+ ariaLabel: { control: 'text' },
26
+ openSourceLabel: { control: 'text' },
27
+ },
28
+ args: {
29
+ index: 1,
30
+ fileName: 'Q2_Business_Plan.pdf',
31
+ snippet: SNIPPET,
32
+ locationLabel: 'Page 3',
33
+ url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
34
+ },
35
+ decorators: [
36
+ () => ({
37
+ template:
38
+ '<div style="max-width: 640px; margin: 0 auto; padding: 96px 64px;"><story /></div>',
39
+ }),
40
+ ],
41
+ };
42
+
43
+ export default meta;
44
+ type Story = StoryObj<typeof meta>;
45
+
46
+ /**
47
+ * A single marker in its default (amber) state. Hover the chip (or focus it and
48
+ * press Enter) to reveal the source card.
49
+ */
50
+ export const Default: Story = {};
51
+
52
+ /**
53
+ * The selected (teal) state — set `selected` when the citation's source is the
54
+ * one currently open/highlighted, so the active marker stands out.
55
+ */
56
+ export const Selected: Story = {
57
+ args: { selected: true },
58
+ };
59
+
60
+ /**
61
+ * How markers read inside a sentence — two sources cited next to each other,
62
+ * the way they appear in an assistant answer.
63
+ */
64
+ export const InlineInText: Story = {
65
+ render: (args) => ({
66
+ components: { CkCitation },
67
+ setup() {
68
+ return { args };
69
+ },
70
+ template: `
71
+ <p style="font-size: 14px; line-height: 20px; max-width: 32rem;">
72
+ Departments that received customized training saw a 25% higher adoption
73
+ rate of the new tools compared to those with generic training.<CkCitation
74
+ v-bind="args"
75
+ :index="1"
76
+ /><CkCitation
77
+ v-bind="args"
78
+ :index="2"
79
+ file-name="Goal-planning-26.pdf"
80
+ location-label="Page 12"
81
+ />
82
+ </p>
83
+ `,
84
+ }),
85
+ };
86
+
87
+ /**
88
+ * Multi-digit indices keep the chip legible — the marker grows with its
89
+ * number while staying aligned to the surrounding text.
90
+ */
91
+ export const MultiDigitIndex: Story = {
92
+ args: { index: 12 },
93
+ };
94
+
95
+ /**
96
+ * No `url` — the card still appears on hover but without the external-link
97
+ * action (e.g. an internal chunk with no servable file).
98
+ */
99
+ export const WithoutLink: Story = {
100
+ args: { url: undefined },
101
+ };
102
+
103
+ /**
104
+ * Non-paginated source — the card omits the page footer.
105
+ */
106
+ export const WithoutLocation: Story = {
107
+ args: { locationLabel: undefined },
108
+ };
109
+
110
+ /**
111
+ * A row of markers with different file types — the card's badge tone follows
112
+ * each source's extension.
113
+ */
114
+ export const SourceTypes: Story = {
115
+ render: () => ({
116
+ components: { CkCitation },
117
+ setup() {
118
+ const sources = [
119
+ { index: 1, fileName: 'q3-report.pdf', locationLabel: 'Page 3' },
120
+ { index: 2, fileName: 'README.md' },
121
+ { index: 3, fileName: 'sales-2026.csv', locationLabel: 'Row 42' },
122
+ ];
123
+ return { sources, snippet: SNIPPET };
124
+ },
125
+ template: `
126
+ <p style="font-size: 14px; line-height: 20px;">
127
+ Multiple sources back this claim.<CkCitation
128
+ v-for="s in sources"
129
+ :key="s.index"
130
+ v-bind="s"
131
+ :snippet="snippet"
132
+ />
133
+ </p>
134
+ `,
135
+ }),
136
+ };
@@ -0,0 +1,145 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, ref } from 'vue';
3
+ import {
4
+ UiPopover,
5
+ UiPopoverTrigger,
6
+ UiPopoverContent,
7
+ } from '@aleph-alpha/ui-library';
8
+ import {
9
+ defaultChatKitLabels,
10
+ useChatKitLabels,
11
+ } from '../../../handlers/useChatKitLabels';
12
+ import { CkCitationCard } from '../CkCitationCard';
13
+ import type { CkCitationProps } from './types';
14
+
15
+ defineOptions({
16
+ name: 'CkCitation',
17
+ });
18
+
19
+ const props = withDefaults(defineProps<CkCitationProps>(), {
20
+ selected: false,
21
+ locationLabel: undefined,
22
+ url: undefined,
23
+ ariaLabel: undefined,
24
+ openSourceLabel: undefined,
25
+ });
26
+
27
+ const emit = defineEmits<{
28
+ 'open-source': [url: string];
29
+ }>();
30
+
31
+ const labels = useChatKitLabels();
32
+
33
+ // Resolution order: per-instance prop, then `"{citationSourceLabel} {index}"`
34
+ // from the injected label, then the English default.
35
+ const triggerLabel = computed(
36
+ () =>
37
+ props.ariaLabel ??
38
+ `${labels.value.citationSourceLabel ?? defaultChatKitLabels.citationSourceLabel!} ${props.index}`,
39
+ );
40
+
41
+ // Close delay so the pointer can cross the gap from the marker into the card
42
+ // without it disappearing on the way.
43
+ const CLOSE_DELAY_MS = 80;
44
+
45
+ const open = ref(false);
46
+ // Tracks whether the current open was driven by the pointer (hover) rather than
47
+ // the keyboard, so we can suppress the popover's focus management for hover only.
48
+ const isPointerInteraction = ref(false);
49
+ let closeTimer: ReturnType<typeof setTimeout> | undefined;
50
+
51
+ function show(): void {
52
+ if (closeTimer) {
53
+ clearTimeout(closeTimer);
54
+ closeTimer = undefined;
55
+ }
56
+ open.value = true;
57
+ }
58
+
59
+ function scheduleHide(): void {
60
+ if (closeTimer) clearTimeout(closeTimer);
61
+ closeTimer = setTimeout(() => {
62
+ open.value = false;
63
+ closeTimer = undefined;
64
+ }, CLOSE_DELAY_MS);
65
+ }
66
+
67
+ function onPointerEnter(): void {
68
+ isPointerInteraction.value = true;
69
+ show();
70
+ }
71
+
72
+ function onPointerLeave(): void {
73
+ isPointerInteraction.value = false;
74
+ scheduleHide();
75
+ }
76
+
77
+ // Suppress auto-focus for hover opens only (so hovering never steals focus or
78
+ // scrolls). Keyboard opens keep it, moving focus into the card so the source
79
+ // action is reachable.
80
+ function onOpenAutoFocus(event: Event): void {
81
+ if (isPointerInteraction.value) event.preventDefault();
82
+ }
83
+
84
+ // Close once focus leaves the card entirely (keyboard users tabbing out). Moving
85
+ // focus between elements inside the card keeps it open.
86
+ function onContentFocusOut(event: FocusEvent): void {
87
+ const nextTarget = event.relatedTarget as Node | null;
88
+ const content = event.currentTarget as HTMLElement;
89
+ if (!nextTarget || !content.contains(nextTarget)) {
90
+ scheduleHide();
91
+ }
92
+ }
93
+
94
+ onBeforeUnmount(() => {
95
+ if (closeTimer) clearTimeout(closeTimer);
96
+ });
97
+ </script>
98
+
99
+ <template>
100
+ <UiPopover v-model:open="open">
101
+ <UiPopoverTrigger as-child>
102
+ <button
103
+ type="button"
104
+ :aria-label="triggerLabel"
105
+ :class="[
106
+ 'ck-citation mx-px inline-flex h-5 min-w-5 select-none items-center justify-center rounded-sm px-1 align-middle text-sm leading-4 tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
107
+ selected
108
+ ? 'bg-background-accent-soft text-black'
109
+ : 'bg-background-accent-yellow text-background-status-warning',
110
+ ]"
111
+ @mouseenter="onPointerEnter"
112
+ @mouseleave="onPointerLeave"
113
+ >
114
+ {{ index }}
115
+ </button>
116
+ </UiPopoverTrigger>
117
+ <UiPopoverContent
118
+ align="start"
119
+ :side-offset="8"
120
+ class="w-auto border-0 bg-transparent p-0 shadow-none"
121
+ @open-auto-focus="onOpenAutoFocus"
122
+ @mouseenter="onPointerEnter"
123
+ @mouseleave="onPointerLeave"
124
+ @focusin="show"
125
+ @focusout="onContentFocusOut"
126
+ >
127
+ <CkCitationCard
128
+ :file-name="fileName"
129
+ :snippet="snippet"
130
+ :location-label="locationLabel"
131
+ :url="url"
132
+ :open-source-label="openSourceLabel"
133
+ @open-source="(u: string) => emit('open-source', u)"
134
+ />
135
+ </UiPopoverContent>
136
+ </UiPopover>
137
+ </template>
138
+
139
+ <style scoped>
140
+ @media (prefers-reduced-motion: reduce) {
141
+ .ck-citation {
142
+ transition-duration: 0.01ms;
143
+ }
144
+ }
145
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as CkCitation } from './CkCitation.vue';
2
+ export type * from './types';
@@ -0,0 +1,80 @@
1
+ /**
2
+ * A single citation source: the data needed to render a marker and its card.
3
+ * This is the per-source shape the host maps each backend citation into (the
4
+ * `index` matches the `[n]` reference in the answer text) and the element type
5
+ * of `CkMarkdownRenderer`'s `citations` prop.
6
+ *
7
+ * @category Data Display
8
+ * @useCases describe a cited source, build the citations map for an assistant reply
9
+ * @keywords citation, source, reference, file, page, snippet
10
+ * @related CkCitation, CkCitationCard, CkMarkdownRenderer
11
+ */
12
+ export interface CkCitationSource {
13
+ /**
14
+ * 1-based source number rendered in the marker. Matches the `[n]`
15
+ * reference it replaces in the answer text.
16
+ */
17
+ index: number;
18
+ /**
19
+ * Source file name. Title of the card and basis for the file-type badge
20
+ * (pass the original name with its extension).
21
+ */
22
+ fileName: string;
23
+ /** The cited chunk text, shown in the card body. */
24
+ snippet: string;
25
+ /**
26
+ * Location label for the card footer, e.g. `"Page 3"`. Omit for
27
+ * non-paginated sources.
28
+ */
29
+ locationLabel?: string;
30
+ /**
31
+ * Source URL for the card's external-link action. Activating it emits
32
+ * `open-source`. Omit to hide the action.
33
+ */
34
+ url?: string;
35
+ }
36
+
37
+ /**
38
+ * An inline citation marker — the numbered chip rendered in place of a `[n]`
39
+ * reference inside an assistant answer. On hover, or when activated by
40
+ * keyboard (Enter/Space) or click, it reveals a `CkCitationCard` describing
41
+ * the source.
42
+ *
43
+ * The marker shows the bare source number (no brackets). Activating the
44
+ * card's external-link action re-emits `open-source` so the host can open
45
+ * the (possibly auth-proxied) source URL.
46
+ *
47
+ * @category Data Display
48
+ * @useCases inline citation in assistant reply, numbered source reference, hover to preview the cited file/page
49
+ * @keywords citation, source, reference, inline, marker, footnote, number, hover, popover
50
+ * @related CkCitationCard, CkMarkdownRenderer, CkAgentMessage
51
+ */
52
+ export interface CkCitationProps extends CkCitationSource {
53
+ /**
54
+ * Marks the citation as selected/active (e.g. the source it points to is
55
+ * currently open or highlighted). Renders the marker in its selected (teal)
56
+ * state instead of the default amber.
57
+ * @default false
58
+ */
59
+ selected?: boolean;
60
+ /**
61
+ * Accessible label for the marker trigger. When omitted, resolves to
62
+ * `"{citationSourceLabel} {index}"` from `useChatKitLabels` (English default
63
+ * `"Source {index}"`). Set this to override per instance.
64
+ */
65
+ ariaLabel?: string;
66
+ /**
67
+ * Accessible label for the card's external-link action. When omitted,
68
+ * resolves from `useChatKitLabels().citationOpenSource` (English default
69
+ * `"Open source"`).
70
+ */
71
+ openSourceLabel?: string;
72
+ }
73
+
74
+ export interface CkCitationEmits {
75
+ /**
76
+ * Re-emitted from the card when the user opens the source. The payload is
77
+ * `url`.
78
+ */
79
+ (e: 'open-source', url: string): void;
80
+ }