@aleph-alpha/chat-kit 3.1.3 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-LPrW7CSQ.js → CkInput.vue_vue_type_script_setup_true_lang-BAcBFs6I.js} +302 -71
  2. package/dist/adapters/responses-api.d.ts.map +1 -1
  3. package/dist/adapters/responses-api.js +63 -37
  4. package/dist/chat-kit.css +6 -0
  5. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +7 -0
  6. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
  7. package/dist/components/base/CkCitation/CkCitation.stories.d.ts +47 -0
  8. package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -0
  9. package/dist/components/base/CkCitation/CkCitation.vue.d.ts +15 -0
  10. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -0
  11. package/dist/components/base/CkCitation/index.d.ts +3 -0
  12. package/dist/components/base/CkCitation/index.d.ts.map +1 -0
  13. package/dist/components/base/CkCitation/types.d.ts +79 -0
  14. package/dist/components/base/CkCitation/types.d.ts.map +1 -0
  15. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +51 -0
  16. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -0
  17. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +13 -0
  18. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -0
  19. package/dist/components/base/CkCitationCard/index.d.ts +3 -0
  20. package/dist/components/base/CkCitationCard/index.d.ts.map +1 -0
  21. package/dist/components/base/CkCitationCard/types.d.ts +51 -0
  22. package/dist/components/base/CkCitationCard/types.d.ts.map +1 -0
  23. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +1 -1
  24. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
  25. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
  26. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +15 -0
  27. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
  28. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts +16 -2
  29. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
  30. package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
  31. package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
  32. package/dist/components/base/CkMarkdownRenderer/types.d.ts +25 -0
  33. package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
  34. package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts +17 -0
  35. package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts.map +1 -0
  36. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
  37. package/dist/components/base/index.d.ts +5 -1
  38. package/dist/components/base/index.d.ts.map +1 -1
  39. package/dist/components/composed/CkInput/CkInput.vue.d.ts +1 -1
  40. package/dist/components/index.d.ts +2 -2
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +25 -23
  43. package/dist/composables/fileUpload/types.d.ts +3 -0
  44. package/dist/composables/fileUpload/types.d.ts.map +1 -1
  45. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
  46. package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
  47. package/dist/composables/index.js +1 -1
  48. package/dist/handlers/index.d.ts +3 -0
  49. package/dist/handlers/index.d.ts.map +1 -1
  50. package/dist/handlers/index.js +4 -1
  51. package/dist/handlers/useChatKitLabels.d.ts +11 -0
  52. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  53. package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
  54. package/dist/helpers/unified/remarkCitations.d.ts +17 -0
  55. package/dist/helpers/unified/remarkCitations.d.ts.map +1 -0
  56. package/dist/helpers/unified/remarkNormalizeMath.d.ts +24 -0
  57. package/dist/helpers/unified/remarkNormalizeMath.d.ts.map +1 -0
  58. package/dist/{index-DVxCNAs3.js → index-C6njJ8EB.js} +96 -49
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +28 -26
  62. package/dist/{useChatKitLabels-Ctz1dHpx.js → useChatKitLabels-BM2I7-9o.js} +126 -3
  63. package/package.json +6 -3
  64. package/src/adapters/responses-api.spec.ts +156 -0
  65. package/src/adapters/responses-api.ts +125 -58
  66. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +112 -0
  67. package/src/components/base/CkCitation/CkCitation.spec.ts +79 -0
  68. package/src/components/base/CkCitation/CkCitation.stories.ts +136 -0
  69. package/src/components/base/CkCitation/CkCitation.vue +145 -0
  70. package/src/components/base/CkCitation/index.ts +2 -0
  71. package/src/components/base/CkCitation/types.ts +80 -0
  72. package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +77 -0
  73. package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +144 -0
  74. package/src/components/base/CkCitationCard/CkCitationCard.vue +76 -0
  75. package/src/components/base/CkCitationCard/index.ts +2 -0
  76. package/src/components/base/CkCitationCard/types.ts +51 -0
  77. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +124 -0
  78. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +118 -0
  79. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +70 -28
  80. package/src/components/base/CkMarkdownRenderer/index.ts +1 -0
  81. package/src/components/base/CkMarkdownRenderer/types.ts +26 -0
  82. package/src/components/base/CkMarkdownRenderer/useCitationRendering.spec.ts +94 -0
  83. package/src/components/base/CkMarkdownRenderer/useCitationRendering.ts +52 -0
  84. package/src/components/base/index.ts +12 -0
  85. package/src/components/index.ts +8 -0
  86. package/src/composables/fileUpload/types.ts +6 -0
  87. package/src/composables/fileUpload/useFileUpload.spec.ts +2 -61
  88. package/src/composables/fileUpload/useFileUpload.ts +9 -10
  89. package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +241 -10
  90. package/src/composables/fileUpload/useFileUploadWrapper.ts +126 -53
  91. package/src/handlers/index.ts +6 -0
  92. package/src/handlers/useChatKitLabels.ts +13 -0
  93. package/src/handlers/useMarkdownParser.spec.ts +34 -0
  94. package/src/handlers/useMarkdownParser.ts +15 -3
  95. package/src/helpers/unified/remarkCitations.spec.ts +112 -0
  96. package/src/helpers/unified/remarkCitations.ts +55 -0
  97. package/src/helpers/unified/remarkNormalizeMath.spec.ts +84 -0
  98. package/src/helpers/unified/remarkNormalizeMath.ts +134 -0
  99. package/src/index.ts +8 -0
