@aleph-alpha/chat-kit 5.10.1 → 5.12.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 (51) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-CKM3MwF_.js → CkHistory.vue_vue_type_script_setup_true_lang-CImSzLYs.js} +123 -75
  2. package/dist/chat-kit.css +13 -13
  3. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
  4. package/dist/components/base/CkCitationLabel/CkCitationLabel.stories.d.ts +39 -0
  5. package/dist/components/base/CkCitationLabel/CkCitationLabel.stories.d.ts.map +1 -0
  6. package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts +9 -0
  7. package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts.map +1 -0
  8. package/dist/components/base/CkCitationLabel/index.d.ts +3 -0
  9. package/dist/components/base/CkCitationLabel/index.d.ts.map +1 -0
  10. package/dist/components/base/CkCitationLabel/types.d.ts +44 -0
  11. package/dist/components/base/CkCitationLabel/types.d.ts.map +1 -0
  12. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +12 -0
  13. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -1
  14. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
  15. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -1
  16. package/dist/components/base/CkFeedbackDialog/types.d.ts +28 -0
  17. package/dist/components/base/CkFeedbackDialog/types.d.ts.map +1 -1
  18. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
  19. package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts.map +1 -1
  20. package/dist/components/base/index.d.ts +2 -0
  21. package/dist/components/base/index.d.ts.map +1 -1
  22. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +12 -0
  23. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  24. package/dist/components/index.d.ts +2 -2
  25. package/dist/components/index.d.ts.map +1 -1
  26. package/dist/components/index.js +28 -27
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +29 -28
  30. package/dist/types.d.ts +33 -4
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +5 -5
  33. package/src/components/base/CkCitation/CkCitation.spec.ts +11 -0
  34. package/src/components/base/CkCitation/CkCitation.vue +9 -36
  35. package/src/components/base/CkCitationLabel/CkCitationLabel.spec.ts +73 -0
  36. package/src/components/base/CkCitationLabel/CkCitationLabel.stories.ts +86 -0
  37. package/src/components/base/CkCitationLabel/CkCitationLabel.vue +69 -0
  38. package/src/components/base/CkCitationLabel/index.ts +2 -0
  39. package/src/components/base/CkCitationLabel/types.ts +43 -0
  40. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +1 -1
  41. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +67 -0
  42. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +57 -0
  43. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +19 -5
  44. package/src/components/base/CkFeedbackDialog/types.ts +29 -0
  45. package/src/components/base/CkUserMessage/CkUserMessage.vue +1 -1
  46. package/src/components/base/index.ts +2 -0
  47. package/src/components/composed/CkConversation/CkConversation.spec.ts +65 -2
  48. package/src/components/composed/CkConversation/CkConversation.stories.ts +136 -0
  49. package/src/components/index.ts +2 -0
  50. package/src/index.ts +2 -0
  51. package/src/types.ts +34 -4
@@ -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,15 @@ 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
+ <CkCitationLabel
113
+ as="button"
114
+ :index="index"
115
+ :selected="selected"
116
+ :aria-label="ariaLabel"
134
117
  @keydown="onTriggerKeydown"
135
118
  @mouseenter="onPointerEnter"
136
119
  @mouseleave="onPointerLeave"
137
- >
138
- {{ index }}
139
- </button>
120
+ />
140
121
  </UiPopoverTrigger>
141
122
  <UiPopoverContent
142
123
  align="start"
@@ -160,11 +141,3 @@ onBeforeUnmount(() => {
160
141
  </UiPopoverContent>
161
142
  </UiPopover>
162
143
  </template>
163
-
164
- <style scoped>
165
- @media (prefers-reduced-motion: reduce) {
166
- .ck-citation {
167
- transition-duration: 0.01ms;
168
- }
169
- }
170
- </style>
@@ -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 (amber) 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-accent-yellow');
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-accent-yellow');
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,69 @@
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 mx-px inline-flex h-5 min-w-5 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',
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
+ isButton ? 'select-none' : '',
54
+ selected
55
+ ? 'bg-background-accent-soft text-black'
56
+ : 'bg-background-accent-yellow text-background-status-warning',
57
+ ]"
58
+ >
59
+ {{ index }}
60
+ </component>
61
+ </template>
62
+
63
+ <style scoped>
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .ck-citation {
66
+ transition-duration: 0.01ms;
67
+ }
68
+ }
69
+ </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
+ }
@@ -261,7 +261,7 @@ describe('CkConversationLayout', () => {
261
261
  });
262
262
 
