@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
@@ -0,0 +1,73 @@
1
+ import CkCitationLabel from './CkCitationLabel.vue';
2
+ import { render, screen } from '@testing-library/vue';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ describe('CkCitationLabel', () => {
6
+ it('renders the source number as an inert span by default', () => {
7
+ const { container } = render(CkCitationLabel, { props: { index: 3 } });
8
+
9
+ const marker = container.querySelector('.ck-citation')!;
10
+ expect(marker.tagName).toBe('SPAN');
11
+ expect(marker).toHaveTextContent('3');
12
+ });
13
+
14
+ // Without a role of its own the digit is announced glued onto the preceding
15
+ // word ("reviews.[1][2]" → "reviews.12"). `role="img"` makes the chip one
16
+ // labelled object, while its text stays the plain number so it is still
17
+ // copied along with the surrounding prose (hence: no `select-none`).
18
+ it('exposes the static chip as a single labelled object', () => {
19
+ const { container } = render(CkCitationLabel, { props: { index: 3 } });
20
+
21
+ const marker = screen.getByRole('img', { name: 'Source 3' });
22
+ expect(marker).toHaveTextContent('3');
23
+ expect(marker).not.toHaveAttribute('tabindex');
24
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
25
+ expect(container.querySelector('.ck-citation')!.className).not.toContain(
26
+ 'select-none',
27
+ );
28
+ });
29
+
30
+ it('honours a custom ariaLabel in span mode', () => {
31
+ render(CkCitationLabel, { props: { index: 3, ariaLabel: 'Quelle 3' } });
32
+
33
+ expect(screen.getByRole('img', { name: 'Quelle 3' })).toBeInTheDocument();
34
+ });
35
+
36
+ it('renders a button with a derived accessible name when `as="button"`', () => {
37
+ render(CkCitationLabel, { props: { index: 7, as: 'button' } });
38
+
39
+ const marker = screen.getByRole('button', { name: 'Source 7' });
40
+ expect(marker).toHaveAttribute('type', 'button');
41
+ expect(marker).toHaveTextContent('7');
42
+ // A control shouldn't start a text selection when clicked/dragged.
43
+ expect(marker.className).toContain('select-none');
44
+ });
45
+
46
+ it('uses a custom ariaLabel in button mode', () => {
47
+ render(CkCitationLabel, {
48
+ props: { index: 1, as: 'button', ariaLabel: 'Quelle 1' },
49
+ });
50
+
51
+ expect(
52
+ screen.getByRole('button', { name: 'Quelle 1' }),
53
+ ).toBeInTheDocument();
54
+ });
55
+
56
+ it('renders the default (neutral) state by default', () => {
57
+ const { container } = render(CkCitationLabel, { props: { index: 1 } });
58
+
59
+ const marker = container.querySelector('.ck-citation')!;
60
+ expect(marker.className).toContain('bg-background-surface-citation');
61
+ expect(marker.className).not.toContain('bg-background-accent-soft');
62
+ });
63
+
64
+ it('renders the selected (teal) state when `selected`', () => {
65
+ const { container } = render(CkCitationLabel, {
66
+ props: { index: 1, selected: true },
67
+ });
68
+
69
+ const marker = container.querySelector('.ck-citation')!;
70
+ expect(marker.className).toContain('bg-background-accent-soft');
71
+ expect(marker.className).not.toContain('bg-background-surface-citation');
72
+ });
73
+ });
@@ -0,0 +1,86 @@
1
+ import CkCitationLabel from './CkCitationLabel.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ /**
5
+ * CkCitationLabel is the numbered chip on its own — the citation marker
6
+ * without a popover. `CkCitation` composes it as its hover-card trigger
7
+ * (`as="button"`); use it directly, in its default static `span` form,
8
+ * wherever cited text should keep the marker styling but stay inert — the
9
+ * response preview in `CkFeedbackDialog` renders it that way.
10
+ */
11
+ const meta: Meta<typeof CkCitationLabel> = {
12
+ title: 'Base/CkCitationLabel',
13
+ component: CkCitationLabel,
14
+ tags: ['autodocs'],
15
+ argTypes: {
16
+ index: { control: 'number' },
17
+ selected: { control: 'boolean' },
18
+ as: { control: 'inline-radio', options: ['span', 'button'] },
19
+ ariaLabel: { control: 'text' },
20
+ },
21
+ args: {
22
+ index: 1,
23
+ },
24
+ decorators: [
25
+ () => ({
26
+ template:
27
+ '<div style="max-width: 640px; margin: 0 auto; padding: 32px 64px;"><story /></div>',
28
+ }),
29
+ ],
30
+ };
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ /**
36
+ * The default: a static `span` in its amber state. Not focusable, no hover
37
+ * card — it just carries the marker styling.
38
+ */
39
+ export const Default: Story = {};
40
+
41
+ /**
42
+ * The selected (teal) state — set `selected` when the citation's source is the
43
+ * one currently open/highlighted.
44
+ */
45
+ export const Selected: Story = {
46
+ args: { selected: true },
47
+ };
48
+
49
+ /**
50
+ * `as="button"` makes the chip an activatable control and gives it an
51
+ * accessible name (`"Source {index}"` by default, overridable with
52
+ * `ariaLabel`). This is the mode `CkCitation` uses for its popover trigger.
53
+ */
54
+ export const AsButton: Story = {
55
+ args: { as: 'button' },
56
+ };
57
+
58
+ /**
59
+ * How static markers read inside a sentence — the way a cited answer looks in
60
+ * a read-only surface such as the feedback dialog's response preview.
61
+ */
62
+ export const InlineInText: Story = {
63
+ render: (args) => ({
64
+ components: { CkCitationLabel },
65
+ setup() {
66
+ return { args };
67
+ },
68
+ template: `
69
+ <p style="font-size: 14px; line-height: 20px; max-width: 32rem;">
70
+ Departments that received customized training saw a 25% higher adoption
71
+ rate of the new tools compared to those with generic training.<CkCitationLabel
72
+ v-bind="args"
73
+ :index="1"
74
+ /><CkCitationLabel v-bind="args" :index="2" />
75
+ </p>
76
+ `,
77
+ }),
78
+ };
79
+
80
+ /**
81
+ * Multi-digit indices keep the chip legible — it grows with its number while
82
+ * staying aligned to the surrounding text.
83
+ */
84
+ export const MultiDigitIndex: Story = {
85
+ args: { index: 12 },
86
+ };
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import {
4
+ defaultChatKitLabels,
5
+ useChatKitLabels,
6
+ } from '../../../handlers/useChatKitLabels';
7
+ import type { CkCitationLabelProps } from './types';
8
+
9
+ defineOptions({
10
+ name: 'CkCitationLabel',
11
+ });
12
+
13
+ const props = withDefaults(defineProps<CkCitationLabelProps>(), {
14
+ selected: false,
15
+ as: 'span',
16
+ ariaLabel: undefined,
17
+ });
18
+
19
+ const labels = useChatKitLabels();
20
+
21
+ const isButton = computed(() => props.as === 'button');
22
+
23
+ // Resolution order: per-instance prop, then `"{citationSourceLabel} {index}"`
24
+ // from the injected label, then the English default.
25
+ const resolvedAriaLabel = computed(
26
+ () =>
27
+ props.ariaLabel ??
28
+ `${labels.value.citationSourceLabel ?? defaultChatKitLabels.citationSourceLabel!} ${props.index}`,
29
+ );
30
+ </script>
31
+
32
+ <template>
33
+ <!-- Single root element with attrs inheritance left on: `CkCitation` passes
34
+ this component to `UiPopoverTrigger as-child`, which clones the reka
35
+ trigger props (id, aria-expanded, data-state, onClick, …) onto it and
36
+ measures the popover anchor from the rendered element.
37
+
38
+ `role="img"` in span mode makes the chip one labelled object for
39
+ assistive tech ("Source 1") instead of a bare digit that gets read glued
40
+ onto the preceding word — "reviews.[1][2]" would otherwise come out as
41
+ "reviews.12". The rendered text stays the plain number, so a copy of the
42
+ surrounding prose still carries it. -->
43
+ <component
44
+ :is="as"
45
+ :type="isButton ? 'button' : undefined"
46
+ :role="isButton ? undefined : 'img'"
47
+ :aria-label="resolvedAriaLabel"
48
+ :class="[
49
+ 'ck-citation ui-focus-ring mx-px inline-flex h-5 min-w-5 items-center justify-center rounded-sm border border-transparent px-1 align-middle text-sm leading-4 tabular-nums transition-colors',
50
+ // The static chip is part of the prose: it must stay selectable so the
51
+ // number survives a copy of the surrounding text. A control shouldn't
52
+ // start a text selection on click, so button mode keeps `select-none`.
53
+ // Only the control gets the hover layer; a static chip has no hover state.
54
+ isButton ? 'select-none hover-layer hover-layer-default' : '',
55
+ selected
56
+ ? 'bg-background-accent-soft text-content-on-accent-soft'
57
+ : 'bg-background-surface-citation text-content-on-surface-primary',
58
+ ]"
59
+ >
60
+ <span>{{ index }}</span>
61
+ </component>
62
+ </template>
63
+
64
+ <style scoped>
65
+ @media (prefers-reduced-motion: reduce) {
66
+ .ck-citation {
67
+ transition-duration: 0.01ms;
68
+ }
69
+ }
70
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as CkCitationLabel } from './CkCitationLabel.vue';
2
+ export type * from './types';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The bare citation marker — the numbered chip rendered in place of a `[n]`
3
+ * reference, with no popover attached. `CkCitation` composes it as its popover
4
+ * trigger (`as="button"`); use it directly, in its default static `span` form,
5
+ * wherever cited text should keep the marker styling without being interactive
6
+ * (e.g. the response preview in `CkFeedbackDialog`).
7
+ *
8
+ * @category Data Display
9
+ * @useCases static citation marker, read-only cited answer, popover trigger for CkCitation
10
+ * @keywords citation, source, reference, inline, marker, footnote, number, chip
11
+ * @related CkCitation, CkCitationCard, CkMarkdownRenderer, CkFeedbackDialog
12
+ */
13
+ export interface CkCitationLabelProps {
14
+ /**
15
+ * 1-based source number shown in the chip. Matches the `[n]` reference it
16
+ * replaces in the answer text.
17
+ */
18
+ index: number;
19
+ /**
20
+ * Marks the citation as selected/active (e.g. the source it points to is
21
+ * currently open or highlighted). Renders the chip in its selected (teal)
22
+ * state instead of the default amber.
23
+ * @default false
24
+ */
25
+ selected?: boolean;
26
+ /**
27
+ * Element the chip renders as. `'span'` (the default) is inert: not
28
+ * focusable and not activatable. It is exposed to assistive tech as a single
29
+ * labelled object (`role="img"`) so the number is announced as
30
+ * `"Source {index}"` rather than being glued onto the preceding word, while
31
+ * the rendered text stays the plain number so it survives a copy of the
32
+ * surrounding prose. `'button'` makes it an activatable control — used by
33
+ * `CkCitation` as its popover trigger.
34
+ * @default 'span'
35
+ */
36
+ as?: 'span' | 'button';
37
+ /**
38
+ * Accessible label for the chip, in both modes. When omitted, resolves to
39
+ * `"{citationSourceLabel} {index}"` from `useChatKitLabels` (English default
40
+ * `"Source {index}"`).
41
+ */
42
+ ariaLabel?: string;
43
+ }
@@ -241,6 +241,42 @@ describe('CkFeedbackDialog', () => {
241
241
  ).not.toBeInTheDocument();
242
242
  });