@@ -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
+ }
@@ -0,0 +1,77 @@
1
+ import CkCitationCard from './CkCitationCard.vue';
2
+ import { provideChatKitLabels } from '../../../handlers/useChatKitLabels';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { render, screen } from '@testing-library/vue';
5
+ import { defineComponent, h } from 'vue';
6
+
7
+ const base = {
8
+ fileName: 'q3-report.pdf',
9
+ snippet: 'The cited chunk text.',
10
+ };
11
+
12
+ describe('CkCitationCard', () => {
13
+ it('renders the file name and snippet', () => {
14
+ render(CkCitationCard, { props: { ...base, locationLabel: 'Page 3' } });
15
+
16
+ expect(screen.getByText('q3-report.pdf')).toBeInTheDocument();
17
+ expect(screen.getByText('The cited chunk text.')).toBeInTheDocument();
18
+ expect(screen.getByText('Page 3')).toBeInTheDocument();
19
+ });
20
+
21
+ it('hides the location footer when no locationLabel is given', () => {
22
+ render(CkCitationCard, { props: base });
23
+
24
+ expect(screen.queryByText('Page 3')).not.toBeInTheDocument();
25
+ });
26
+
27
+ it('hides the external-link action when no url is provided', () => {
28
+ render(CkCitationCard, { props: base });
29
+
30
+ expect(
31
+ screen.queryByRole('button', { name: 'Open source' }),
32
+ ).not.toBeInTheDocument();
33
+ });
34
+
35
+ it('shows the external-link action and emits open-source with the url', async () => {
36
+ const user = userEvent.setup();
37
+ const { emitted } = render(CkCitationCard, {
38
+ props: { ...base, url: '/api/v1/.../public-urls' },
39
+ });
40
+
41
+ await user.click(screen.getByRole('button', { name: 'Open source' }));
42
+
43
+ expect(emitted('open-source')).toEqual([['/api/v1/.../public-urls']]);
44
+ });
45
+
46
+ it('uses a custom openSourceLabel for the action', () => {
47
+ render(CkCitationCard, {
48
+ props: { ...base, url: '/u', openSourceLabel: 'Quelle öffnen' },
49
+ });
50
+
51
+ expect(
52
+ screen.getByRole('button', { name: 'Quelle öffnen' }),
53
+ ).toBeInTheDocument();
54
+ });
55
+
56
+ it('wraps a long unbroken snippet so it cannot overflow the fixed-width card', () => {
57
+ const snippet = 'x'.repeat(200);
58
+ render(CkCitationCard, { props: { ...base, snippet } });
59
+
60
+ expect(screen.getByText(snippet).className).toContain('break-words');
61
+ });
62
+
63
+ it('uses the injected citationOpenSource label via provideChatKitLabels', () => {
64
+ const Wrapper = defineComponent({
65
+ setup() {
66
+ provideChatKitLabels(() => ({ citationOpenSource: 'Quelle öffnen' }));
67
+ return () => h(CkCitationCard, { ...base, url: '/u' });
68
+ },
69
+ });
70
+
71
+ render(Wrapper);
72
+
73
+ expect(
74
+ screen.getByRole('button', { name: 'Quelle öffnen' }),
75
+ ).toBeInTheDocument();
76
+ });
77
+ });
@@ -0,0 +1,144 @@
1
+ import CkCitationCard from './CkCitationCard.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const LOREM =
5
+ 'Lorem ipsum dolor sit amet consectetur. Phasellus ipsum nunc pellentesque diam donec id fermentum. Morbi amet lacus cras sagittis sagittis libero. Elit netus sodales in risus diam ut.';
6
+
7
+ /**
8
+ * CkCitationCard is the floating card that describes a single citation source:
9
+ * the file, the cited excerpt, an optional location (e.g. page), and an
10
+ * optional external-link action. It is the hover/focus content of `CkCitation`,
11
+ * but can be used on its own wherever a source preview is needed. The
12
+ * external-link action is exposed as an `open-source` event rather than a plain
13
+ * link, because the source URL is often auth-proxied.
14
+ */
15
+ const meta: Meta<typeof CkCitationCard> = {
16
+ title: 'Base/CkCitationCard',
17
+ component: CkCitationCard,
18
+ tags: ['autodocs'],
19
+ argTypes: {
20
+ fileName: { control: 'text' },
21
+ snippet: { control: 'text' },
22
+ locationLabel: { control: 'text' },
23
+ url: { control: 'text' },
24
+ openSourceLabel: { control: 'text' },
25
+ },
26
+ args: {
27
+ fileName: 'Q2_Business_Plan.pdf',
28
+ snippet: LOREM,
29
+ locationLabel: 'Page 3',
30
+ url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
31
+ },
32
+ decorators: [
33
+ () => ({
34
+ template:
35
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
36
+ }),
37
+ ],
38
+ };
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ const defaultTemplateSource = `<script setup lang="ts">
44
+ import { CkCitationCard } from '@aleph-alpha/chat-kit';
45
+
46
+ function openSource(url: string) {
47
+ // The source URL is often auth-proxied — the host opens it.
48
+ window.open(url, '_blank', 'noopener');
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <CkCitationCard
54
+ file-name="Q2_Business_Plan.pdf"
55
+ snippet="Lorem ipsum dolor sit amet consectetur…"
56
+ location-label="Page 3"
57
+ url="/api/v1/search-stores/ss_abc/files/file_123/public-urls"
58
+ @open-source="openSource"
59
+ />
60
+ </template>`;
61
+
62
+ /**
63
+ * Full card: file-type badge, title, external-link action, excerpt, and a
64
+ * page footer. Matches the Figma `citation card` frame.
65
+ */
66
+ export const Default: Story = {
67
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
68
+ };
69
+
70
+ /**
71
+ * No `url` — the external-link action is hidden. Use for sources that can't
72
+ * be opened (e.g. an internal chunk with no servable file).
73
+ */
74
+ export const WithoutLink: Story = {
75
+ args: { url: undefined },
76
+ };
77
+
78
+ /**
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
+ */
82
+ export const WithoutLocation: Story = {
83
+ args: { locationLabel: undefined },
84
+ };
85
+
86
+ /**
87
+ * Minimal: title + excerpt only (no link, no location). The leanest source a
88
+ * card can describe.
89
+ */
90
+ export const MinimalSnippetOnly: Story = {
91
+ args: { url: undefined, locationLabel: undefined },
92
+ };
93
+
94
+ /**
95
+ * A long file name truncates with an ellipsis (the badge and external-link
96
+ * action stay pinned, the card keeps its width); the full name stays available
97
+ * on hover via the `title` attribute. The `truncate` behaviour holds for any
98
+ * length, including a single unbroken token with no separators.
99
+ */
100
+ export const LongFileName: Story = {
101
+ args: {
102
+ fileName:
103
+ 'Annual-Strategy-And-Operational-Review-And-Lessons-Learned-Digital-Transformation-Phase-1-FY2026-Final-APPROVED-v3.pdf',
104
+ },
105
+ };
106
+
107
+ /**
108
+ * A long excerpt wraps freely — the card grows vertically while keeping its
109
+ * fixed width.
110
+ */
111
+ export const LongSnippet: Story = {
112
+ args: {
113
+ snippet: `${LOREM} ${LOREM}`,
114
+ },
115
+ };
116
+
117
+ /**
118
+ * The file-type badge derives its icon and tone from the file extension:
119
+ * `.pdf`/`.docx` → pink, `.md`/`.html` → soft, `.csv`/`.xlsx` → yellow.
120
+ */
121
+ export const FileTypeVariants: Story = {
122
+ render: (args) => ({
123
+ components: { CkCitationCard },
124
+ setup() {
125
+ const files = [
126
+ 'q3-report.pdf',
127
+ 'README.md',
128
+ 'sales-2026.csv',
129
+ 'notes.txt',
130
+ ];
131
+ return { args, files };
132
+ },
133
+ template: `
134
+ <div style="display: flex; flex-direction: column; gap: 16px;">
135
+ <CkCitationCard
136
+ v-for="f in files"
137
+ :key="f"
138
+ v-bind="args"
139
+ :file-name="f"
140
+ />
141
+ </div>
142
+ `,
143
+ }),
144
+ };