@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.
- package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-CKM3MwF_.js → CkHistory.vue_vue_type_script_setup_true_lang-CImSzLYs.js} +123 -75
- package/dist/chat-kit.css +13 -13
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
- package/dist/components/base/CkCitationLabel/CkCitationLabel.stories.d.ts +39 -0
- package/dist/components/base/CkCitationLabel/CkCitationLabel.stories.d.ts.map +1 -0
- package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts +9 -0
- package/dist/components/base/CkCitationLabel/CkCitationLabel.vue.d.ts.map +1 -0
- package/dist/components/base/CkCitationLabel/index.d.ts +3 -0
- package/dist/components/base/CkCitationLabel/index.d.ts.map +1 -0
- package/dist/components/base/CkCitationLabel/types.d.ts +44 -0
- package/dist/components/base/CkCitationLabel/types.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +12 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -1
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -1
- package/dist/components/base/CkFeedbackDialog/types.d.ts +28 -0
- package/dist/components/base/CkFeedbackDialog/types.d.ts.map +1 -1
- package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
- package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts.map +1 -1
- package/dist/components/base/index.d.ts +2 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +12 -0
- package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +28 -27
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -28
- package/dist/types.d.ts +33 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/components/base/CkCitation/CkCitation.spec.ts +11 -0
- package/src/components/base/CkCitation/CkCitation.vue +9 -36
- package/src/components/base/CkCitationLabel/CkCitationLabel.spec.ts +73 -0
- package/src/components/base/CkCitationLabel/CkCitationLabel.stories.ts +86 -0
- package/src/components/base/CkCitationLabel/CkCitationLabel.vue +69 -0
- package/src/components/base/CkCitationLabel/index.ts +2 -0
- package/src/components/base/CkCitationLabel/types.ts +43 -0
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +1 -1
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +67 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +57 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +19 -5
- package/src/components/base/CkFeedbackDialog/types.ts +29 -0
- package/src/components/base/CkUserMessage/CkUserMessage.vue +1 -1
- package/src/components/base/index.ts +2 -0
- package/src/components/composed/CkConversation/CkConversation.spec.ts +65 -2
- package/src/components/composed/CkConversation/CkConversation.stories.ts +136 -0
- package/src/components/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/types.ts +34 -4
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
<
|
|
126
|
-
|
|
127
|
-
:
|
|
128
|
-
:
|
|
129
|
-
|
|
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,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:
|
|
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
|
-
|
|
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,
|