263
263
  describe('scroll fade indicators', () => {
264
- function getFadeElements(container: HTMLElement) {
264
+ function getFadeElements(container: Element) {
265
265
  return {
266
266
  topFade: container.querySelector('.bg-gradient-to-b') as HTMLElement,
267
267
  bottomFade: container.querySelector('.bg-gradient-to-t') as HTMLElement,
@@ -148,6 +148,37 @@ describe('CkFeedbackDialog', () => {
148
148
  });
149
149
  });
150
150
 
151
+ // With `reasonRequired: false` the bare sentiment is a valid submission, so
152
+ // Submit is enabled from the start.
153
+ it('allows submitting with no input when reasonRequired is false', async () => {
154
+ const user = userEvent.setup();
155
+ const { emitted } = renderDialog({
156
+ sentiment: 'negative',
157
+ reasonRequired: false,
158
+ });
159
+
160
+ const submit = await screen.findByRole('button', { name: SUBMIT });
161
+ expect(submit).not.toBeDisabled();
162
+
163
+ await user.click(submit);
164
+
165
+ const submitEvents = emitted('submit') as [CkFeedbackDialogSubmitPayload][];
166
+ expect(submitEvents).toHaveLength(1);
167
+ expect(submitEvents[0][0]).toEqual({
168
+ sentiment: 'negative',
169
+ reasons: [],
170
+ reasonText: '',
171
+ });
172
+ });
173
+
174
+ // `reasonRequired: false` only relaxes the gate — submitting/disabled still
175
+ // freeze the action.
176
+ it('keeps submit disabled while submitting even when reasonRequired is false', async () => {
177
+ renderDialog({ reasonRequired: false, submitting: true });
178
+
179
+ expect(await screen.findByRole('button', { name: SUBMIT })).toBeDisabled();
180
+ });
181
+
151
182
  // Edge case: reasons + free text combine into one payload, free text trimmed.
152
183
  it('emits both reasons and free text when given', async () => {
153
184
  const user = userEvent.setup();
@@ -210,6 +241,42 @@ describe('CkFeedbackDialog', () => {
210
241
  ).not.toBeInTheDocument();
211
242
  });
212
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-accent-yellow');
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
+
213
280
  // When no response text is given the left preview column is omitted.
214
281
  it('omits the response heading when responseText is empty', async () => {
215
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,8 +100,10 @@ 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' },
106
+ reasonRequired: { control: 'boolean' },
80
107
  submitting: { control: 'boolean' },
81
108
  disabled: { control: 'boolean' },
82
109
  },
@@ -176,6 +203,22 @@ export const Negative: Story = {
176
203
  },
177
204
  };
178
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
+
179
222
  /**
180
223
  * When no response text is provided, the left column collapses and the reasons
181
224
  * + free-text input fill the dialog.
@@ -189,6 +232,20 @@ export const NoResponseColumn: Story = {
189
232
  },
190
233
  };
191
234
 
235
+ /**
236
+ * With `reasonRequired: false` the reasons and free text are both optional —
237
+ * Submit is enabled immediately and submits the bare sentiment.
238
+ */
239
+ export const OptionalInput: Story = {
240
+ args: {
241
+ open: true,
242
+ sentiment: 'negative',
243
+ reasons: NEGATIVE_REASONS,
244
+ responseText: SAMPLE_RESPONSE,
245
+ reasonRequired: false,
246
+ },
247
+ };
248
+
192
249
  /**
193
250
  * Submit shows a busy state while the host persists the payload; both actions
194
251
  * are non-interactive.
@@ -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,8 +31,10 @@ const {
29
31
  open = false,
30
32
  sentiment,
31
33
  responseText = '',
34
+ citations,
32
35
  reasons,
33
36
  maxFreeTextLength = 250,
37
+ reasonRequired = true,
34
38
  submitting = false,
35
39
  disabled = false,
36
40
  titlePositive,
@@ -132,11 +136,14 @@ const showResponseColumn = computed(
132
136
  () => typeof responseText === 'string' && responseText !== '',
133
137
  );
134
138
  const characterCount = computed(() => freeText.value.length);
139
+ const hasInput = computed(
140
+ () => selectedReasons.value.length > 0 || freeText.value.trim() !== '',
141
+ );
142
+ // With `reasonRequired` (the default) the user must give at least one signal —
143
+ // a reason chip or some free text — so we never submit an empty qualitative
144
+ // payload. When it's off, the bare sentiment is a valid submission.
135
145
  const canSubmit = computed(
136
- () =>
137
- !disabled &&
138
- !submitting &&
139
- (selectedReasons.value.length > 0 || freeText.value.trim() !== ''),
146
+ () => !disabled && !submitting && (!reasonRequired || hasInput.value),
140
147
  );
141
148
 
142
149
  function handleOpenChange(next: boolean): void {
@@ -198,7 +205,14 @@ function handleSubmit(): void {
198
205
  <div
199
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"
200
207
  >
201
- <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>
202
216
  </div>
203
217
  </section>
204
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
@@ -80,6 +100,15 @@ export interface CkFeedbackDialogProps {
80
100
  * @default 250
81
101
  */
82
102
  maxFreeTextLength?: number;
103
+ /**
104
+ * Whether the user must provide at least one signal before submitting. When
105
+ * `true` (the default) Submit stays disabled until a reason chip is selected
106
+ * or the free-text field is non-empty, so an empty qualitative payload is
107
+ * never sent. Set to `false` to let the user submit the bare sentiment — the
108
+ * payload then carries an empty `reasons` array and an empty `reasonText`.
109
+ * @default true
110
+ */
111
+ reasonRequired?: boolean;
83
112
  /**
84
113
  * When `true`, the Submit button shows a busy state and clicks are ignored.
85
114
  * Use while the parent is awaiting the backend POST.
@@ -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,