@aleph-alpha/chat-kit 5.11.0 → 5.13.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 (69) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-SL1vOAd9.js → CkHistory.vue_vue_type_script_setup_true_lang-BNcJURkq.js} +216 -87
  2. package/dist/chat-kit.css +13 -13
  3. package/dist/components/base/CkCitation/CkCitation.stories.d.ts +1 -4
  4. package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -1
  5. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
  6. package/dist/components/base/CkCitation/types.d.ts +6 -1
  7. package/dist/components/base/CkCitation/types.d.ts.map +1 -1
  8. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +13 -2
  9. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -1
  10. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +1 -0
  11. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
  12. package/dist/components/base/CkCitationCard/types.d.ts +6 -1
  13. package/dist/components/base/CkCitationCard/types.d.ts.map +1 -1
  14. package/dist/components/base/CkCitationLabel/CkCitationLabel.stories.d.ts +39 -0
  15. package/dist/components/base/CkCitationLabel/CkCitationLabel.stories.d.ts.map +1 -0
  16. package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts +9 -0
  17. package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts.map +1 -0
  18. package/dist/components/base/CkCitationLabel/index.d.ts +3 -0
  19. package/dist/components/base/CkCitationLabel/index.d.ts.map +1 -0
  20. package/dist/components/base/CkCitationLabel/types.d.ts +44 -0
  21. package/dist/components/base/CkCitationLabel/types.d.ts.map +1 -0
  22. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +7 -0
  23. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -1
  24. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
  25. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -1
  26. package/dist/components/base/CkFeedbackDialog/types.d.ts +19 -0
  27. package/dist/components/base/CkFeedbackDialog/types.d.ts.map +1 -1
  28. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
  29. package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts.map +1 -1
  30. package/dist/components/base/index.d.ts +2 -0
  31. package/dist/components/base/index.d.ts.map +1 -1
  32. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +12 -0
  33. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  34. package/dist/components/index.d.ts +2 -2
  35. package/dist/components/index.d.ts.map +1 -1
  36. package/dist/components/index.js +28 -27
  37. package/dist/composables/useIsTruncated.d.ts +22 -0
  38. package/dist/composables/useIsTruncated.d.ts.map +1 -0
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +29 -28
  42. package/dist/types.d.ts +33 -4
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +5 -5
  45. package/src/components/base/CkCitation/CkCitation.spec.ts +14 -3
  46. package/src/components/base/CkCitation/CkCitation.stories.ts +85 -3
  47. package/src/components/base/CkCitation/CkCitation.vue +13 -36
  48. package/src/components/base/CkCitation/types.ts +6 -1
  49. package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +37 -0
  50. package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +30 -3
  51. package/src/components/base/CkCitationCard/CkCitationCard.vue +53 -11
  52. package/src/components/base/CkCitationCard/types.ts +6 -1
  53. package/src/components/base/CkCitationLabel/CkCitationLabel.spec.ts +73 -0
  54. package/src/components/base/CkCitationLabel/CkCitationLabel.stories.ts +86 -0
  55. package/src/components/base/CkCitationLabel/CkCitationLabel.vue +70 -0
  56. package/src/components/base/CkCitationLabel/index.ts +2 -0
  57. package/src/components/base/CkCitationLabel/types.ts +43 -0
  58. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +36 -0
  59. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +42 -0
  60. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +11 -1
  61. package/src/components/base/CkFeedbackDialog/types.ts +20 -0
  62. package/src/components/base/CkUserMessage/CkUserMessage.vue +1 -1
  63. package/src/components/base/index.ts +2 -0
  64. package/src/components/composed/CkConversation/CkConversation.spec.ts +65 -2
  65. package/src/components/composed/CkConversation/CkConversation.stories.ts +136 -0
  66. package/src/components/index.ts +2 -0
  67. package/src/composables/useIsTruncated.ts +82 -0
  68. package/src/index.ts +2 -0
  69. package/src/types.ts +34 -4