243
243
 
244
+ // The preview keeps the marker styling of the rated answer, but the markers
245
+ // are static: no hover card, nothing focusable inside the response column.
246
+ it('renders `[n]` references in the response as static citation markers', async () => {
247
+ renderDialog({
248
+ responseText: 'Adoption rose by 25%.[1]',
249
+ citations: [
250
+ { index: 1, fileName: 'q3-report.pdf', snippet: 'The cited chunk.' },
251
+ ],
252
+ });
253
+
254
+ await screen.findByRole('button', { name: SUBMIT });
255
+
256
+ // The dialog content is portaled to the body, so query from there.
257
+ const marker = document.body.querySelector('.ck-citation')!;
258
+ expect(marker.tagName).toBe('SPAN');
259
+ expect(marker).toHaveTextContent('1');
260
+ expect(marker.className).toContain('bg-background-surface-citation');
261
+ // Announced as its own labelled object, and never as a control.
262
+ expect(screen.getByRole('img', { name: 'Source 1' })).toBeInTheDocument();
263
+ expect(
264
+ screen.queryByRole('button', { name: /Source/ }),
265
+ ).not.toBeInTheDocument();
266
+ });
267
+
268
+ // Fully opt-in: without citations the response renders exactly as before.
269
+ it('leaves `[n]` references literal when no citations are given', async () => {
270
+ renderDialog({ responseText: 'Adoption rose by 25%.[1]' });
271
+
272
+ await screen.findByRole('button', { name: SUBMIT });
273
+
274
+ expect(document.body.querySelector('.ck-citation')).toBeNull();
275
+ expect(
276
+ await screen.findByText('Adoption rose by 25%.[1]'),
277
+ ).toBeInTheDocument();
278
+ });
279
+
244
280
  // When no response text is given the left preview column is omitted.
