@aleph-alpha/chat-kit 1.1.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.
- package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DpYyyCgU.js → CkInput.vue_vue_type_script_setup_true_lang-BoUxOQbH.js} +154 -39
- package/dist/chat-kit.css +18 -0
- package/dist/components/base/CkFeedbackButton/CkFeedbackButton.stories.d.ts +38 -0
- package/dist/components/base/CkFeedbackButton/CkFeedbackButton.stories.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackButton/CkFeedbackButton.vue.d.ts +16 -0
- package/dist/components/base/CkFeedbackButton/CkFeedbackButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackButton/index.d.ts +3 -0
- package/dist/components/base/CkFeedbackButton/index.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackButton/types.d.ts +60 -0
- package/dist/components/base/CkFeedbackButton/types.d.ts.map +1 -0
- 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 +4 -3
- 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 +15 -14
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/index.js +3 -2
- package/dist/composables/useConversationSessions.d.ts +29 -0
- package/dist/composables/useConversationSessions.d.ts.map +1 -0
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/useChatKitLabels.d.ts +18 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -19
- package/dist/purify.es-md74IFlY.js +1081 -0
- package/dist/{useChatKitLabels-Dvi2FmjE.js → useChatKitLabels-ftC52m7f.js} +78 -1152
- package/dist/{useConversations-Ca242D2P.js → useConversationSessions-ulFhtpQr.js} +50 -2
- package/dist/{useMessageHandler-uLHNac39.js → useMessageHandler-BmBdu2EW.js} +4 -1
- package/package.json +2 -2
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.spec.ts +174 -0
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.stories.ts +179 -0
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +157 -0
- package/src/components/base/CkFeedbackButton/index.ts +2 -0
- package/src/components/base/CkFeedbackButton/types.ts +61 -0
- package/src/components/base/index.ts +5 -0
- package/src/components/composed/CkConversation/CkConversation.stories.ts +46 -17
- package/src/components/index.ts +3 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useConversationSessions.spec.ts +86 -0
- package/src/composables/useConversationSessions.ts +85 -0
- package/src/handlers/useChatKitLabels.spec.ts +70 -50
- package/src/handlers/useChatKitLabels.ts +21 -0
- package/src/index.ts +11 -1
- package/dist/generateId-BHf0W4Rq.js +0 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { g as generateId } from "./
|
|
1
|
+
import { g as generateId, u as useMessageHandler } from "./useMessageHandler-BmBdu2EW.js";
|
|
2
2
|
import { ref, shallowRef, computed, readonly } from "vue";
|
|
3
|
+
import "./purify.es-md74IFlY.js";
|
|
3
4
|
function secondsToDate(seconds) {
|
|
4
5
|
return new Date(seconds * 1e3);
|
|
5
6
|
}
|
|
@@ -189,7 +190,54 @@ function makePlaceholder(title) {
|
|
|
189
190
|
updatedAt: now
|
|
190
191
|
};
|
|
191
192
|
}
|
|
193
|
+
function useConversationSessions() {
|
|
194
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
195
|
+
const focusedId = ref(null);
|
|
196
|
+
function createSession(conversationId) {
|
|
197
|
+
const existing = sessions.get(conversationId);
|
|
198
|
+
if (existing) return existing;
|
|
199
|
+
const session = {
|
|
200
|
+
id: conversationId,
|
|
201
|
+
messageHandler: useMessageHandler()
|
|
202
|
+
};
|
|
203
|
+
sessions.set(conversationId, session);
|
|
204
|
+
return session;
|
|
205
|
+
}
|
|
206
|
+
function getSession(conversationId) {
|
|
207
|
+
return sessions.get(conversationId);
|
|
208
|
+
}
|
|
209
|
+
function setActiveConversation(conversationId) {
|
|
210
|
+
if (conversationId === null) {
|
|
211
|
+
focusedId.value = null;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
createSession(conversationId);
|
|
215
|
+
focusedId.value = conversationId;
|
|
216
|
+
}
|
|
217
|
+
function removeSession(conversationId) {
|
|
218
|
+
sessions.delete(conversationId);
|
|
219
|
+
if (focusedId.value === conversationId) {
|
|
220
|
+
focusedId.value = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const activeSession = computed(() => {
|
|
224
|
+
const id = focusedId.value;
|
|
225
|
+
return id !== null ? sessions.get(id) ?? null : null;
|
|
226
|
+
});
|
|
227
|
+
const messageHandler = computed(
|
|
228
|
+
() => activeSession.value?.messageHandler ?? null
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
activeSession,
|
|
232
|
+
messageHandler,
|
|
233
|
+
createSession,
|
|
234
|
+
getSession,
|
|
235
|
+
setActiveConversation,
|
|
236
|
+
removeSession
|
|
237
|
+
};
|
|
238
|
+
}
|
|
192
239
|
export {
|
|
193
|
-
|
|
240
|
+
useConversationSessions as a,
|
|
241
|
+
useConversations as b,
|
|
194
242
|
useChat as u
|
|
195
243
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { g as generateId } from "./generateId-BHf0W4Rq.js";
|
|
2
1
|
import { reactive, ref, computed, readonly } from "vue";
|
|
3
2
|
function createEmptyTree() {
|
|
4
3
|
return { nodes: {}, rootId: null };
|
|
@@ -78,6 +77,9 @@ function navigate(tree, targetId) {
|
|
|
78
77
|
if (idx === -1) return;
|
|
79
78
|
parent.activeChildIndex = idx;
|
|
80
79
|
}
|
|
80
|
+
function generateId() {
|
|
81
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : String(Date.now());
|
|
82
|
+
}
|
|
81
83
|
const useMessageHandler = () => {
|
|
82
84
|
const tree = reactive(createEmptyTree());
|
|
83
85
|
const currentUserMessage = ref("");
|
|
@@ -178,5 +180,6 @@ const useMessageHandler = () => {
|
|
|
178
180
|
};
|
|
179
181
|
};
|
|
180
182
|
export {
|
|
183
|
+
generateId as g,
|
|
181
184
|
useMessageHandler as u
|
|
182
185
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aleph-alpha/chat-kit",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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,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';
|