@aleph-alpha/chat-kit 1.2.0 → 1.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.
Files changed (36) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-CRymGkQs.js → CkInput.vue_vue_type_script_setup_true_lang-BoUxOQbH.js} +154 -39
  2. package/dist/chat-kit.css +18 -0
  3. package/dist/components/base/CkFeedbackButton/CkFeedbackButton.stories.d.ts +38 -0
  4. package/dist/components/base/CkFeedbackButton/CkFeedbackButton.stories.d.ts.map +1 -0
  5. package/dist/components/base/CkFeedbackButton/CkFeedbackButton.vue.d.ts +16 -0
  6. package/dist/components/base/CkFeedbackButton/CkFeedbackButton.vue.d.ts.map +1 -0
  7. package/dist/components/base/CkFeedbackButton/index.d.ts +3 -0
  8. package/dist/components/base/CkFeedbackButton/index.d.ts.map +1 -0
  9. package/dist/components/base/CkFeedbackButton/types.d.ts +60 -0
  10. package/dist/components/base/CkFeedbackButton/types.d.ts.map +1 -0
  11. package/dist/components/base/index.d.ts +2 -0
  12. package/dist/components/base/index.d.ts.map +1 -1
  13. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +4 -3
  14. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  15. package/dist/components/index.d.ts +2 -2
  16. package/dist/components/index.d.ts.map +1 -1
  17. package/dist/components/index.js +15 -14
  18. package/dist/handlers/index.js +1 -1
  19. package/dist/handlers/useChatKitLabels.d.ts +18 -0
  20. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +16 -15
  24. package/dist/{useChatKitLabels-BmWmMyqX.js → useChatKitLabels-ftC52m7f.js} +3 -0
  25. package/package.json +2 -2
  26. package/src/components/base/CkFeedbackButton/CkFeedbackButton.spec.ts +174 -0
  27. package/src/components/base/CkFeedbackButton/CkFeedbackButton.stories.ts +179 -0
  28. package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +157 -0
  29. package/src/components/base/CkFeedbackButton/index.ts +2 -0
  30. package/src/components/base/CkFeedbackButton/types.ts +61 -0
  31. package/src/components/base/index.ts +5 -0
  32. package/src/components/composed/CkConversation/CkConversation.stories.ts +46 -17
  33. package/src/components/index.ts +3 -0
  34. package/src/handlers/useChatKitLabels.spec.ts +70 -50
  35. package/src/handlers/useChatKitLabels.ts +21 -0
  36. package/src/index.ts +3 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACL,OAAO,EACP,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,aAAa,EACb,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,yBAAyB,EACzB,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,EACvB,UAAU,EACV,UAAU,EACV,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,yBAAyB,EACzB,mBAAmB,EACnB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACL,OAAO,EACP,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,aAAa,EACb,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,yBAAyB,EACzB,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,OAAO,EACP,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,EACvB,UAAU,EACV,UAAU,EACV,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,yBAAyB,EACzB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { C } from "./ChatService-LogwJNxh.js";
2
2
  import { u } from "./useMessageHandler-BmBdu2EW.js";
3
- import { C as C2, d, p, u as u2, a } from "./useChatKitLabels-BmWmMyqX.js";
3
+ import { C as C2, d, p, u as u2, a } from "./useChatKitLabels-ftC52m7f.js";
4
4
  import { u as u3, a as a2, b } from "./useConversationSessions-ulFhtpQr.js";