245
281
  it('omits the response heading when responseText is empty', async () => {
246
282
  renderDialog({ responseText: '' });
@@ -6,6 +6,7 @@ import type {
6
6
  CkFeedbackSentiment,
7
7
  } from './types';
8
8
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
9
+ import type { CkCitationSource } from '../CkCitation';
9
10
  import type { CkFeedbackValue } from '../CkFeedbackButton';
10
11
  import { ref } from 'vue';
11
12
 
@@ -43,6 +44,30 @@ Key Lessons Learned:
43
44
  - **Stakeholder Involvement:** The most critical lesson was the need for early and **continuous stakeholder involvement** from all affected departments. A lack of initial engagement led to significant user resistance and process rework in the initial rollout.
44
45
  - **User Training:** Tailored, hands-on training was found to be far more effective than generic sessions. Departments that received customized training saw a **25% higher adoption rate** of the new tools compared to those with generic training.`;
45
46
 
47
+ const CITED_RESPONSE = `Based on a review of internal documents, the rollout of the new tooling produced two measurable effects:
48
+
49
+ - **Adoption:** Departments that received customized training saw a **25% higher adoption rate** of the new tools compared to those with generic training.[1]
50
+ - **Rework:** A lack of initial stakeholder engagement led to significant process rework during the first wave.[2]
51
+
52
+ Both findings are consistent across the quarterly reviews.[1][2]`;
53
+
54
+ const CITATIONS: CkCitationSource[] = [
55
+ {
56
+ index: 1,
57
+ fileName: 'Q2_Business_Plan.pdf',
58
+ snippet:
59
+ 'Departments that received customized training saw a 25% higher adoption rate of the new tools compared to those with generic training.',
60
+ locationLabel: 'Page 3',
61
+ },
62
+ {
63
+ index: 2,
64
+ fileName: 'Goal-planning-26.pdf',
65
+ snippet:
66
+ 'Early stakeholder involvement remains the single strongest predictor of a rollout that does not require process rework.',
67
+ locationLabel: 'Page 12',
68
+ },
69
+ ];
70
+
46
71
  const LONG_RESPONSE = `Below is the complete corporate travel and expense policy. Several sections expand the original guidelines based on feedback from the 2026 internal audit.
47
72
 
48
73
  1. **General Travel Principles** – travel must serve clear business objectives, be cost-conscious, sustainable, and respect time and well-being. Short trips (e.g., Berlin → Heidelberg) are discouraged unless essential.
@@ -75,6 +100,7 @@ const meta: Meta<typeof CkFeedbackDialog> = {
75
100
  options: ['positive', 'negative'] satisfies CkFeedbackSentiment[],
76
101
  },
77
102
  responseText: { control: 'text' },
103
+ citations: { control: 'object' },
78
104
  reasons: { control: 'object' },
79
105
  maxFreeTextLength: { control: 'number' },
80
106
  reasonRequired: { control: 'boolean' },
@@ -177,6 +203,22 @@ export const Negative: Story = {
177
203
  },
178
204
  };
179
205
 
206
+ /**
207
+ * Pass the same `citations` array the message was rendered with and the `[n]`
208
+ * references in the preview pick up the marker styling of the original answer.
209
+ * They are static here — no hover card, no focus stop — so the preview stays a
210
+ * preview.
211
+ */
212
+ export const WithCitations: Story = {
213
+ args: {
214
+ open: true,
215
+ sentiment: 'positive',
216
+ reasons: POSITIVE_REASONS,
217
+ responseText: CITED_RESPONSE,
218
+ citations: CITATIONS,
219
+ },
220
+ };
221
+
180
222
  /**
181
223
  * When no response text is provided, the left column collapses and the reasons
182
224
  * + free-text input fill the dialog.
@@ -3,7 +3,9 @@ import {
3
3
  defaultChatKitLabels,
4
4
  useChatKitLabels,
5
5
  } from '../../../handlers/useChatKitLabels';
6
+ import { CkCitationLabel } from '../CkCitationLabel';
6
7
  import { CkMarkdownRenderer } from '../CkMarkdownRenderer';
8
+ import type { CkCitationSource } from '../CkCitation';
7
9
  import type {
8
10
  CkFeedbackDialogProps,
9
11
  CkFeedbackDialogSubmitPayload,
@@ -29,6 +31,7 @@ const {
29
31
  open = false,
30
32
  sentiment,
31
33
  responseText = '',
34
+ citations,
32
35
  reasons,
33
36
  maxFreeTextLength = 250,
34
37
  reasonRequired = true,
@@ -202,7 +205,14 @@ function handleSubmit(): void {
202
205
  <div
203
206
  class="ck-feedback-dialog__response min-h-0 flex-1 overflow-y-auto rounded-md border border-input bg-background-input-default px-3 py-2 shadow-xs"
204
207
  >
205
- <CkMarkdownRenderer :content="responseText" />
208
+ <!-- Citations are rendered through the `citation` slot so the
209
+ preview keeps the marker styling of the original answer
210
+ without its hover cards — nothing here is interactive. -->
211
+ <CkMarkdownRenderer :content="responseText" :citations="citations">
212
+ <template #citation="{ source }: { source: CkCitationSource }">
213
+ <CkCitationLabel :index="source.index" />
214
+ </template>
215
+ </CkMarkdownRenderer>
206
216
  </div>
207
217
  </section>
208
218
 
@@ -1,3 +1,5 @@
1
+ import type { CkCitationSource } from '../CkCitation';
2
+
1
3
  /**
2
4
  * Modal dialog for collecting qualitative feedback on an assistant response.
3
5
  * Opened after a binary thumbs-up / thumbs-down click (see `CkFeedbackButton`),
@@ -67,6 +69,24 @@ export interface CkFeedbackDialogProps {
67
69
  * @default undefined
68
70
  */
69
71
  responseText?: string;
72
+ /**
73
+ * Citation sources for inline `[n]` references in `responseText`. Pass the
74
+ * same array the message itself was rendered with, so the preview matches
75
+ * the answer the user is rating.
76
+ *
77
+ * The dialog renders the markers as **static** numbered chips
78
+ * (`CkCitationLabel`): styling only — no hover card, no focus stop, no
79
+ * `open-source`. Only each source's `index` is used; the rest of the shape
80
+ * is accepted so callers can forward their existing array unchanged.
81
+ * Omit it and `[n]` stays literal text.
82
+ *
83
+ * Same caveats as `CkMarkdownRenderer`'s `citations`: any numeric `[n]` in
84
+ * prose with a matching source is converted (wrap incidental ones in a code
85
+ * span to keep them literal), `index` must be unique, and the array
86
+ * reference should be stable/memoized.
87
+ * @default undefined
88
+ */
89
+ citations?: CkCitationSource[];
70
90
  /**
71
91
  * Available reasons to render as multi-select chips. The host fetches the
72
92
  * appropriate per-sentiment list from `GET /feedback/config` and passes it
@@ -63,7 +63,7 @@ watch([() => props.message, () => props.collapsedLines], () => {
63
63
  <div class="inline-flex max-w-[536px] flex-col items-end justify-start gap-1">
64
64
  <div
65
65
  ref="contentRef"
66
- class="ck-user-message__content text-content-on-surface-muted min-w-0 max-w-full self-stretch text-lg leading-7 font-medium break-words whitespace-pre-wrap [overflow-wrap:anywhere]"
66
+ class="ck-user-message__content select-text text-content-on-surface-muted min-w-0 max-w-full self-stretch text-lg leading-7 font-medium break-words whitespace-pre-wrap [overflow-wrap:anywhere]"
67
67
  :class="{
68
68
  'ck-user-message__content--clamped': clampEnabled && !expanded,
69
69
  'ck-user-message__content--faded':
@@ -13,6 +13,8 @@ export type {
13
13
  CkCitationEmits,
14
14
  CkCitationSource,
15
15
  } from './CkCitation';
16
+ export { CkCitationLabel } from './CkCitationLabel';
17
+ export type { CkCitationLabelProps } from './CkCitationLabel';
16
18
  export { CkCitationCard } from './CkCitationCard';
17
19
  export type {
18
20
  CkCitationCardProps,
@@ -1,7 +1,13 @@
1
- import type { BaseMessage, MessageContent } from '../../../types';
1
+ import type {
2
+ BaseMessage,
3
+ MessageContent,
4
+ MessageTextContent,
5
+ } from '../../../types';
2
6
  import CkConversation from './CkConversation.vue';
7
+ import { CkMarkdownRenderer } from '../../base/CkMarkdownRenderer';
3
8
  import { defaultMarkdownClasses } from '../../base/CkMarkdownRenderer/defaultMarkdownClasses';
4
- import { render, screen } from '@testing-library/vue';
9
+ import type { CkCitationSource } from '../../base/CkCitation';
10
+ import { fireEvent, render, screen } from '@testing-library/vue';
5
11
  import {
6
12
  createCommentVNode,
7
13
  defineComponent,
@@ -26,6 +32,11 @@ function text(value: string): MessageContent {
26
32
  };
27
33
  }
28
34
 
35
+ const CITATIONS: CkCitationSource[] = [
36
+ { index: 1, fileName: 'q3-report.pdf', snippet: 'The cited chunk.' },
37
+ { index: 2, fileName: 'goals-26.pdf', snippet: 'Another cited chunk.' },
38
+ ];
39
+
29
40
  const messages: BaseMessage[] = [
30
41
  {
31
42
  id: '1',
@@ -258,6 +269,12 @@ describe('CkConversation', () => {
258
269
  });
259
270
 
260
271
  describe('copy action', () => {
272
+ // jsdom ships no Clipboard API, so the copy button's default write target
273
+ // has to be stubbed in. Removed again so the stub can't leak.
274
+ afterEach(() => {
275
+ Reflect.deleteProperty(globalThis.navigator, 'clipboard');
276
+ });
277
+
261
278
  it('exposes the joined text via the copy action button', () => {
262
279
  const message: BaseMessage = {
263
280
  id: 'mix2',
@@ -300,6 +317,52 @@ describe('CkConversation', () => {
300
317
  container.querySelector('button[aria-label="Copy response"]'),
301
318
  ).toBeNull();
302
319
  });
320
+
321
+ // A host that wires citations renders the `[n]` references as markers, so
322
+ // they are no longer part of the message text in the DOM. The copy action
323
+ // copies the message's own text, which must still carry every reference —
324
+ // pasting a cited answer keeps its citations.
325
+ it('copies the citation references of a cited answer', async () => {
326
+ const writeText = vi.fn(() => Promise.resolve());
327
+ Object.defineProperty(globalThis.navigator, 'clipboard', {
328
+ value: { writeText },
329
+ configurable: true,
330
+ });
331
+
332
+ const message: BaseMessage = {
333
+ id: 'cited',
334
+ role: 'assistant',
335
+ content: [text('Adoption rose by 25%.[1] Rework fell.[2]')],
336
+ createdAt: now,
337
+ status: 'completed',
338
+ };
339
+ const { container } = render(CkConversation, {
340
+ props: { messages: [message] },
341
+ slots: {
342
+ // How a host opts into citations: override the content slot and pass
343
+ // its sources to the renderer.
344
+ 'agent-message-content': (slotProps: { message: BaseMessage }) =>
345
+ h(CkMarkdownRenderer, {
346
+ content: (slotProps.message.content[0] as MessageTextContent)
347
+ .text,
348
+ citations: CITATIONS,
349
+ }),
350
+ },
351
+ });
352
+
353
+ // Both references really did become markers rather than staying text.
354
+ expect(container.querySelectorAll('.ck-citation')).toHaveLength(2);
355
+
356
+ // `fireEvent`, not `userEvent`: the latter's `setup()` installs its own
357
+ // `navigator.clipboard` stub over the one under test here.
358
+ await fireEvent.click(
359
+ container.querySelector('button[aria-label="Copy response"]')!,
360
+ );
361
+
362
+ expect(writeText).toHaveBeenCalledWith(
363
+ 'Adoption rose by 25%.[1] Rework fell.[2]',
364
+ );
365
+ });
303
366
  });
304
367
 
305
368
  describe('slot overrides', () => {