package/dist/index.js CHANGED
@@ -1,43 +1,44 @@
1
1
  import { u } from "./useMessageHandler-Tellov3w.js";
2
2
  import { C, d, p, u as u2, b } from "./useChatKitLabels-CMOr3Kjq.js";
3
3
  import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-Dn4ctxk8.js";
4
- import { _, a as a2, C as C2, b as b3, c as c2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A } from "./CkHistory.vue_vue_type_script_setup_true_lang-SL1vOAd9.js";
4
+ import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B } from "./CkHistory.vue_vue_type_script_setup_true_lang-BNcJURkq.js";
5
5
  export {
6
6
  C as CHAT_KIT_LABELS,
7
7
  _ as CkAgentMessage,
8
8
  a2 as CkAgentMessageHeader,
9
- C2 as CkCitation,
10
- b3 as CkCitationCard,
11
- c2 as CkConversation,
12
- d3 as CkConversationLayout,
13
- e2 as CkCopyMessageButton,
14
- f2 as CkDropZoneOverlay,
15
- g as CkEditableUserMessage,
16
- h as CkFeedbackButton,
17
- i as CkFeedbackDialog,
18
- j as CkFileItemsList,
19
- k as CkHeaderActivityButton,
20
- l as CkHistory,
21
- m as CkHistoryItem,
22
- n as CkHistoryWrapper,
23
- o as CkMarkdownRenderer,
24
- p2 as CkMessageButton,
25
- q as CkModelSelector,
26
- r as CkPromptInput,
27
- s as CkPromptInputDisclaimer,
28
- t as CkPromptMenuButton,
29
- u4 as CkPromptSendButton,
30
- v as CkPromptStopButton,
31
- w as CkRegenerateMessageButton,
32
- x as CkSendIcon,
33
- y as CkStopIcon,
34
- z as CkUserMessage,
9
+ b3 as CkCitation,
10
+ c2 as CkCitationCard,
11
+ C2 as CkCitationLabel,
12
+ d3 as CkConversation,
13
+ e2 as CkConversationLayout,
14
+ f2 as CkCopyMessageButton,
15
+ g as CkDropZoneOverlay,
16
+ h as CkEditableUserMessage,
17
+ i as CkFeedbackButton,
18
+ j as CkFeedbackDialog,
19
+ k as CkFileItemsList,
20
+ l as CkHeaderActivityButton,
21
+ m as CkHistory,
22
+ n as CkHistoryItem,
23
+ o as CkHistoryWrapper,
24
+ p2 as CkMarkdownRenderer,
25
+ q as CkMessageButton,
26
+ r as CkModelSelector,
27
+ s as CkPromptInput,
28
+ t as CkPromptInputDisclaimer,
29
+ u4 as CkPromptMenuButton,
30
+ v as CkPromptSendButton,
31
+ w as CkPromptStopButton,
32
+ x as CkRegenerateMessageButton,
33
+ y as CkSendIcon,
34
+ z as CkStopIcon,
35
+ A as CkUserMessage,
35
36
  S as SdkError,
36
37
  c as createDocumentUploadProvider,
37
38
  d as defaultChatKitLabels,
38
39
  d2 as defaultDocumentUploadConfig,
39
40
  f as defaultEndpoints,
40
- A as formatRelativeTime,
41
+ B as formatRelativeTime,
41
42
  p as provideChatKitLabels,
42
43
  u3 as useChat,
43
44
  u2 as useChatKitLabels,
package/dist/types.d.ts CHANGED
@@ -57,6 +57,35 @@ export interface MessageReasoningContent {
57
57
  * Discriminated union of all content parts that can appear inside a message.
58
58
  */
59
59
  export type MessageContent = MessageTextContent | MessageToolContent | MessageReasoningContent;
60
+ declare global {
61
+ /**
62
+ * Extension point for {@link MessageMetadata}. Consumers declare their
63
+ * well-known metadata keys here and get them typed on every message,
64
+ * without chat-kit knowing what any of them mean:
65
+ *
66
+ * ```ts
67
+ * // app/src/types/chat-kit.d.ts
68
+ * declare global {
69
+ * interface ChatKitMessageMetadata {
70
+ * responseId?: string;
71
+ * citations?: CitationManifest;
72
+ * }
73
+ * }
74
+ * export {};
75
+ * ```
76
+ *
77
+ * Declare every key **optional**: chat-kit itself never populates them, and
78
+ * a message can reach a consumer before its host has stamped anything (mid
79
+ * stream, or on an optimistic node).
80
+ *
81
+ * This is a `declare global` interface rather than a module augmentation
82
+ * because `MessageMetadata` is re-exported through the package barrel —
83
+ * `declare module '@aleph-alpha/chat-kit'` does not merge across a
84
+ * re-export, and would silently type nothing.
85
+ */
86
+ interface ChatKitMessageMetadata {
87
+ }
88
+ }
60
89
  /**
61
90
  * Open-ended metadata bag carried alongside a message. chat-kit is
62
91
  * intentionally agnostic about its contents — adapter authors
@@ -64,11 +93,11 @@ export type MessageContent = MessageTextContent | MessageToolContent | MessageRe
64
93
  * through the tree → linear path projection, but never inspect it.
65
94
  *
66
95
  * Consumers attach their own well-known keys (attachments, citations,
67
- * ratings, …) and define their own typed projections at the read site
68
- * rather than extending this base type. That keeps the handler API stable
69
- * even as the shape of what gets stored on a message evolves.
96
+ * ratings, …). Declaring them on {@link ChatKitMessageMetadata} types those
97
+ * reads everywhere; the retained index signature keeps any other key legal,
98
+ * so the handler API stays stable as the stored shape evolves.
70
99
  */
71
- export type MessageMetadata = Record<string, unknown>;
100
+ export type MessageMetadata = ChatKitMessageMetadata & Record<string, unknown>;
72
101
  export interface BaseMessage {
73
102
  id: string;
74
103
  role: MessageRole;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE1D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,WAAW,GAAG,YAAY,CAAC;AAE9E;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,kBAAkB,GAClB,uBAAuB,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACnC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAC;IAClB,+CAA+C;IAC/C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/chat-kit",
3
- "version": "5.11.0",
3
+ "version": "5.13.0",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -67,7 +67,6 @@
67
67
  "vue": "^3.5.27"
68
68
  },
69
69
  "dependencies": {
70
- "@aleph-alpha/ui-library": "^5.3.0",
71
70
  "dompurify": "^3.3.2",
72
71
  "katex": "^0.17.0",
73
72
  "mdast-util-find-and-replace": "^3.0.2",
@@ -77,7 +76,8 @@
77
76
  "remark-math": "^6.0.0",
78
77
  "remark-parse": "^11.0.0",
79
78
  "remark-rehype": "^11.1.2",
80
- "unified": "^11.0.5"
79
+ "unified": "^11.0.5",
80
+ "@aleph-alpha/ui-library": "5.10.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@storybook/addon-a11y": "10.2.17",
@@ -108,8 +108,8 @@
108
108
  "vue-tsc": "^3.2.4",
109
109
  "wait-on": "9.0.3",
110
110
  "@aleph-alpha/eslint-config-frontend": "0.7.1",
111
- "@aleph-alpha/tsconfig-frontend": "0.7.2",
112
- "@aleph-alpha/prettier-config-frontend": "2.1.1"
111
+ "@aleph-alpha/prettier-config-frontend": "2.1.1",
112
+ "@aleph-alpha/tsconfig-frontend": "0.7.3"
113
113
  },
114
114
  "scripts": {
115
115
  "build": "vite build && vue-tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
@@ -61,11 +61,22 @@ describe('CkCitation', () => {
61
61
  expect(await screen.findByText('q3-report.pdf')).toBeInTheDocument();
62
62
  });
63
63
 
64
- it('renders the default (amber) marker state by default', () => {
64
+ // The marker is a `CkCitationLabel` passed to `UiPopoverTrigger as-child`,
65
+ // so the popover's trigger props have to reach the rendered button through
66
+ // attribute fallthrough. Guards that composition against regressions.
67
+ it('forwards the popover trigger attributes onto the marker', () => {
65
68
  render(CkCitation, { props: base });
66
69
 
67
70
  const marker = screen.getByRole('button', { name: 'Source 1' });
68
- expect(marker.className).toContain('bg-background-accent-yellow');
71
+ expect(marker).toHaveAttribute('aria-haspopup', 'dialog');
72
+ expect(marker).toHaveAttribute('aria-expanded', 'false');
73
+ });
74
+
75
+ it('renders the default (neutral) marker state by default', () => {
76
+ render(CkCitation, { props: base });
77
+
78
+ const marker = screen.getByRole('button', { name: 'Source 1' });
79
+ expect(marker.className).toContain('bg-background-surface-citation');
69
80
  expect(marker.className).not.toContain('bg-background-accent-soft');
70
81
  });
71
82
 
@@ -74,6 +85,6 @@ describe('CkCitation', () => {
74
85
 
75
86
  const marker = screen.getByRole('button', { name: 'Source 1' });
76
87
  expect(marker.className).toContain('bg-background-accent-soft');
77
- expect(marker.className).not.toContain('bg-background-accent-yellow');
88
+ expect(marker.className).not.toContain('bg-background-surface-citation');
78
89
  });
79
90
  });
@@ -1,4 +1,5 @@
1
1
  import CkCitation from './CkCitation.vue';
2
+ import { CkCitationLabel } from '../CkCitationLabel';
2
3
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
4
 
4
5
  const SNIPPET =
@@ -44,10 +45,91 @@ export default meta;
44
45
  type Story = StoryObj<typeof meta>;
45
46
 
46
47
  /**
47
- * A single marker in its default (amber) state. Hover the chip (or focus it and
48
- * press Enter) to reveal the source card.
48
+ * A single marker in its default (neutral) state. Hover the chip (or focus it
49
+ * and press Enter) to reveal the source card.
49
50
  */
50
- export const Default: Story = {};
51
+ const defaultTemplateSource = `<script setup lang="ts">
52
+ import { CkCitation } from '@aleph-alpha/chat-kit';
53
+
54
+ function openSource(url: string) {
55
+ // The source URL is often auth-proxied, so the host opens it.
56
+ window.open(url, '_blank', 'noopener');
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <p>
62
+ Departments with tailored training adopted the tools faster.<CkCitation
63
+ :index="1"
64
+ file-name="Q2_Business_Plan.pdf"
65
+ snippet="Lorem ipsum dolor sit amet consectetur…"
66
+ location-label="Page 3"
67
+ collection-name="Collection name"
68
+ url="/api/v1/search-stores/ss_abc/files/file_123/public-urls"
69
+ @open-source="openSource"
70
+ />
71
+ </p>
72
+ </template>`;
73
+
74
+ export const Default: Story = {
75
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
76
+ };
77
+
78
+ /**
79
+ * The four marker states side by side, as specified in the design: default,
80
+ * hover, focus and selected. Hover and focus are live — mouse over the second
81
+ * chip or tab to the third to see them.
82
+ */
83
+ const statesTemplateSource = `<script setup lang="ts">
84
+ import { CkCitationLabel } from '@aleph-alpha/chat-kit';
85
+ </script>
86
+
87
+ <template>
88
+ <!-- \`hover-layer-active\` pins the hover overlay open; focus is a real
89
+ :focus-visible, so it only shows when the chip is focused. -->
90
+ <CkCitationLabel as="button" :index="1" />
91
+ <CkCitationLabel as="button" :index="1" class="hover-layer-active" />
92
+ <CkCitationLabel as="button" :index="1" />
93
+ <CkCitationLabel as="button" :index="1" selected />
94
+ </template>`;
95
+
96
+ export const States: Story = {
97
+ parameters: { docs: { source: { code: statesTemplateSource } } },
98
+ render: (args) => ({
99
+ components: { CkCitationLabel },
100
+ setup() {
101
+ return { args };
102
+ },
103
+ // `CkCitationLabel` renders the chip on its own, so each state can be shown
104
+ // at rest. Hover is forced with `hover-layer-active` (the class ui-library
105
+ // uses to pin its overlay open); focus is real, applied by the play
106
+ // function below, because `:focus-visible` cannot be faked with a class.
107
+ template: `
108
+ <div style="display: flex; align-items: flex-start; gap: 32px;">
109
+ <div style="display: grid; justify-items: center; gap: 8px;">
110
+ <CkCitationLabel v-bind="args" :index="1" as="button" />
111
+ <small style="font-size: 11px; opacity: 0.6;">default</small>
112
+ </div>
113
+ <div style="display: grid; justify-items: center; gap: 8px;">
114
+ <CkCitationLabel v-bind="args" :index="1" as="button" class="hover-layer-active" />
115
+ <small style="font-size: 11px; opacity: 0.6;">hover</small>
116
+ </div>
117
+ <div style="display: grid; justify-items: center; gap: 8px;">
118
+ <CkCitationLabel v-bind="args" :index="1" as="button" data-focus-demo />
119
+ <small style="font-size: 11px; opacity: 0.6;">focus</small>
120
+ </div>
121
+ <div style="display: grid; justify-items: center; gap: 8px;">
122
+ <CkCitationLabel v-bind="args" :index="1" as="button" selected />
123
+ <small style="font-size: 11px; opacity: 0.6;">selected</small>
124
+ </div>
125
+ </div>
126
+ `,
127
+ }),
128
+ play: async ({ canvasElement }) => {
129
+ // A real focus, so the ring is the one keyboard users actually get.
130
+ canvasElement.querySelector<HTMLElement>('[data-focus-demo]')?.focus();
131
+ },
132
+ };
51
133
 
52
134
  /**
53
135
  * The selected (teal) state — set `selected` when the citation's source is the
@@ -1,22 +1,19 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, ref } from 'vue';
2
+ import { onBeforeUnmount, ref } from 'vue';
3
3
  import {
4
4
  UiPopover,
5
5
  UiPopoverTrigger,
6
6
  UiPopoverContent,
7
7
  } from '@aleph-alpha/ui-library';
8
- import {
9
- defaultChatKitLabels,
10
- useChatKitLabels,
11
- } from '../../../handlers/useChatKitLabels';
12
8
  import { CkCitationCard } from '../CkCitationCard';
9
+ import { CkCitationLabel } from '../CkCitationLabel';
13
10
  import type { CkCitationProps } from './types';
14
11
 
15
12
  defineOptions({
16
13
  name: 'CkCitation',
17
14
  });
18
15
 
19
- const props = withDefaults(defineProps<CkCitationProps>(), {
16
+ withDefaults(defineProps<CkCitationProps>(), {
20
17
  selected: false,
21
18
  locationLabel: undefined,
22
19
  url: undefined,
@@ -28,16 +25,6 @@ const emit = defineEmits<{
28
25
  'open-source': [url: string];
29
26
  }>();
30
27
 
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
28
  // Close delay so the pointer can cross the gap from the marker into the card
42
29
  // without it disappearing on the way.
43
30
  const CLOSE_DELAY_MS = 80;
@@ -122,21 +109,18 @@ onBeforeUnmount(() => {
122
109
  <template>
123
110
  <UiPopover v-model:open="open">
124
111
  <UiPopoverTrigger as-child>
125
- <button
126
- type="button"
127
- :aria-label="triggerLabel"
128
- :class="[
129
- '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',
130
- selected
131
- ? 'bg-background-accent-soft text-black'
132
- : 'bg-background-accent-yellow text-background-status-warning',
133
- ]"
112
+ <!-- An open card is itself a selection: the chip the user is reading
113
+ about shows the selected state, so it stands out from its neighbours
114
+ while the card is up. -->
115
+ <CkCitationLabel
116
+ as="button"
117
+ :index="index"
118
+ :selected="selected || open"
119
+ :aria-label="ariaLabel"
134
120
  @keydown="onTriggerKeydown"
135
121
  @mouseenter="onPointerEnter"
136
122
  @mouseleave="onPointerLeave"
137
- >
138
- {{ index }}
139
- </button>
123
+ />
140
124
  </UiPopoverTrigger>
141
125
  <UiPopoverContent
142
126
  align="start"
@@ -153,6 +137,7 @@ onBeforeUnmount(() => {
153
137
  :file-name="fileName"
154
138
  :snippet="snippet"
155
139
  :location-label="locationLabel"
140
+ :collection-name="collectionName"
156
141
  :url="url"
157
142
  :open-source-label="openSourceLabel"
158
143
  @open-source="(u: string) => emit('open-source', u)"
@@ -160,11 +145,3 @@ onBeforeUnmount(() => {
160
145
  </UiPopoverContent>
161
146
  </UiPopover>
162
147
  </template>
163
-
164
- <style scoped>
165
- @media (prefers-reduced-motion: reduce) {
166
- .ck-citation {
167
- transition-duration: 0.01ms;
168
- }
169
- }
170
- </style>
@@ -27,6 +27,11 @@ export interface CkCitationSource {
27
27
  * non-paginated sources.
28
28
  */
29
29
  locationLabel?: string;
30
+ /**
31
+ * Name of the collection the source belongs to, shown in the card footer
32
+ * before the location. Omit when the source has no collection.
33
+ */
34
+ collectionName?: string;
30
35
  /**
31
36
  * Source URL for the card's external-link action. Activating it emits
32
37
  * `open-source`. Omit to hide the action.
@@ -53,7 +58,7 @@ export interface CkCitationProps extends CkCitationSource {
53
58
  /**
54
59
  * Marks the citation as selected/active (e.g. the source it points to is
55
60
  * currently open or highlighted). Renders the marker in its selected (teal)
56
- * state instead of the default amber.
61
+ * state instead of the default neutral one.
57
62
  * @default false
58
63
  */
59
64
  selected?: boolean;
@@ -69,6 +69,43 @@ describe('CkCitationCard', () => {
69
69
  expect(screen.getByText(snippet).className).toContain('line-clamp-6');
70
70
  });
71
71
 
72
+ it('shows the collection name before the location in the footer', () => {
73
+ render(CkCitationCard, {
74
+ props: { ...base, collectionName: 'EU AI Act', locationLabel: 'Page 3' },
75
+ });
76
+
77
+ const footer = screen.getByText('EU AI Act').closest('p');
78
+ expect(footer).toHaveTextContent('EU AI Act | Page 3');
79
+ });
80
+
81
+ it('drops the separator when only one footer part is present', () => {
82
+ render(CkCitationCard, { props: { ...base, collectionName: 'EU AI Act' } });
83
+
84
+ expect(screen.getByText('EU AI Act').closest('p')).not.toHaveTextContent(
85
+ '|',
86
+ );
87
+ });
88
+
89
+ it('hides the footer entirely with neither a collection nor a location', () => {
90
+ render(CkCitationCard, { props: base });
91
+
92
+ expect(screen.queryByText('Page 3')).not.toBeInTheDocument();
93
+ expect(screen.queryByText('|')).not.toBeInTheDocument();
94
+ });
95
+
96
+ it('keeps the location intact when the collection name is too long', () => {
97
+ const collectionName =
98
+ 'Public Sector Knowledge Base — European Union Regulatory Corpus 2026';
99
+ render(CkCitationCard, {
100
+ props: { ...base, collectionName, locationLabel: 'Page 3' },
101
+ });
102
+
103
+ // jsdom has no layout, so the guarantee is asserted through the classes
104
+ // that produce it: the name is the only part allowed to give way.
105
+ expect(screen.getByText(collectionName).className).toContain('truncate');
106
+ expect(screen.getByText('Page 3').className).toContain('shrink-0');
107
+ });
108
+
72
109
  it('uses the injected citationOpenSource label via provideChatKitLabels', () => {
73
110
  const Wrapper = defineComponent({
74
111
  setup() {
@@ -20,6 +20,7 @@ const meta: Meta<typeof CkCitationCard> = {
20
20
  fileName: { control: 'text' },
21
21
  snippet: { control: 'text' },
22
22
  locationLabel: { control: 'text' },
23
+ collectionName: { control: 'text' },
23
24
  url: { control: 'text' },
24
25
  openSourceLabel: { control: 'text' },
25
26
  },
@@ -27,6 +28,7 @@ const meta: Meta<typeof CkCitationCard> = {
27
28
  fileName: 'Q2_Business_Plan.pdf',
28
29
  snippet: LOREM,
29
30
  locationLabel: 'Page 3',
31
+ collectionName: 'Collection name',
30
32
  url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
31
33
  },
32
34
  decorators: [
@@ -76,19 +78,44 @@ export const WithoutLink: Story = {
76
78
  };
77
79
 
78
80
  /**
79
- * No `locationLabel` — the footer is hidden. Use for non-paginated sources
80
- * such as a plain web page or a whole-document citation.
81
+ * No `locationLabel` — the footer keeps just the collection. Use for
82
+ * non-paginated sources such as a plain web page.
81
83
  */
82
84
  export const WithoutLocation: Story = {
83
85
  args: { locationLabel: undefined },
84
86
  };
85
87
 
88
+ /**
89
+ * No `collectionName` — the footer keeps just the location, and the separator
90
+ * goes with it. Use for sources that don't come from a collection.
91
+ */
92
+ export const WithoutCollection: Story = {
93
+ args: { collectionName: undefined },
94
+ };
95
+
96
+ /**
97
+ * A collection name longer than the card is wide. The name truncates and the
98
+ * location stays on the same line — the page a citation points at is the more
99
+ * useful half, so it must not be pushed out or wrapped away.
100
+ */
101
+ export const LongCollectionName: Story = {
102
+ args: {
103
+ collectionName:
104
+ 'Public Sector Knowledge Base — European Union Regulatory Corpus 2026',
105
+ locationLabel: 'Page 3',
106
+ },
107
+ };
108
+
86
109
  /**
87
110
  * Minimal: title + excerpt only (no link, no location). The leanest source a
88
111
  * card can describe.
89
112
  */
90
113
  export const MinimalSnippetOnly: Story = {
91
- args: { url: undefined, locationLabel: undefined },
114
+ args: {
115
+ url: undefined,
116
+ locationLabel: undefined,
117
+ collectionName: undefined,
118
+ },
92
119
  };
93
120
 
94
121
  /**
@@ -1,6 +1,12 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue';
3
- import { UiFileType, UiButton, UiIcon } from '@aleph-alpha/ui-library';
2
+ import { computed, useTemplateRef } from 'vue';
3
+ import {
4
+ UiFileType,
5
+ UiButton,
6
+ UiIcon,
7
+ UiTooltip,
8
+ } from '@aleph-alpha/ui-library';
9
+ import { useIsTruncated } from '../../../composables/useIsTruncated';
4
10
  import {
5
11
  defaultChatKitLabels,
6
12
  useChatKitLabels,
@@ -13,6 +19,7 @@ defineOptions({
13
19
 
14
20
  const props = withDefaults(defineProps<CkCitationCardProps>(), {
15
21
  locationLabel: undefined,
22
+ collectionName: undefined,
16
23
  url: undefined,
17
24
  openSourceLabel: undefined,
18
25
  });
@@ -31,6 +38,16 @@ const openSourceAriaLabel = computed(
31
38
  defaultChatKitLabels.citationOpenSource!,
32
39
  );
33
40
 
41
+ // Both labels are clipped to the card width, so each gets the same tooltip
42
+ // treatment as a file row: revealed only when the text is actually cut off.
43
+ const fileNameRef = useTemplateRef<HTMLElement>('fileNameRef');
44
+ const collectionRef = useTemplateRef<HTMLElement>('collectionRef');
45
+ const isFileNameTruncated = useIsTruncated(fileNameRef, () => props.fileName);
46
+ const isCollectionTruncated = useIsTruncated(
47
+ collectionRef,
48
+ () => props.collectionName,
49
+ );
50
+
34
51
  function handleOpen(): void {
35
52
  if (props.url) {
36
53
  emit('open-source', props.url);
@@ -42,14 +59,20 @@ function handleOpen(): void {
42
59
  <div
43
60
  class="ck-citation-card flex w-[22.5rem] max-w-[90vw] flex-col gap-2 rounded-md bg-background-surface-floating px-6 pb-6 pt-4 shadow-md"
44
61
  >
45
- <div class="flex items-center gap-2">
62
+ <div class="flex min-w-0 items-center gap-3 py-0.5">
46
63
  <UiFileType :filename="fileName" class="shrink-0" />
47
- <p
48
- class="min-w-0 truncate text-sm font-medium text-content-on-surface-primary"
49
- :title="fileName"
64
+ <UiTooltip
65
+ :content="fileName"
66
+ :disabled="!isFileNameTruncated"
67
+ align="start"
50
68
  >
51
- {{ fileName }}
52
- </p>
69
+ <p
70
+ ref="fileNameRef"
71
+ class="min-w-0 truncate text-sm font-medium leading-4 text-content-on-surface-primary"
72
+ >
73
+ {{ fileName }}
74
+ </p>
75
+ </UiTooltip>
53
76
  <UiButton
54
77
  v-if="url"
55
78
  variant="ghost"
@@ -68,11 +91,30 @@ function handleOpen(): void {
68
91
  {{ snippet }}
69
92
  </p>
70
93
 
94
+ <!-- The location is the more useful half, so the collection name is the
95
+ part that gives way when the two don't fit on one line. -->
71
96
  <p
72
- v-if="locationLabel"
73
- class="text-sm leading-5 text-content-on-surface-muted"
97
+ v-if="collectionName || locationLabel"
98
+ class="flex items-center gap-1.5 text-sm leading-5 text-content-on-surface-muted"
74
99
  >
75
- {{ locationLabel }}
100
+ <UiTooltip
101
+ v-if="collectionName"
102
+ :content="collectionName"
103
+ :disabled="!isCollectionTruncated"
104
+ align="start"
105
+ >
106
+ <span ref="collectionRef" class="min-w-0 truncate">
107
+ {{ collectionName }}
108
+ </span>
109
+ </UiTooltip>
110
+ <span
111
+ v-if="collectionName && locationLabel"
112
+ aria-hidden="true"
113
+ class="shrink-0 text-border-input-strong"
114
+ >
115
+ |
116
+ </span>
117
+ <span v-if="locationLabel" class="shrink-0">{{ locationLabel }}</span>
76
118
  </p>
77
119
  </div>
78
120
  </template>
@@ -25,9 +25,14 @@ export interface CkCitationCardProps {
25
25
  /**
26
26
  * Location label shown in the footer, e.g. `"Page 3"`. Pass a fully
27
27
  * formatted, localized string — the component does no number formatting.
28
- * Omit for non-paginated sources to hide the footer.
28
+ * Omit for non-paginated sources.
29
29
  */
30
30
  locationLabel?: string;
31
+ /**
32
+ * Name of the collection the source belongs to, shown in the footer before
33
+ * the location. Omit when the source has no collection.
34
+ */
35
+ collectionName?: string;
31
36
  /**
32
37
  * When set, the external-link action is shown in the header. Activating it
33
38
  * emits `open-source` with this value; the host opens it (it may be an