5
- import { _, a as a3, b as b2, C as C3, c, d as d2, e, f, g, h, i, j, k, l, m, n, o } from "./CkInput.vue_vue_type_script_setup_true_lang-CRymGkQs.js";
5
+ import { _, a as a3, b as b2, C as C3, c, d as d2, e, f, g, h, i, j, k, l, m, n, o, p as p2 } from "./CkInput.vue_vue_type_script_setup_true_lang-BoUxOQbH.js";
6
6
  export {
7
7
  C2 as CHAT_KIT_LABELS,
8
8
  C as ChatService,
@@ -10,19 +10,20 @@ export {
10
10
  a3 as CkConversation,
11
11
  b2 as CkConversationLayout,
12
12
  C3 as CkCopyMessageActionButton,
13
- c as CkHistory,
14
- d2 as CkHistoryItem,
15
- e as CkHistoryWrapper,
16
- f as CkInput,
17
- g as CkMarkdownRenderer,
18
- h as CkMessageActionButton,
19
- i as CkPromptActionButton,
20
- j as CkPromptInput,
21
- k as CkPromptInputDisclaimer,
22
- l as CkPromptStopButton,
23
- m as CkSendIcon,
24
- n as CkStopIcon,
25
- o as CkUserMessage,
13
+ c as CkFeedbackButton,
14
+ d2 as CkHistory,
15
+ e as CkHistoryItem,
16
+ f as CkHistoryWrapper,
17
+ g as CkInput,
18
+ h as CkMarkdownRenderer,
19
+ i as CkMessageActionButton,
20
+ j as CkPromptActionButton,
21
+ k as CkPromptInput,
22
+ l as CkPromptInputDisclaimer,
23
+ m as CkPromptStopButton,
24
+ n as CkSendIcon,
25
+ o as CkStopIcon,
26
+ p2 as CkUserMessage,
26
27
  d as defaultChatKitLabels,
27
28
  p as provideChatKitLabels,
28
29
  u3 as useChat,
@@ -12705,6 +12705,9 @@ const defaultChatKitLabels = {
12705
12705
  inputPlaceholder: "Type a message...",
12706
12706
  copyMessageAction: "Copy response",
12707
12707
  copyMessageActionCopied: "Copied!",
12708
+ feedbackThumbsUp: "Good response",
12709
+ feedbackThumbsDown: "Bad response",
12710
+ feedbackGroup: "Rate this response",
12708
12711
  historyItemUntitled: "Untitled conversation",
12709
12712
  historyItemActionAriaLabel: "Conversation options",
12710
12713
  historyItemEditAction: "Edit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/chat-kit",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -64,7 +64,7 @@
64
64
  "remark-parse": "^11.0.0",
65
65
  "remark-rehype": "^11.1.2",
66
66
  "unified": "^11.0.5",
67
- "@aleph-alpha/ui-library": "2.2.0"
67
+ "@aleph-alpha/ui-library": "2.3.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@storybook/addon-a11y": "10.2.17",
@@ -0,0 +1,174 @@
1
+ import {
2
+ defaultChatKitLabels,
3
+ provideChatKitLabels,
4
+ } from '../../../handlers/useChatKitLabels';
5
+ import CkFeedbackButton from './CkFeedbackButton.vue';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { render, screen } from '@testing-library/vue';
8
+ import { describe, expect, it } from 'vitest';
9
+ import { defineComponent, h } from 'vue';
10
+
11
+ // Source expected labels from the canonical defaults so the suite asserts
12
+ // behaviour (which button received which event) rather than English copy.
13
+ // `feedbackThumbsUp` / `feedbackThumbsDown` are typed as optional on
14
+ // `ChatKitLabels` (for backward-compatible consumer types), but the defaults
15
+ // object always provides them, so we narrow to `string` for `getByRole`.
16
+ const THUMBS_UP = defaultChatKitLabels.feedbackThumbsUp!;
17
+ const THUMBS_DOWN = defaultChatKitLabels.feedbackThumbsDown!;
18
+
19
+ describe('CkFeedbackButton', () => {
20
+ // AC (SPHER-4415): "Each completed assistant message shows thumbs up/down icons."
21
+ it('renders both thumbs unselected and interactive by default', () => {
22
+ render(CkFeedbackButton);
23
+
24
+ const up = screen.getByRole('button', { name: THUMBS_UP });
25
+ const down = screen.getByRole('button', { name: THUMBS_DOWN });
26
+
27
+ expect(up).not.toBeDisabled();
28
+ expect(down).not.toBeDisabled();
29
+ expect(up).toHaveAttribute('aria-pressed', 'false');
30
+ expect(down).toHaveAttribute('aria-pressed', 'false');
31
+ });
32
+
33
+ // AC (SPHER-4415): "Clicking a thumb submits binary feedback (positive/negative)."
34
+ it("emits 'up' when the thumbs-up button is clicked", async () => {
35
+ const user = userEvent.setup();
36
+ const { emitted } = render(CkFeedbackButton);
37
+
38
+ await user.click(screen.getByRole('button', { name: THUMBS_UP }));
39
+
40
+ expect(emitted('update:value')).toEqual([['up']]);
41
+ });
42
+
43
+ it("emits 'down' when the thumbs-down button is clicked", async () => {
44
+ const user = userEvent.setup();
45
+ const { emitted } = render(CkFeedbackButton);
46
+
47
+ await user.click(screen.getByRole('button', { name: THUMBS_DOWN }));
48
+
49
+ expect(emitted('update:value')).toEqual([['down']]);
50
+ });
51
+
52
+ // AC (SPHER-4452): "Selected (thumbs up active / thumbs down active)" must
53
+ // be communicated to assistive tech as well as visually.
54
+ it('reflects the current value via aria-pressed', () => {
55
+ render(CkFeedbackButton, { props: { value: 'up' } });
56
+
57
+ expect(screen.getByRole('button', { name: THUMBS_UP })).toHaveAttribute(
58
+ 'aria-pressed',
59
+ 'true',
60
+ );
61
+ expect(screen.getByRole('button', { name: THUMBS_DOWN })).toHaveAttribute(
62
+ 'aria-pressed',
63
+ 'false',
64
+ );
65
+ });
66
+
67
+ // Pre-lock UX: while feedback is still mutable, users can change their mind.
68
+ it('lets the user swap directly to the other thumb', async () => {
69
+ const user = userEvent.setup();
70
+ const { emitted } = render(CkFeedbackButton, { props: { value: 'up' } });
71
+
72
+ await user.click(screen.getByRole('button', { name: THUMBS_DOWN }));
73
+
74
+ expect(emitted('update:value')).toEqual([['down']]);
75
+ });
76
+
77
+ it('clears the choice when the selected thumb is clicked again', async () => {
78
+ const user = userEvent.setup();
79
+ const { emitted } = render(CkFeedbackButton, { props: { value: 'up' } });
80
+
81
+ await user.click(screen.getByRole('button', { name: THUMBS_UP }));
82
+
83
+ expect(emitted('update:value')).toEqual([[null]]);
84
+ });
85
+
86
+ // AC (SPHER-4415 + SPHER-4452): "Once submitted, feedback is locked"; the
87
+ // unselected thumb is non-interactive and the choice cannot be changed.
88
+ describe('when locked', () => {
89
+ it('disables the unselected thumb and ignores clicks on the selected one', async () => {
90
+ const user = userEvent.setup();
91
+ const { emitted } = render(CkFeedbackButton, {
92
+ props: { value: 'up', locked: true },
93
+ });
94
+
95
+ const up = screen.getByRole('button', { name: THUMBS_UP });
96
+ const down = screen.getByRole('button', { name: THUMBS_DOWN });
97
+
98
+ expect(up).not.toBeDisabled();
99
+ expect(down).toBeDisabled();
100
+
101
+ await user.click(up);
102
+ await user.click(down);
103
+
104
+ expect(emitted('update:value')).toBeUndefined();
105
+ });
106
+ });
107
+
108
+ // Documented prop: `disabled` suppresses interaction while the assistant
109
+ // message is still streaming or in an error state.
110
+ describe('when disabled', () => {
111
+ it('disables both thumbs and does not emit', async () => {
112
+ const user = userEvent.setup();
113
+ const { emitted } = render(CkFeedbackButton, {
114
+ props: { disabled: true },
115
+ });
116
+
117
+ const up = screen.getByRole('button', { name: THUMBS_UP });
118
+ const down = screen.getByRole('button', { name: THUMBS_DOWN });
119
+
120
+ expect(up).toBeDisabled();
121
+ expect(down).toBeDisabled();
122
+
123
+ await user.click(up);
124
+ await user.click(down);
125
+
126
+ expect(emitted('update:value')).toBeUndefined();
127
+ });
128
+ });
129
+
130
+ // Localisation: labels resolve through `useChatKitLabels`, with per-instance
131
+ // props as the override path for one-off custom strings.
132
+ describe('label resolution', () => {
133
+ it('uses labels injected via provideChatKitLabels', () => {
134
+ const translated = {
135
+ feedbackThumbsUp: 'Gute Antwort',
136
+ feedbackThumbsDown: 'Schlechte Antwort',
137
+ };
138
+ const Wrapper = defineComponent({
139
+ setup() {
140
+ provideChatKitLabels(() => translated);
141
+ return () => h(CkFeedbackButton);
142
+ },
143
+ });
144
+
145
+ render(Wrapper);
146
+
147
+ expect(
148
+ screen.getByRole('button', { name: translated.feedbackThumbsUp }),
149
+ ).toBeInTheDocument();
150
+ expect(
151
+ screen.getByRole('button', { name: translated.feedbackThumbsDown }),
152
+ ).toBeInTheDocument();
153
+ });
154
+
155
+ it('lets per-instance aria-label props override the injected labels', () => {
156
+ const Wrapper = defineComponent({
157
+ setup() {
158
+ provideChatKitLabels(() => ({ feedbackThumbsUp: 'Gute Antwort' }));
159
+ return () =>
160
+ h(CkFeedbackButton, { ariaLabelThumbsUp: 'Per-instance up' });
161
+ },
162
+ });
163
+
164
+ render(Wrapper);
165
+
166
+ expect(
167
+ screen.getByRole('button', { name: 'Per-instance up' }),
168
+ ).toBeInTheDocument();
169
+ expect(
170
+ screen.queryByRole('button', { name: 'Gute Antwort' }),
171
+ ).not.toBeInTheDocument();
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,179 @@
1
+ import CkFeedbackButton from './CkFeedbackButton.vue';
2
+ import type { CkFeedbackValue } from './types';
3
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
4
+ import { ref } from 'vue';
5
+
6
+ const meta: Meta<typeof CkFeedbackButton> = {
7
+ title: 'Base/CkFeedbackButton',
8
+ component: CkFeedbackButton,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ value: {
12
+ control: 'select',
13
+ options: [null, 'up', 'down'],
14
+ description:
15
+ 'Currently selected feedback value. `null` means no choice has been made.',
16
+ },
17
+ locked: {
18
+ control: 'boolean',
19
+ description:
20
+ 'When `true`, the current value is final: clicks are ignored and the unselected thumb becomes non-interactive.',
21
+ },
22
+ disabled: {
23
+ control: 'boolean',
24
+ description:
25
+ 'When `true`, both thumbs are disabled regardless of `value`.',
26
+ },
27
+ ariaLabelThumbsUp: { control: 'text' },
28
+ ariaLabelThumbsDown: { control: 'text' },
29
+ ariaLabelGroup: { control: 'text' },
30
+ },
31
+ decorators: [
32
+ () => ({
33
+ template:
34
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
35
+ }),
36
+ ],
37
+ };
38
+
39
+ export default meta;
40
+ type Story = StoryObj<typeof meta>;
41
+
42
+ const defaultTemplateSource = `<script setup lang="ts">
43
+ import { ref } from 'vue';
44
+ import { CkFeedbackButton } from '@aleph-alpha/chat-kit';
45
+ import type { CkFeedbackValue } from '@aleph-alpha/chat-kit';
46
+
47
+ const value = ref<CkFeedbackValue>(null);
48
+ </script>
49
+
50
+ <template>
51
+ <CkFeedbackButton v-model:value="value" />
52
+ </template>`;
53
+
54
+ const selectedUpTemplateSource = `<script setup lang="ts">
55
+ import { CkFeedbackButton } from '@aleph-alpha/chat-kit';
56
+ </script>
57
+
58
+ <template>
59
+ <CkFeedbackButton value="up" />
60
+ </template>`;
61
+
62
+ const selectedDownTemplateSource = `<script setup lang="ts">
63
+ import { CkFeedbackButton } from '@aleph-alpha/chat-kit';
64
+ </script>
65
+
66
+ <template>
67
+ <CkFeedbackButton value="down" />
68
+ </template>`;
69
+
70
+ const lockedTemplateSource = `<script setup lang="ts">
71
+ import { CkFeedbackButton } from '@aleph-alpha/chat-kit';
72
+ </script>
73
+
74
+ <template>
75
+ <!-- Choice has been submitted; user cannot change it. -->
76
+ <CkFeedbackButton value="up" locked />
77
+ </template>`;
78
+
79
+ const disabledTemplateSource = `<script setup lang="ts">
80
+ import { CkFeedbackButton } from '@aleph-alpha/chat-kit';
81
+ </script>
82
+
83
+ <template>
84
+ <!-- e.g. while the assistant reply is still streaming. -->
85
+ <CkFeedbackButton disabled />
86
+ </template>`;
87
+
88
+ const submitOnSelectTemplateSource = `<script setup lang="ts">
89
+ import { ref } from 'vue';
90
+ import { CkFeedbackButton } from '@aleph-alpha/chat-kit';
91
+ import type { CkFeedbackValue } from '@aleph-alpha/chat-kit';
92
+
93
+ const value = ref<CkFeedbackValue>(null);
94
+ const locked = ref(false);
95
+
96
+ function onUpdate(next: CkFeedbackValue) {
97
+ value.value = next;
98
+ if (next !== null) {
99
+ // Persist to backend, then freeze the choice.
100
+ locked.value = true;
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <template>
106
+ <CkFeedbackButton :value="value" :locked="locked" @update:value="onUpdate" />
107
+ </template>`;
108
+
109
+ /**
110
+ * Default, idle state. Both thumbs are interactive and unfilled. Use this
111
+ * inside `CkConversation`'s `#agent-message-footer` slot to collect binary
112
+ * feedback on assistant replies.
113
+ */
114
+ export const Default: Story = {
115
+ args: { value: null },
116
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
117
+ };
118
+
119
+ /**
120
+ * Thumbs-up is selected. The icon fills and the button shows a tinted
121
+ * background to mark the active choice. The host still receives
122
+ * `update:value` events; pass `locked` to make the choice final.
123
+ */
124
+ export const SelectedUp: Story = {
125
+ args: { value: 'up' },
126
+ parameters: { docs: { source: { code: selectedUpTemplateSource } } },
127
+ };
128
+
129
+ /**
130
+ * Thumbs-down is selected. Same affordance as `SelectedUp`, on the other
131
+ * thumb.
132
+ */
133
+ export const SelectedDown: Story = {
134
+ args: { value: 'down' },
135
+ parameters: { docs: { source: { code: selectedDownTemplateSource } } },
136
+ };
137
+
138
+ /**
139
+ * Locked after submission. The chosen thumb stays filled; the other becomes
140
+ * non-interactive. Clicks no longer emit `update:value`.
141
+ */
142
+ export const Locked: Story = {
143
+ args: { value: 'up', locked: true },
144
+ parameters: { docs: { source: { code: lockedTemplateSource } } },
145
+ };
146
+
147
+ /**
148
+ * Both thumbs disabled, for example while the assistant message is still
149
+ * streaming or after a network error.
150
+ */
151
+ export const Disabled: Story = {
152
+ args: { value: null, disabled: true },
153
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
154
+ };
155
+
156
+ /**
157
+ * Realistic flow: the host listens for `update:value`, persists the choice,
158
+ * then sets `locked` to freeze the control. Click a thumb to see it lock.
159
+ */
160
+ export const SubmitOnSelect: Story = {
161
+ render: () => ({
162
+ components: { CkFeedbackButton },
163
+ setup() {
164
+ const value = ref<CkFeedbackValue>(null);
165
+ const locked = ref(false);
166
+ function onUpdate(next: CkFeedbackValue) {
167
+ value.value = next;
168
+ if (next !== null) {
169
+ locked.value = true;
170
+ }
171
+ }
172
+ return { value, locked, onUpdate };
173
+ },
174
+ template: `
175
+ <CkFeedbackButton :value="value" :locked="locked" @update:value="onUpdate" />
176
+ `,
177
+ }),
178
+ parameters: { docs: { source: { code: submitOnSelectTemplateSource } } },
179
+ };
@@ -0,0 +1,157 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ defaultChatKitLabels,
4
+ useChatKitLabels,
5
+ } from '../../../handlers/useChatKitLabels';
6
+ import { CkMessageActionButton } from '../CkMessageActionButton';
7
+ import type { CkFeedbackButtonProps, CkFeedbackValue } from './types';
8
+ import { UiIcon, UiTooltip } from '@aleph-alpha/ui-library';
9
+ import { computed } from 'vue';
10
+
11
+ defineOptions({
12
+ name: 'CkFeedbackButton',
13
+ });
14
+
15
+ const props = withDefaults(defineProps<CkFeedbackButtonProps>(), {
16
+ value: null,
17
+ locked: false,
18
+ disabled: false,
19
+ ariaLabelThumbsUp: undefined,
20
+ ariaLabelThumbsDown: undefined,
21
+ ariaLabelGroup: undefined,
22
+ });
23
+
24
+ const emit = defineEmits<{
25
+ 'update:value': [value: CkFeedbackValue];
26
+ }>();
27
+
28
+ const labels = useChatKitLabels();
29
+
30
+ // Label resolution order: per-instance prop, then injected i18n label, then
31
+ // the English default. The final fallback covers consumers who type their
32
+ // labels object as `ChatKitLabels` without the (optional) feedback keys, so
33
+ // the value is always a string at the call site.
34
+ const thumbsUpLabel = computed(
35
+ () =>
36
+ props.ariaLabelThumbsUp ??
37
+ labels.value.feedbackThumbsUp ??
38
+ defaultChatKitLabels.feedbackThumbsUp!,
39
+ );
40
+ const thumbsDownLabel = computed(
41
+ () =>
42
+ props.ariaLabelThumbsDown ??
43
+ labels.value.feedbackThumbsDown ??
44
+ defaultChatKitLabels.feedbackThumbsDown!,
45
+ );
46
+ const groupLabel = computed(
47
+ () =>
48
+ props.ariaLabelGroup ??
49
+ labels.value.feedbackGroup ??
50
+ defaultChatKitLabels.feedbackGroup!,
51
+ );
52
+
53
+ const isUpSelected = computed(() => props.value === 'up');
54
+ const isDownSelected = computed(() => props.value === 'down');
55
+
56
+ // When locked, the unselected thumb is disabled and skipped in the tab order.
57
+ // The selected thumb stays focusable (no native `disabled`) so assistive tech
58
+ // can still announce the locked-in choice via `aria-pressed`, but both thumbs
59
+ // carry `aria-disabled="true"` so screen readers communicate that the choice
60
+ // is no longer interactive. Clicks on either are no-ops, gated by the early
61
+ // return in `handleClick`.
62
+ const isUpDisabled = computed(
63
+ () => props.disabled || (props.locked && !isUpSelected.value),
64
+ );
65
+ const isDownDisabled = computed(
66
+ () => props.disabled || (props.locked && !isDownSelected.value),
67
+ );
68
+
69
+ function handleClick(target: 'up' | 'down'): void {
70
+ if (props.disabled || props.locked) {
71
+ return;
72
+ }
73
+ const isCurrentlySelected =
74
+ target === 'up' ? isUpSelected.value : isDownSelected.value;
75
+ emit('update:value', isCurrentlySelected ? null : target);
76
+ }
77
+ </script>
78
+
79
+ <template>
80
+ <div
81
+ role="group"
82
+ :aria-label="groupLabel"
83
+ class="ck-feedback-button flex items-center gap-1"
84
+ >
85
+ <UiTooltip
86
+ :content="thumbsUpLabel"
87
+ side="bottom"
88
+ align="center"
89
+ :delay-duration="0"
90
+ >
91
+ <CkMessageActionButton
92
+ v-bind="{ ariaLabel: thumbsUpLabel }"
93
+ :aria-pressed="isUpSelected"
94
+ :aria-disabled="locked || undefined"
95
+ :disabled="isUpDisabled"
96
+ :class="[
97
+ 'ck-feedback-button__thumb',
98
+ { 'ck-feedback-button__thumb--selected': isUpSelected },
99
+ ]"
100
+ @click="handleClick('up')"
101
+ >
102
+ <UiIcon
103
+ name="thumbs-up"
104
+ :size="16"
105
+ aria-hidden="true"
106
+ :class="isUpSelected ? 'fill-current' : undefined"
107
+ />
108
+ </CkMessageActionButton>
109
+ </UiTooltip>
110
+ <UiTooltip
111
+ :content="thumbsDownLabel"
112
+ side="bottom"
113
+ align="center"
114
+ :delay-duration="0"
115
+ >
116
+ <CkMessageActionButton
117
+ v-bind="{ ariaLabel: thumbsDownLabel }"
118
+ :aria-pressed="isDownSelected"
119
+ :aria-disabled="locked || undefined"
120
+ :disabled="isDownDisabled"
121
+ :class="[
122
+ 'ck-feedback-button__thumb',
123
+ { 'ck-feedback-button__thumb--selected': isDownSelected },
124
+ ]"
125
+ @click="handleClick('down')"
126
+ >
127
+ <UiIcon
128
+ name="thumbs-down"
129
+ :size="16"
130
+ aria-hidden="true"
131
+ :class="isDownSelected ? 'fill-current' : undefined"
132
+ />
133
+ </CkMessageActionButton>
134
+ </UiTooltip>
135
+ </div>
136
+ </template>
137
+
138
+ <style scoped>
139
+ .ck-feedback-button__thumb :deep(button) {
140
+ transition: background-color 0.15s ease-out;
141
+ }
142
+
143
+ /* Selected thumbs stay visually emphasised on top of the default ghost
144
+ variant: the filled icon (via `fill-current`) carries the primary
145
+ affordance; the tinted background reinforces it without competing with
146
+ neighbouring action buttons. The token matches `bg-core-bg-tertiary`
147
+ used elsewhere in chat-kit. */
148
+ .ck-feedback-button__thumb--selected :deep(button) {
149
+ background-color: var(--core-bg-tertiary);
150
+ }
151
+
152
+ @media (prefers-reduced-motion: reduce) {
153
+ .ck-feedback-button__thumb :deep(button) {
154
+ transition-duration: 0.01ms;
155
+ }
156
+ }
157
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as CkFeedbackButton } from './CkFeedbackButton.vue';
2
+ export type * from './types';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Binary thumbs up / thumbs down feedback control for assistant messages.
3
+ * Fully prop-driven: the host application owns the selection and lock state
4
+ * and decides when (or if) to persist it to a backend.
5
+ *
6
+ * Labels default to those provided by `useChatKitLabels` (which can be
7
+ * translated via `provideChatKitLabels`). The `ariaLabel*` props override
8
+ * the injected labels on a per-instance basis when set.
9
+ *
10
+ * @category Feedback
11
+ * @useCases like or dislike an assistant reply, collect binary message rating, lock feedback after submission, rate AI response
12
+ * @keywords feedback, thumbs up, thumbs down, like, dislike, rating, vote, action, message, button
13
+ * @related CkMessageActionButton, CkCopyMessageActionButton, CkAgentMessage, CkConversation
14
+ */
15
+ export type CkFeedbackValue = 'up' | 'down' | null;
16
+
17
+ export interface CkFeedbackButtonProps {
18
+ /**
19
+ * Currently selected feedback value. `null` means no choice has been made.
20
+ * @default null
21
+ */
22
+ value?: CkFeedbackValue;
23
+ /**
24
+ * When `true`, the current `value` is final: clicks are ignored and the
25
+ * unselected thumb becomes non-interactive. Use after the host has
26
+ * persisted the user's choice.
27
+ * @default false
28
+ */
29
+ locked?: boolean;
30
+ /**
31
+ * When `true`, both thumbs are disabled regardless of `value`. Use while
32
+ * the parent message is still streaming or in an error state.
33
+ * @default false
34
+ */
35
+ disabled?: boolean;
36
+ /**
37
+ * Overrides the injected `feedbackThumbsUp` label for the thumbs-up button.
38
+ * When omitted, falls back to `useChatKitLabels().feedbackThumbsUp`.
39
+ */
40
+ ariaLabelThumbsUp?: string;
41
+ /**
42
+ * Overrides the injected `feedbackThumbsDown` label for the thumbs-down
43
+ * button. When omitted, falls back to
44
+ * `useChatKitLabels().feedbackThumbsDown`.
45
+ */
46
+ ariaLabelThumbsDown?: string;
47
+ /**
48
+ * Overrides the injected `feedbackGroup` label for the surrounding group.
49
+ * When omitted, falls back to `useChatKitLabels().feedbackGroup`.
50
+ */
51
+ ariaLabelGroup?: string;
52
+ }
53
+
54
+ export interface CkFeedbackButtonEmits {
55
+ /**
56
+ * Emitted when the user clicks an enabled thumb. The payload is the new
57
+ * value: `'up'`, `'down'`, or `null` when toggling off the current choice.
58
+ * Not emitted while `locked` or `disabled`.
59
+ */
60
+ (e: 'update:value', value: CkFeedbackValue): void;
61
+ }
@@ -3,6 +3,11 @@ export { CkAgentMessage } from './CkAgentMessage';
3
3
  export { CkConversationLayout } from './CkConversationLayout';
4
4
  export type { CkConversationLayoutProps } from './CkConversationLayout';
5
5
  export { CkCopyMessageActionButton } from './CkCopyMessageActionButton';
6
+ export { CkFeedbackButton } from './CkFeedbackButton';
7
+ export type {
8
+ CkFeedbackButtonProps,
9
+ CkFeedbackValue,
10
+ } from './CkFeedbackButton';
6
11
  export { CkHistoryItem } from './CkHistoryItem';
7
12
  export type { CkHistoryItemProps, CkHistoryItemEmits } from './CkHistoryItem';
8
13
  export { CkHistoryWrapper } from './CkHistoryWrapper';