@aleph-alpha/chat-kit 3.1.3 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-LPrW7CSQ.js → CkInput.vue_vue_type_script_setup_true_lang-BAcBFs6I.js} +302 -71
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +63 -37
- package/dist/chat-kit.css +6 -0
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +7 -0
- package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts +47 -0
- package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -0
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts +15 -0
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -0
- package/dist/components/base/CkCitation/index.d.ts +3 -0
- package/dist/components/base/CkCitation/index.d.ts.map +1 -0
- package/dist/components/base/CkCitation/types.d.ts +79 -0
- package/dist/components/base/CkCitation/types.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +51 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +13 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/index.d.ts +3 -0
- package/dist/components/base/CkCitationCard/index.d.ts.map +1 -0
- package/dist/components/base/CkCitationCard/types.d.ts +51 -0
- package/dist/components/base/CkCitationCard/types.d.ts.map +1 -0
- package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +1 -1
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +2 -2
- package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +15 -0
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts +16 -2
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
- package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/types.d.ts +25 -0
- package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts +17 -0
- package/dist/components/base/CkMarkdownRenderer/useCitationRendering.d.ts.map +1 -0
- package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
- package/dist/components/base/index.d.ts +5 -1
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkInput/CkInput.vue.d.ts +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +25 -23
- package/dist/composables/fileUpload/types.d.ts +3 -0
- package/dist/composables/fileUpload/types.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
- package/dist/composables/fileUpload/useFileUploadWrapper.d.ts.map +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/handlers/index.d.ts +3 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +4 -1
- package/dist/handlers/useChatKitLabels.d.ts +11 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
- package/dist/helpers/unified/remarkCitations.d.ts +17 -0
- package/dist/helpers/unified/remarkCitations.d.ts.map +1 -0
- package/dist/helpers/unified/remarkNormalizeMath.d.ts +24 -0
- package/dist/helpers/unified/remarkNormalizeMath.d.ts.map +1 -0
- package/dist/{index-DVxCNAs3.js → index-C6njJ8EB.js} +96 -49
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -26
- package/dist/{useChatKitLabels-Ctz1dHpx.js → useChatKitLabels-BM2I7-9o.js} +126 -3
- package/package.json +6 -3
- package/src/adapters/responses-api.spec.ts +156 -0
- package/src/adapters/responses-api.ts +125 -58
- package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +112 -0
- package/src/components/base/CkCitation/CkCitation.spec.ts +79 -0
- package/src/components/base/CkCitation/CkCitation.stories.ts +136 -0
- package/src/components/base/CkCitation/CkCitation.vue +145 -0
- package/src/components/base/CkCitation/index.ts +2 -0
- package/src/components/base/CkCitation/types.ts +80 -0
- package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +77 -0
- package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +144 -0
- package/src/components/base/CkCitationCard/CkCitationCard.vue +76 -0
- package/src/components/base/CkCitationCard/index.ts +2 -0
- package/src/components/base/CkCitationCard/types.ts +51 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +124 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +118 -0
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +70 -28
- package/src/components/base/CkMarkdownRenderer/index.ts +1 -0
- package/src/components/base/CkMarkdownRenderer/types.ts +26 -0
- package/src/components/base/CkMarkdownRenderer/useCitationRendering.spec.ts +94 -0
- package/src/components/base/CkMarkdownRenderer/useCitationRendering.ts +52 -0
- package/src/components/base/index.ts +12 -0
- package/src/components/index.ts +8 -0
- package/src/composables/fileUpload/types.ts +6 -0
- package/src/composables/fileUpload/useFileUpload.spec.ts +2 -61
- package/src/composables/fileUpload/useFileUpload.ts +9 -10
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +241 -10
- package/src/composables/fileUpload/useFileUploadWrapper.ts +126 -53
- package/src/handlers/index.ts +6 -0
- package/src/handlers/useChatKitLabels.ts +13 -0
- package/src/handlers/useMarkdownParser.spec.ts +34 -0
- package/src/handlers/useMarkdownParser.ts +15 -3
- package/src/helpers/unified/remarkCitations.spec.ts +112 -0
- package/src/helpers/unified/remarkCitations.ts +55 -0
- package/src/helpers/unified/remarkNormalizeMath.spec.ts +84 -0
- package/src/helpers/unified/remarkNormalizeMath.ts +134 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import CkCitation from './CkCitation.vue';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/vue';
|
|
4
|
+
|
|
5
|
+
const base = {
|
|
6
|
+
index: 1,
|
|
7
|
+
fileName: 'q3-report.pdf',
|
|
8
|
+
snippet: 'The cited chunk text.',
|
|
9
|
+
locationLabel: 'Page 3',
|
|
10
|
+
url: '/api/v1/.../public-urls',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('CkCitation', () => {
|
|
14
|
+
it('renders a marker button showing the source number', () => {
|
|
15
|
+
render(CkCitation, { props: base });
|
|
16
|
+
|
|
17
|
+
const marker = screen.getByRole('button', { name: 'Source 1' });
|
|
18
|
+
expect(marker).toHaveTextContent('1');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('derives the accessible name from the index by default', () => {
|
|
22
|
+
render(CkCitation, { props: { ...base, index: 7 } });
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
screen.getByRole('button', { name: 'Source 7' }),
|
|
26
|
+
).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('uses a custom ariaLabel when provided', () => {
|
|
30
|
+
render(CkCitation, { props: { ...base, ariaLabel: 'Quelle 1' } });
|
|
31
|
+
|
|
32
|
+
expect(
|
|
33
|
+
screen.getByRole('button', { name: 'Quelle 1' }),
|
|
34
|
+
).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reveals the source card on activation and re-emits open-source', async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
const { emitted } = render(CkCitation, { props: base });
|
|
40
|
+
|
|
41
|
+
// The marker reveals its card on hover, matching the design.
|
|
42
|
+
await user.hover(screen.getByRole('button', { name: 'Source 1' }));
|
|
43
|
+
|
|
44
|
+
// The card content is rendered (in a popover portal) once open.
|
|
45
|
+
expect(await screen.findByText('q3-report.pdf')).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText('The cited chunk text.')).toBeInTheDocument();
|
|
47
|
+
|
|
48
|
+
await user.click(screen.getByRole('button', { name: 'Open source' }));
|
|
49
|
+
|
|
50
|
+
expect(emitted('open-source')).toEqual([['/api/v1/.../public-urls']]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('opens the card on keyboard activation of the marker', async () => {
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
render(CkCitation, { props: base });
|
|
56
|
+
|
|
57
|
+
const marker = screen.getByRole('button', { name: 'Source 1' });
|
|
58
|
+
marker.focus();
|
|
59
|
+
await user.keyboard('{Enter}');
|
|
60
|
+
|
|
61
|
+
expect(await screen.findByText('q3-report.pdf')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders the default (amber) marker state by default', () => {
|
|
65
|
+
render(CkCitation, { props: base });
|
|
66
|
+
|
|
67
|
+
const marker = screen.getByRole('button', { name: 'Source 1' });
|
|
68
|
+
expect(marker.className).toContain('bg-background-accent-yellow');
|
|
69
|
+
expect(marker.className).not.toContain('bg-background-accent-soft');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders the selected (teal) marker state when `selected`', () => {
|
|
73
|
+
render(CkCitation, { props: { ...base, selected: true } });
|
|
74
|
+
|
|
75
|
+
const marker = screen.getByRole('button', { name: 'Source 1' });
|
|
76
|
+
expect(marker.className).toContain('bg-background-accent-soft');
|
|
77
|
+
expect(marker.className).not.toContain('bg-background-accent-yellow');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import CkCitation from './CkCitation.vue';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
|
+
|
|
4
|
+
const SNIPPET =
|
|
5
|
+
'Departments that received customized training saw a 25% higher adoption rate of the new tools compared to those with generic training.';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CkCitation is the inline marker rendered in place of a `[n]` reference in an
|
|
9
|
+
* assistant answer — a numbered chip that reveals a `CkCitationCard` describing
|
|
10
|
+
* the source on hover or keyboard activation (Enter/Space). Drive it through `CkMarkdownRenderer`'s
|
|
11
|
+
* `citations` prop to render cited answers; use `CkCitationCard` on its own for
|
|
12
|
+
* a standalone source preview.
|
|
13
|
+
*/
|
|
14
|
+
const meta: Meta<typeof CkCitation> = {
|
|
15
|
+
title: 'Base/CkCitation',
|
|
16
|
+
component: CkCitation,
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
argTypes: {
|
|
19
|
+
index: { control: 'number' },
|
|
20
|
+
selected: { control: 'boolean' },
|
|
21
|
+
fileName: { control: 'text' },
|
|
22
|
+
snippet: { control: 'text' },
|
|
23
|
+
locationLabel: { control: 'text' },
|
|
24
|
+
url: { control: 'text' },
|
|
25
|
+
ariaLabel: { control: 'text' },
|
|
26
|
+
openSourceLabel: { control: 'text' },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
index: 1,
|
|
30
|
+
fileName: 'Q2_Business_Plan.pdf',
|
|
31
|
+
snippet: SNIPPET,
|
|
32
|
+
locationLabel: 'Page 3',
|
|
33
|
+
url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
|
|
34
|
+
},
|
|
35
|
+
decorators: [
|
|
36
|
+
() => ({
|
|
37
|
+
template:
|
|
38
|
+
'<div style="max-width: 640px; margin: 0 auto; padding: 96px 64px;"><story /></div>',
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A single marker in its default (amber) state. Hover the chip (or focus it and
|
|
48
|
+
* press Enter) to reveal the source card.
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The selected (teal) state — set `selected` when the citation's source is the
|
|
54
|
+
* one currently open/highlighted, so the active marker stands out.
|
|
55
|
+
*/
|
|
56
|
+
export const Selected: Story = {
|
|
57
|
+
args: { selected: true },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How markers read inside a sentence — two sources cited next to each other,
|
|
62
|
+
* the way they appear in an assistant answer.
|
|
63
|
+
*/
|
|
64
|
+
export const InlineInText: Story = {
|
|
65
|
+
render: (args) => ({
|
|
66
|
+
components: { CkCitation },
|
|
67
|
+
setup() {
|
|
68
|
+
return { args };
|
|
69
|
+
},
|
|
70
|
+
template: `
|
|
71
|
+
<p style="font-size: 14px; line-height: 20px; max-width: 32rem;">
|
|
72
|
+
Departments that received customized training saw a 25% higher adoption
|
|
73
|
+
rate of the new tools compared to those with generic training.<CkCitation
|
|
74
|
+
v-bind="args"
|
|
75
|
+
:index="1"
|
|
76
|
+
/><CkCitation
|
|
77
|
+
v-bind="args"
|
|
78
|
+
:index="2"
|
|
79
|
+
file-name="Goal-planning-26.pdf"
|
|
80
|
+
location-label="Page 12"
|
|
81
|
+
/>
|
|
82
|
+
</p>
|
|
83
|
+
`,
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Multi-digit indices keep the chip legible — the marker grows with its
|
|
89
|
+
* number while staying aligned to the surrounding text.
|
|
90
|
+
*/
|
|
91
|
+
export const MultiDigitIndex: Story = {
|
|
92
|
+
args: { index: 12 },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* No `url` — the card still appears on hover but without the external-link
|
|
97
|
+
* action (e.g. an internal chunk with no servable file).
|
|
98
|
+
*/
|
|
99
|
+
export const WithoutLink: Story = {
|
|
100
|
+
args: { url: undefined },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Non-paginated source — the card omits the page footer.
|
|
105
|
+
*/
|
|
106
|
+
export const WithoutLocation: Story = {
|
|
107
|
+
args: { locationLabel: undefined },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A row of markers with different file types — the card's badge tone follows
|
|
112
|
+
* each source's extension.
|
|
113
|
+
*/
|
|
114
|
+
export const SourceTypes: Story = {
|
|
115
|
+
render: () => ({
|
|
116
|
+
components: { CkCitation },
|
|
117
|
+
setup() {
|
|
118
|
+
const sources = [
|
|
119
|
+
{ index: 1, fileName: 'q3-report.pdf', locationLabel: 'Page 3' },
|
|
120
|
+
{ index: 2, fileName: 'README.md' },
|
|
121
|
+
{ index: 3, fileName: 'sales-2026.csv', locationLabel: 'Row 42' },
|
|
122
|
+
];
|
|
123
|
+
return { sources, snippet: SNIPPET };
|
|
124
|
+
},
|
|
125
|
+
template: `
|
|
126
|
+
<p style="font-size: 14px; line-height: 20px;">
|
|
127
|
+
Multiple sources back this claim.<CkCitation
|
|
128
|
+
v-for="s in sources"
|
|
129
|
+
:key="s.index"
|
|
130
|
+
v-bind="s"
|
|
131
|
+
:snippet="snippet"
|
|
132
|
+
/>
|
|
133
|
+
</p>
|
|
134
|
+
`,
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, ref } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
UiPopover,
|
|
5
|
+
UiPopoverTrigger,
|
|
6
|
+
UiPopoverContent,
|
|
7
|
+
} from '@aleph-alpha/ui-library';
|
|
8
|
+
import {
|
|
9
|
+
defaultChatKitLabels,
|
|
10
|
+
useChatKitLabels,
|
|
11
|
+
} from '../../../handlers/useChatKitLabels';
|
|
12
|
+
import { CkCitationCard } from '../CkCitationCard';
|
|
13
|
+
import type { CkCitationProps } from './types';
|
|
14
|
+
|
|
15
|
+
defineOptions({
|
|
16
|
+
name: 'CkCitation',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<CkCitationProps>(), {
|
|
20
|
+
selected: false,
|
|
21
|
+
locationLabel: undefined,
|
|
22
|
+
url: undefined,
|
|
23
|
+
ariaLabel: undefined,
|
|
24
|
+
openSourceLabel: undefined,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{
|
|
28
|
+
'open-source': [url: string];
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
const labels = useChatKitLabels();
|
|
32
|
+
|
|
33
|
+
// Resolution order: per-instance prop, then `"{citationSourceLabel} {index}"`
|
|
34
|
+
// from the injected label, then the English default.
|
|
35
|
+
const triggerLabel = computed(
|
|
36
|
+
() =>
|
|
37
|
+
props.ariaLabel ??
|
|
38
|
+
`${labels.value.citationSourceLabel ?? defaultChatKitLabels.citationSourceLabel!} ${props.index}`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Close delay so the pointer can cross the gap from the marker into the card
|
|
42
|
+
// without it disappearing on the way.
|
|
43
|
+
const CLOSE_DELAY_MS = 80;
|
|
44
|
+
|
|
45
|
+
const open = ref(false);
|
|
46
|
+
// Tracks whether the current open was driven by the pointer (hover) rather than
|
|
47
|
+
// the keyboard, so we can suppress the popover's focus management for hover only.
|
|
48
|
+
const isPointerInteraction = ref(false);
|
|
49
|
+
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
|
|
51
|
+
function show(): void {
|
|
52
|
+
if (closeTimer) {
|
|
53
|
+
clearTimeout(closeTimer);
|
|
54
|
+
closeTimer = undefined;
|
|
55
|
+
}
|
|
56
|
+
open.value = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scheduleHide(): void {
|
|
60
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
61
|
+
closeTimer = setTimeout(() => {
|
|
62
|
+
open.value = false;
|
|
63
|
+
closeTimer = undefined;
|
|
64
|
+
}, CLOSE_DELAY_MS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onPointerEnter(): void {
|
|
68
|
+
isPointerInteraction.value = true;
|
|
69
|
+
show();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onPointerLeave(): void {
|
|
73
|
+
isPointerInteraction.value = false;
|
|
74
|
+
scheduleHide();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Suppress auto-focus for hover opens only (so hovering never steals focus or
|
|
78
|
+
// scrolls). Keyboard opens keep it, moving focus into the card so the source
|
|
79
|
+
// action is reachable.
|
|
80
|
+
function onOpenAutoFocus(event: Event): void {
|
|
81
|
+
if (isPointerInteraction.value) event.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Close once focus leaves the card entirely (keyboard users tabbing out). Moving
|
|
85
|
+
// focus between elements inside the card keeps it open.
|
|
86
|
+
function onContentFocusOut(event: FocusEvent): void {
|
|
87
|
+
const nextTarget = event.relatedTarget as Node | null;
|
|
88
|
+
const content = event.currentTarget as HTMLElement;
|
|
89
|
+
if (!nextTarget || !content.contains(nextTarget)) {
|
|
90
|
+
scheduleHide();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onBeforeUnmount(() => {
|
|
95
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
96
|
+
});
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<UiPopover v-model:open="open">
|
|
101
|
+
<UiPopoverTrigger as-child>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
:aria-label="triggerLabel"
|
|
105
|
+
:class="[
|
|
106
|
+
'ck-citation mx-px inline-flex h-5 min-w-5 select-none items-center justify-center rounded-sm px-1 align-middle text-sm leading-4 tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
107
|
+
selected
|
|
108
|
+
? 'bg-background-accent-soft text-black'
|
|
109
|
+
: 'bg-background-accent-yellow text-background-status-warning',
|
|
110
|
+
]"
|
|
111
|
+
@mouseenter="onPointerEnter"
|
|
112
|
+
@mouseleave="onPointerLeave"
|
|
113
|
+
>
|
|
114
|
+
{{ index }}
|
|
115
|
+
</button>
|
|
116
|
+
</UiPopoverTrigger>
|
|
117
|
+
<UiPopoverContent
|
|
118
|
+
align="start"
|
|
119
|
+
:side-offset="8"
|
|
120
|
+
class="w-auto border-0 bg-transparent p-0 shadow-none"
|
|
121
|
+
@open-auto-focus="onOpenAutoFocus"
|
|
122
|
+
@mouseenter="onPointerEnter"
|
|
123
|
+
@mouseleave="onPointerLeave"
|
|
124
|
+
@focusin="show"
|
|
125
|
+
@focusout="onContentFocusOut"
|
|
126
|
+
>
|
|
127
|
+
<CkCitationCard
|
|
128
|
+
:file-name="fileName"
|
|
129
|
+
:snippet="snippet"
|
|
130
|
+
:location-label="locationLabel"
|
|
131
|
+
:url="url"
|
|
132
|
+
:open-source-label="openSourceLabel"
|
|
133
|
+
@open-source="(u: string) => emit('open-source', u)"
|
|
134
|
+
/>
|
|
135
|
+
</UiPopoverContent>
|
|
136
|
+
</UiPopover>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<style scoped>
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.ck-citation {
|
|
142
|
+
transition-duration: 0.01ms;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single citation source: the data needed to render a marker and its card.
|
|
3
|
+
* This is the per-source shape the host maps each backend citation into (the
|
|
4
|
+
* `index` matches the `[n]` reference in the answer text) and the element type
|
|
5
|
+
* of `CkMarkdownRenderer`'s `citations` prop.
|
|
6
|
+
*
|
|
7
|
+
* @category Data Display
|
|
8
|
+
* @useCases describe a cited source, build the citations map for an assistant reply
|
|
9
|
+
* @keywords citation, source, reference, file, page, snippet
|
|
10
|
+
* @related CkCitation, CkCitationCard, CkMarkdownRenderer
|
|
11
|
+
*/
|
|
12
|
+
export interface CkCitationSource {
|
|
13
|
+
/**
|
|
14
|
+
* 1-based source number rendered in the marker. Matches the `[n]`
|
|
15
|
+
* reference it replaces in the answer text.
|
|
16
|
+
*/
|
|
17
|
+
index: number;
|
|
18
|
+
/**
|
|
19
|
+
* Source file name. Title of the card and basis for the file-type badge
|
|
20
|
+
* (pass the original name with its extension).
|
|
21
|
+
*/
|
|
22
|
+
fileName: string;
|
|
23
|
+
/** The cited chunk text, shown in the card body. */
|
|
24
|
+
snippet: string;
|
|
25
|
+
/**
|
|
26
|
+
* Location label for the card footer, e.g. `"Page 3"`. Omit for
|
|
27
|
+
* non-paginated sources.
|
|
28
|
+
*/
|
|
29
|
+
locationLabel?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Source URL for the card's external-link action. Activating it emits
|
|
32
|
+
* `open-source`. Omit to hide the action.
|
|
33
|
+
*/
|
|
34
|
+
url?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An inline citation marker — the numbered chip rendered in place of a `[n]`
|
|
39
|
+
* reference inside an assistant answer. On hover, or when activated by
|
|
40
|
+
* keyboard (Enter/Space) or click, it reveals a `CkCitationCard` describing
|
|
41
|
+
* the source.
|
|
42
|
+
*
|
|
43
|
+
* The marker shows the bare source number (no brackets). Activating the
|
|
44
|
+
* card's external-link action re-emits `open-source` so the host can open
|
|
45
|
+
* the (possibly auth-proxied) source URL.
|
|
46
|
+
*
|
|
47
|
+
* @category Data Display
|
|
48
|
+
* @useCases inline citation in assistant reply, numbered source reference, hover to preview the cited file/page
|
|
49
|
+
* @keywords citation, source, reference, inline, marker, footnote, number, hover, popover
|
|
50
|
+
* @related CkCitationCard, CkMarkdownRenderer, CkAgentMessage
|
|
51
|
+
*/
|
|
52
|
+
export interface CkCitationProps extends CkCitationSource {
|
|
53
|
+
/**
|
|
54
|
+
* Marks the citation as selected/active (e.g. the source it points to is
|
|
55
|
+
* currently open or highlighted). Renders the marker in its selected (teal)
|
|
56
|
+
* state instead of the default amber.
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
selected?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Accessible label for the marker trigger. When omitted, resolves to
|
|
62
|
+
* `"{citationSourceLabel} {index}"` from `useChatKitLabels` (English default
|
|
63
|
+
* `"Source {index}"`). Set this to override per instance.
|
|
64
|
+
*/
|
|
65
|
+
ariaLabel?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Accessible label for the card's external-link action. When omitted,
|
|
68
|
+
* resolves from `useChatKitLabels().citationOpenSource` (English default
|
|
69
|
+
* `"Open source"`).
|
|
70
|
+
*/
|
|
71
|
+
openSourceLabel?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CkCitationEmits {
|
|
75
|
+
/**
|
|
76
|
+
* Re-emitted from the card when the user opens the source. The payload is
|
|
77
|
+
* `url`.
|
|
78
|
+
*/
|
|
79
|
+
(e: 'open-source', url: string): void;
|
|
80
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import CkCitationCard from './CkCitationCard.vue';
|
|
2
|
+
import { provideChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { render, screen } from '@testing-library/vue';
|
|
5
|
+
import { defineComponent, h } from 'vue';
|
|
6
|
+
|
|
7
|
+
const base = {
|
|
8
|
+
fileName: 'q3-report.pdf',
|
|
9
|
+
snippet: 'The cited chunk text.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('CkCitationCard', () => {
|
|
13
|
+
it('renders the file name and snippet', () => {
|
|
14
|
+
render(CkCitationCard, { props: { ...base, locationLabel: 'Page 3' } });
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText('q3-report.pdf')).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText('The cited chunk text.')).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText('Page 3')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('hides the location footer when no locationLabel is given', () => {
|
|
22
|
+
render(CkCitationCard, { props: base });
|
|
23
|
+
|
|
24
|
+
expect(screen.queryByText('Page 3')).not.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('hides the external-link action when no url is provided', () => {
|
|
28
|
+
render(CkCitationCard, { props: base });
|
|
29
|
+
|
|
30
|
+
expect(
|
|
31
|
+
screen.queryByRole('button', { name: 'Open source' }),
|
|
32
|
+
).not.toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows the external-link action and emits open-source with the url', async () => {
|
|
36
|
+
const user = userEvent.setup();
|
|
37
|
+
const { emitted } = render(CkCitationCard, {
|
|
38
|
+
props: { ...base, url: '/api/v1/.../public-urls' },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await user.click(screen.getByRole('button', { name: 'Open source' }));
|
|
42
|
+
|
|
43
|
+
expect(emitted('open-source')).toEqual([['/api/v1/.../public-urls']]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('uses a custom openSourceLabel for the action', () => {
|
|
47
|
+
render(CkCitationCard, {
|
|
48
|
+
props: { ...base, url: '/u', openSourceLabel: 'Quelle öffnen' },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
screen.getByRole('button', { name: 'Quelle öffnen' }),
|
|
53
|
+
).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('wraps a long unbroken snippet so it cannot overflow the fixed-width card', () => {
|
|
57
|
+
const snippet = 'x'.repeat(200);
|
|
58
|
+
render(CkCitationCard, { props: { ...base, snippet } });
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText(snippet).className).toContain('break-words');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('uses the injected citationOpenSource label via provideChatKitLabels', () => {
|
|
64
|
+
const Wrapper = defineComponent({
|
|
65
|
+
setup() {
|
|
66
|
+
provideChatKitLabels(() => ({ citationOpenSource: 'Quelle öffnen' }));
|
|
67
|
+
return () => h(CkCitationCard, { ...base, url: '/u' });
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
render(Wrapper);
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByRole('button', { name: 'Quelle öffnen' }),
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import CkCitationCard from './CkCitationCard.vue';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
|
+
|
|
4
|
+
const LOREM =
|
|
5
|
+
'Lorem ipsum dolor sit amet consectetur. Phasellus ipsum nunc pellentesque diam donec id fermentum. Morbi amet lacus cras sagittis sagittis libero. Elit netus sodales in risus diam ut.';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CkCitationCard is the floating card that describes a single citation source:
|
|
9
|
+
* the file, the cited excerpt, an optional location (e.g. page), and an
|
|
10
|
+
* optional external-link action. It is the hover/focus content of `CkCitation`,
|
|
11
|
+
* but can be used on its own wherever a source preview is needed. The
|
|
12
|
+
* external-link action is exposed as an `open-source` event rather than a plain
|
|
13
|
+
* link, because the source URL is often auth-proxied.
|
|
14
|
+
*/
|
|
15
|
+
const meta: Meta<typeof CkCitationCard> = {
|
|
16
|
+
title: 'Base/CkCitationCard',
|
|
17
|
+
component: CkCitationCard,
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
argTypes: {
|
|
20
|
+
fileName: { control: 'text' },
|
|
21
|
+
snippet: { control: 'text' },
|
|
22
|
+
locationLabel: { control: 'text' },
|
|
23
|
+
url: { control: 'text' },
|
|
24
|
+
openSourceLabel: { control: 'text' },
|
|
25
|
+
},
|
|
26
|
+
args: {
|
|
27
|
+
fileName: 'Q2_Business_Plan.pdf',
|
|
28
|
+
snippet: LOREM,
|
|
29
|
+
locationLabel: 'Page 3',
|
|
30
|
+
url: '/api/v1/search-stores/ss_abc/files/file_123/public-urls',
|
|
31
|
+
},
|
|
32
|
+
decorators: [
|
|
33
|
+
() => ({
|
|
34
|
+
template:
|
|
35
|
+
'<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof meta>;
|
|
42
|
+
|
|
43
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
44
|
+
import { CkCitationCard } from '@aleph-alpha/chat-kit';
|
|
45
|
+
|
|
46
|
+
function openSource(url: string) {
|
|
47
|
+
// The source URL is often auth-proxied — the host opens it.
|
|
48
|
+
window.open(url, '_blank', 'noopener');
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<CkCitationCard
|
|
54
|
+
file-name="Q2_Business_Plan.pdf"
|
|
55
|
+
snippet="Lorem ipsum dolor sit amet consectetur…"
|
|
56
|
+
location-label="Page 3"
|
|
57
|
+
url="/api/v1/search-stores/ss_abc/files/file_123/public-urls"
|
|
58
|
+
@open-source="openSource"
|
|
59
|
+
/>
|
|
60
|
+
</template>`;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Full card: file-type badge, title, external-link action, excerpt, and a
|
|
64
|
+
* page footer. Matches the Figma `citation card` frame.
|
|
65
|
+
*/
|
|
66
|
+
export const Default: Story = {
|
|
67
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* No `url` — the external-link action is hidden. Use for sources that can't
|
|
72
|
+
* be opened (e.g. an internal chunk with no servable file).
|
|
73
|
+
*/
|
|
74
|
+
export const WithoutLink: Story = {
|
|
75
|
+
args: { url: undefined },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* No `locationLabel` — the footer is hidden. Use for non-paginated sources
|
|
80
|
+
* such as a plain web page or a whole-document citation.
|
|
81
|
+
*/
|
|
82
|
+
export const WithoutLocation: Story = {
|
|
83
|
+
args: { locationLabel: undefined },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Minimal: title + excerpt only (no link, no location). The leanest source a
|
|
88
|
+
* card can describe.
|
|
89
|
+
*/
|
|
90
|
+
export const MinimalSnippetOnly: Story = {
|
|
91
|
+
args: { url: undefined, locationLabel: undefined },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A long file name truncates with an ellipsis (the badge and external-link
|
|
96
|
+
* action stay pinned, the card keeps its width); the full name stays available
|
|
97
|
+
* on hover via the `title` attribute. The `truncate` behaviour holds for any
|
|
98
|
+
* length, including a single unbroken token with no separators.
|
|
99
|
+
*/
|
|
100
|
+
export const LongFileName: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
fileName:
|
|
103
|
+
'Annual-Strategy-And-Operational-Review-And-Lessons-Learned-Digital-Transformation-Phase-1-FY2026-Final-APPROVED-v3.pdf',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A long excerpt wraps freely — the card grows vertically while keeping its
|
|
109
|
+
* fixed width.
|
|
110
|
+
*/
|
|
111
|
+
export const LongSnippet: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
snippet: `${LOREM} ${LOREM}`,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The file-type badge derives its icon and tone from the file extension:
|
|
119
|
+
* `.pdf`/`.docx` → pink, `.md`/`.html` → soft, `.csv`/`.xlsx` → yellow.
|
|
120
|
+
*/
|
|
121
|
+
export const FileTypeVariants: Story = {
|
|
122
|
+
render: (args) => ({
|
|
123
|
+
components: { CkCitationCard },
|
|
124
|
+
setup() {
|
|
125
|
+
const files = [
|
|
126
|
+
'q3-report.pdf',
|
|
127
|
+
'README.md',
|
|
128
|
+
'sales-2026.csv',
|
|
129
|
+
'notes.txt',
|
|
130
|
+
];
|
|
131
|
+
return { args, files };
|
|
132
|
+
},
|
|
133
|
+
template: `
|
|
134
|
+
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
135
|
+
<CkCitationCard
|
|
136
|
+
v-for="f in files"
|
|
137
|
+
:key="f"
|
|
138
|
+
v-bind="args"
|
|
139
|
+
:file-name="f"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
`,
|
|
143
|
+
}),
|
|
144
|
+
};
|