@aleph-alpha/chat-kit 5.11.0 → 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-SL1vOAd9.js → CkHistory.vue_vue_type_script_setup_true_lang-CImSzLYs.js} +118 -74
- 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 +7 -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 +19 -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 +3 -3
- 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/CkFeedbackDialog/CkFeedbackDialog.spec.ts +36 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +42 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +11 -1
- package/src/components/base/CkFeedbackDialog/types.ts +20 -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
|
+
}
|
|
@@ -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-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
|
+
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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', () => {
|