@aleph-alpha/chat-kit 1.12.0 → 1.14.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-C1Jnn1fK.js → CkInput.vue_vue_type_script_setup_true_lang-BVZ7FG88.js} +430 -142
- package/dist/chat-kit.css +47 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts +30 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts +13 -0
- package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/index.d.ts +3 -0
- package/dist/components/base/CkFeedbackDialog/index.d.ts.map +1 -0
- package/dist/components/base/CkFeedbackDialog/types.d.ts +152 -0
- package/dist/components/base/CkFeedbackDialog/types.d.ts.map +1 -0
- package/dist/components/base/CkUserMessage/CkUserMessage.stories.d.ts +6 -0
- package/dist/components/base/CkUserMessage/CkUserMessage.stories.d.ts.map +1 -1
- package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts +4 -1
- package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts.map +1 -1
- package/dist/components/base/CkUserMessage/types.d.ts +14 -5
- package/dist/components/base/CkUserMessage/types.d.ts.map +1 -1
- package/dist/components/base/index.d.ts +2 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +16 -15
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +22 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -16
- package/dist/{useChatKitLabels-ftC52m7f.js → useChatKitLabels-BxNkTzr2.js} +11 -0
- package/package.json +5 -5
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.spec.ts +177 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.stories.ts +258 -0
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +288 -0
- package/src/components/base/CkFeedbackDialog/index.ts +2 -0
- package/src/components/base/CkFeedbackDialog/types.ts +155 -0
- package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +25 -0
- package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +34 -0
- package/src/components/base/CkUserMessage/CkUserMessage.vue +115 -5
- package/src/components/base/CkUserMessage/types.ts +14 -5
- package/src/components/base/index.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/handlers/useChatKitLabels.spec.ts +13 -0
- package/src/handlers/useChatKitLabels.ts +35 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
defaultChatKitLabels,
|
|
4
|
+
useChatKitLabels,
|
|
5
|
+
} from '../../../handlers/useChatKitLabels';
|
|
6
|
+
import { CkMarkdownRenderer } from '../CkMarkdownRenderer';
|
|
7
|
+
import type {
|
|
8
|
+
CkFeedbackDialogProps,
|
|
9
|
+
CkFeedbackDialogSubmitPayload,
|
|
10
|
+
} from './types';
|
|
11
|
+
import {
|
|
12
|
+
UiButton,
|
|
13
|
+
UiDialog,
|
|
14
|
+
UiDialogContent,
|
|
15
|
+
UiDialogDescription,
|
|
16
|
+
UiField,
|
|
17
|
+
UiFieldLabel,
|
|
18
|
+
UiTextarea,
|
|
19
|
+
UiToggleGroup,
|
|
20
|
+
UiToggleGroupItem,
|
|
21
|
+
} from '@aleph-alpha/ui-library';
|
|
22
|
+
import { computed, ref, useId, watch } from 'vue';
|
|
23
|
+
|
|
24
|
+
defineOptions({
|
|
25
|
+
name: 'CkFeedbackDialog',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
open = false,
|
|
30
|
+
sentiment,
|
|
31
|
+
responseText = '',
|
|
32
|
+
reasons,
|
|
33
|
+
maxFreeTextLength = 250,
|
|
34
|
+
submitting = false,
|
|
35
|
+
disabled = false,
|
|
36
|
+
titlePositive,
|
|
37
|
+
titleNegative,
|
|
38
|
+
responseHeading: responseHeadingProp,
|
|
39
|
+
description: descriptionProp,
|
|
40
|
+
otherLabel: otherLabelProp,
|
|
41
|
+
otherTooltip: otherTooltipProp,
|
|
42
|
+
freeTextPlaceholder: freeTextPlaceholderProp,
|
|
43
|
+
cancelLabel: cancelLabelProp,
|
|
44
|
+
submitLabel: submitLabelProp,
|
|
45
|
+
} = defineProps<CkFeedbackDialogProps>();
|
|
46
|
+
|
|
47
|
+
const emit = defineEmits<{
|
|
48
|
+
'update:open': [open: boolean];
|
|
49
|
+
submit: [payload: CkFeedbackDialogSubmitPayload];
|
|
50
|
+
cancel: [];
|
|
51
|
+
}>();
|
|
52
|
+
|
|
53
|
+
const labels = useChatKitLabels();
|
|
54
|
+
|
|
55
|
+
const title = computed(() => {
|
|
56
|
+
if (sentiment === 'positive') {
|
|
57
|
+
return (
|
|
58
|
+
titlePositive ??
|
|
59
|
+
labels.value.feedbackDialogTitlePositive ??
|
|
60
|
+
defaultChatKitLabels.feedbackDialogTitlePositive!
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
titleNegative ??
|
|
65
|
+
labels.value.feedbackDialogTitleNegative ??
|
|
66
|
+
defaultChatKitLabels.feedbackDialogTitleNegative!
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
const responseHeading = computed(
|
|
70
|
+
() =>
|
|
71
|
+
responseHeadingProp ??
|
|
72
|
+
labels.value.feedbackDialogResponseHeading ??
|
|
73
|
+
defaultChatKitLabels.feedbackDialogResponseHeading!,
|
|
74
|
+
);
|
|
75
|
+
const description = computed(
|
|
76
|
+
() =>
|
|
77
|
+
descriptionProp ??
|
|
78
|
+
labels.value.feedbackDialogDescription ??
|
|
79
|
+
defaultChatKitLabels.feedbackDialogDescription!,
|
|
80
|
+
);
|
|
81
|
+
const otherLabel = computed(
|
|
82
|
+
() =>
|
|
83
|
+
otherLabelProp ??
|
|
84
|
+
labels.value.feedbackDialogOtherLabel ??
|
|
85
|
+
defaultChatKitLabels.feedbackDialogOtherLabel!,
|
|
86
|
+
);
|
|
87
|
+
const otherTooltip = computed(
|
|
88
|
+
() =>
|
|
89
|
+
otherTooltipProp ??
|
|
90
|
+
labels.value.feedbackDialogOtherTooltip ??
|
|
91
|
+
defaultChatKitLabels.feedbackDialogOtherTooltip!,
|
|
92
|
+
);
|
|
93
|
+
const freeTextPlaceholder = computed(
|
|
94
|
+
() =>
|
|
95
|
+
freeTextPlaceholderProp ??
|
|
96
|
+
labels.value.feedbackDialogFreeTextPlaceholder ??
|
|
97
|
+
defaultChatKitLabels.feedbackDialogFreeTextPlaceholder!,
|
|
98
|
+
);
|
|
99
|
+
const cancelLabel = computed(
|
|
100
|
+
() =>
|
|
101
|
+
cancelLabelProp ??
|
|
102
|
+
labels.value.feedbackDialogCancelLabel ??
|
|
103
|
+
defaultChatKitLabels.feedbackDialogCancelLabel!,
|
|
104
|
+
);
|
|
105
|
+
const submitLabel = computed(
|
|
106
|
+
() =>
|
|
107
|
+
submitLabelProp ??
|
|
108
|
+
labels.value.feedbackDialogSubmitLabel ??
|
|
109
|
+
defaultChatKitLabels.feedbackDialogSubmitLabel!,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const selectedReason = ref('');
|
|
113
|
+
const freeText = ref('');
|
|
114
|
+
|
|
115
|
+
// Per-instance id so the label/textarea association stays correct even when
|
|
116
|
+
// several dialogs are mounted at once.
|
|
117
|
+
const freeTextId = useId();
|
|
118
|
+
|
|
119
|
+
watch(
|
|
120
|
+
() => open,
|
|
121
|
+
(isOpen, wasOpen) => {
|
|
122
|
+
if (isOpen && !wasOpen) {
|
|
123
|
+
selectedReason.value = '';
|
|
124
|
+
freeText.value = '';
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const showResponseColumn = computed(
|
|
130
|
+
() => typeof responseText === 'string' && responseText !== '',
|
|
131
|
+
);
|
|
132
|
+
const characterCount = computed(() => freeText.value.length);
|
|
133
|
+
const canSubmit = computed(
|
|
134
|
+
() =>
|
|
135
|
+
!disabled &&
|
|
136
|
+
!submitting &&
|
|
137
|
+
(selectedReason.value !== '' || freeText.value.trim() !== ''),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
function handleOpenChange(next: boolean): void {
|
|
141
|
+
// While submitting/disabled the dialog is non-interactive, so ignore
|
|
142
|
+
// reka-ui's close requests (ESC / outside-click / close icon). `open` is
|
|
143
|
+
// controlled, so not emitting keeps it open until the host closes it.
|
|
144
|
+
if (!next && (submitting || disabled)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
emit('update:open', next);
|
|
148
|
+
// reka-ui emits update:open(false) on ESC / outside-click / close icon —
|
|
149
|
+
// surface those as `cancel` too.
|
|
150
|
+
if (!next && open) {
|
|
151
|
+
emit('cancel');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function handleCancel(): void {
|
|
156
|
+
if (disabled || submitting) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
emit('cancel');
|
|
160
|
+
emit('update:open', false);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleSubmit(): void {
|
|
164
|
+
if (!canSubmit.value) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
emit('submit', {
|
|
168
|
+
sentiment,
|
|
169
|
+
reason: selectedReason.value === '' ? null : selectedReason.value,
|
|
170
|
+
reasonText: freeText.value.trim(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<template>
|
|
176
|
+
<UiDialog :open="open" @update:open="handleOpenChange">
|
|
177
|
+
<UiDialogContent
|
|
178
|
+
:title="title"
|
|
179
|
+
max-width="max-w-4xl"
|
|
180
|
+
class="ck-feedback-dialog"
|
|
181
|
+
:show-close-button="!(submitting || disabled)"
|
|
182
|
+
>
|
|
183
|
+
<UiDialogDescription class="sr-only">
|
|
184
|
+
{{ description }}
|
|
185
|
+
</UiDialogDescription>
|
|
186
|
+
<div
|
|
187
|
+
:class="[
|
|
188
|
+
'grid gap-12',
|
|
189
|
+
showResponseColumn ? 'grid-cols-2' : 'grid-cols-1',
|
|
190
|
+
]"
|
|
191
|
+
>
|
|
192
|
+
<section v-if="showResponseColumn" class="flex min-h-0 flex-col gap-2">
|
|
193
|
+
<UiFieldLabel>{{ responseHeading }}</UiFieldLabel>
|
|
194
|
+
<div
|
|
195
|
+
class="ck-feedback-dialog__response min-h-0 flex-1 overflow-y-auto rounded-md border border-input bg-background-input-default px-3 py-2 shadow-xs"
|
|
196
|
+
>
|
|
197
|
+
<CkMarkdownRenderer :content="responseText" />
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
|
|
201
|
+
<section class="flex min-h-0 flex-col gap-4">
|
|
202
|
+
<UiToggleGroup
|
|
203
|
+
v-model="selectedReason"
|
|
204
|
+
type="single"
|
|
205
|
+
variant="outline"
|
|
206
|
+
:disabled="disabled || submitting"
|
|
207
|
+
class="grid grid-cols-2 gap-4"
|
|
208
|
+
:aria-label="title"
|
|
209
|
+
>
|
|
210
|
+
<UiToggleGroupItem
|
|
211
|
+
v-for="reason in reasons"
|
|
212
|
+
:key="reason.code"
|
|
213
|
+
:value="reason.code"
|
|
214
|
+
class="ck-feedback-dialog__chip"
|
|
215
|
+
>
|
|
216
|
+
{{ reason.label }}
|
|
217
|
+
</UiToggleGroupItem>
|
|
218
|
+
</UiToggleGroup>
|
|
219
|
+
|
|
220
|
+
<UiField>
|
|
221
|
+
<UiFieldLabel :for="freeTextId" :tooltip="otherTooltip">
|
|
222
|
+
{{ otherLabel }}
|
|
223
|
+
</UiFieldLabel>
|
|
224
|
+
<UiTextarea
|
|
225
|
+
:id="freeTextId"
|
|
226
|
+
v-model="freeText"
|
|
227
|
+
name="ck-feedback-dialog-free-text"
|
|
228
|
+
:placeholder="freeTextPlaceholder"
|
|
229
|
+
:disabled="disabled || submitting"
|
|
230
|
+
:maxlength="maxFreeTextLength"
|
|
231
|
+
rows="3"
|
|
232
|
+
/>
|
|
233
|
+
<div
|
|
234
|
+
class="self-end text-sm text-content-on-surface-muted tabular-nums"
|
|
235
|
+
aria-live="polite"
|
|
236
|
+
>
|
|
237
|
+
{{ characterCount }}/{{ maxFreeTextLength }}
|
|
238
|
+
</div>
|
|
239
|
+
</UiField>
|
|
240
|
+
</section>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<template #actions>
|
|
244
|
+
<UiButton
|
|
245
|
+
type="button"
|
|
246
|
+
variant="outline"
|
|
247
|
+
:aria-label="cancelLabel"
|
|
248
|
+
:disabled="disabled || submitting"
|
|
249
|
+
@click="handleCancel"
|
|
250
|
+
>
|
|
251
|
+
{{ cancelLabel }}
|
|
252
|
+
</UiButton>
|
|
253
|
+
<UiButton
|
|
254
|
+
type="button"
|
|
255
|
+
variant="default"
|
|
256
|
+
:aria-label="submitLabel"
|
|
257
|
+
:loading="submitting"
|
|
258
|
+
:disabled="!canSubmit"
|
|
259
|
+
@click="handleSubmit"
|
|
260
|
+
>
|
|
261
|
+
{{ submitLabel }}
|
|
262
|
+
</UiButton>
|
|
263
|
+
</template>
|
|
264
|
+
</UiDialogContent>
|
|
265
|
+
</UiDialog>
|
|
266
|
+
</template>
|
|
267
|
+
|
|
268
|
+
<style scoped>
|
|
269
|
+
.ck-feedback-dialog__chip {
|
|
270
|
+
width: 100%;
|
|
271
|
+
min-height: 36px;
|
|
272
|
+
height: auto;
|
|
273
|
+
justify-content: center;
|
|
274
|
+
text-align: center;
|
|
275
|
+
line-height: 1rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.ck-feedback-dialog__response :deep(p),
|
|
279
|
+
.ck-feedback-dialog__response :deep(li) {
|
|
280
|
+
line-height: 1.5rem;
|
|
281
|
+
}
|
|
282
|
+
.ck-feedback-dialog__response :deep(p) {
|
|
283
|
+
margin: 0 0 0.5rem 0;
|
|
284
|
+
}
|
|
285
|
+
.ck-feedback-dialog__response :deep(p:last-child) {
|
|
286
|
+
margin-bottom: 0;
|
|
287
|
+
}
|
|
288
|
+
</style>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal dialog for collecting qualitative feedback on an assistant response.
|
|
3
|
+
* Opened after a binary thumbs-up / thumbs-down click (see `CkFeedbackButton`),
|
|
4
|
+
* it lets the user pick a single reason code and add an optional free-text
|
|
5
|
+
* comment, then submit both alongside the original sentiment.
|
|
6
|
+
*
|
|
7
|
+
* One reason per submission: the backend (feedback-kit) stores a single
|
|
8
|
+
* `reason` enum per response, so the dialog is single-select by design.
|
|
9
|
+
*
|
|
10
|
+
* Fully prop-driven: the host application owns `open`, the list of reasons to
|
|
11
|
+
* show (fetched from `GET /feedback/config`), the response text to display, and
|
|
12
|
+
* the submission handler. The component only manages transient form state (the
|
|
13
|
+
* selected reason and free-text value) until the user submits or cancels.
|
|
14
|
+
*
|
|
15
|
+
* @category Feedback
|
|
16
|
+
* @useCases collect qualitative feedback, gather a reason code for a response,
|
|
17
|
+
* capture a free-text comment, drive a two-step rating flow
|
|
18
|
+
* @keywords feedback, dialog, modal, reason codes, free text, qualitative,
|
|
19
|
+
* rating, comment
|
|
20
|
+
* @related CkFeedbackButton, CkMessageActionButton, CkAgentMessage,
|
|
21
|
+
* CkConversation, UiDialog, UiToggleGroup
|
|
22
|
+
*/
|
|
23
|
+
export type CkFeedbackSentiment = 'positive' | 'negative';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A selectable reason: `code` is the stable machine value submitted to the
|
|
27
|
+
* backend; `label` is the human-readable string shown on the chip. Both come
|
|
28
|
+
* from the backend's `GET /feedback/config` so the UI never drifts from what
|
|
29
|
+
* the backend accepts.
|
|
30
|
+
*/
|
|
31
|
+
export interface CkFeedbackReason {
|
|
32
|
+
code: string;
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CkFeedbackDialogSubmitPayload {
|
|
37
|
+
/** The sentiment the dialog was opened for. */
|
|
38
|
+
sentiment: CkFeedbackSentiment;
|
|
39
|
+
/** The selected reason `code`, or `null` if the user only left free text. */
|
|
40
|
+
reason: string | null;
|
|
41
|
+
/** Free-text comment, trimmed. Empty string when the user typed nothing. */
|
|
42
|
+
reasonText: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CkFeedbackDialogProps {
|
|
46
|
+
/**
|
|
47
|
+
* Controlled open state. Use with `v-model:open`. When `true` the dialog is
|
|
48
|
+
* visible and form state is editable; when `false` the dialog is closed.
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
open?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Which sentiment the user gave before this dialog opened. Drives the title
|
|
54
|
+
* ("Give positive feedback" vs "Give negative feedback") and is echoed back
|
|
55
|
+
* on submit.
|
|
56
|
+
*/
|
|
57
|
+
sentiment: CkFeedbackSentiment;
|
|
58
|
+
/**
|
|
59
|
+
* The assistant response being rated. Rendered as markdown in the left
|
|
60
|
+
* column so the user can see what they're giving feedback on. Omit (or pass
|
|
61
|
+
* an empty string) to hide the response column entirely.
|
|
62
|
+
* @default undefined
|
|
63
|
+
*/
|
|
64
|
+
responseText?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Available reasons to render as single-select chips. The host fetches the
|
|
67
|
+
* appropriate per-sentiment list from `GET /feedback/config` and passes it
|
|
68
|
+
* here. The chip shows `label`; the selected `code` is emitted on submit.
|
|
69
|
+
*/
|
|
70
|
+
reasons: CkFeedbackReason[];
|
|
71
|
+
/**
|
|
72
|
+
* Maximum length for the free-text input. The browser will hard-limit the
|
|
73
|
+
* field via the native `maxlength` attribute, and a "x/max" counter is
|
|
74
|
+
* shown beneath the textarea.
|
|
75
|
+
* @default 250
|
|
76
|
+
*/
|
|
77
|
+
maxFreeTextLength?: number;
|
|
78
|
+
/**
|
|
79
|
+
* When `true`, the Submit button shows a busy state and clicks are ignored.
|
|
80
|
+
* Use while the parent is awaiting the backend POST.
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
submitting?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* When `true`, both Submit and Cancel are non-interactive. Primarily used
|
|
86
|
+
* to keep the dialog frozen while showing a confirmation or error state
|
|
87
|
+
* before the host closes it.
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
disabled?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Overrides the injected dialog title for positive feedback. Defaults to
|
|
93
|
+
* `useChatKitLabels().feedbackDialogTitlePositive`.
|
|
94
|
+
*/
|
|
95
|
+
titlePositive?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Overrides the injected dialog title for negative feedback. Defaults to
|
|
98
|
+
* `useChatKitLabels().feedbackDialogTitleNegative`.
|
|
99
|
+
*/
|
|
100
|
+
titleNegative?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Overrides the injected "Response" heading shown above the response
|
|
103
|
+
* preview column. Defaults to `useChatKitLabels().feedbackDialogResponseHeading`.
|
|
104
|
+
*/
|
|
105
|
+
responseHeading?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Screen-reader description for the dialog (sets `aria-describedby`; not
|
|
108
|
+
* shown visually). Defaults to `useChatKitLabels().feedbackDialogDescription`.
|
|
109
|
+
*/
|
|
110
|
+
description?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Overrides the injected "Other" label above the free-text input.
|
|
113
|
+
* Defaults to `useChatKitLabels().feedbackDialogOtherLabel`.
|
|
114
|
+
*/
|
|
115
|
+
otherLabel?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Tooltip text shown when the user hovers the (i) icon next to the "Other"
|
|
118
|
+
* label. Defaults to `useChatKitLabels().feedbackDialogOtherTooltip`.
|
|
119
|
+
*/
|
|
120
|
+
otherTooltip?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Placeholder text for the free-text input. Defaults to
|
|
123
|
+
* `useChatKitLabels().feedbackDialogFreeTextPlaceholder`.
|
|
124
|
+
*/
|
|
125
|
+
freeTextPlaceholder?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Label for the Cancel button. Defaults to
|
|
128
|
+
* `useChatKitLabels().feedbackDialogCancelLabel`.
|
|
129
|
+
*/
|
|
130
|
+
cancelLabel?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Label for the Submit button. Defaults to
|
|
133
|
+
* `useChatKitLabels().feedbackDialogSubmitLabel`.
|
|
134
|
+
*/
|
|
135
|
+
submitLabel?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface CkFeedbackDialogEmits {
|
|
139
|
+
/**
|
|
140
|
+
* Two-way binding for the `open` prop. Emitted when the dialog requests to
|
|
141
|
+
* be closed (Cancel button, close icon, ESC, or outside click).
|
|
142
|
+
*/
|
|
143
|
+
(e: 'update:open', open: boolean): void;
|
|
144
|
+
/**
|
|
145
|
+
* Emitted when the user clicks Submit. The host should persist the payload
|
|
146
|
+
* (e.g. POST to `/feedback/`) then close the dialog by setting `open` to
|
|
147
|
+
* `false`. Not emitted while `submitting` or `disabled`.
|
|
148
|
+
*/
|
|
149
|
+
(e: 'submit', payload: CkFeedbackDialogSubmitPayload): void;
|
|
150
|
+
/**
|
|
151
|
+
* Emitted when the user clicks Cancel or otherwise dismisses the dialog
|
|
152
|
+
* without submitting (close icon, ESC, outside click).
|
|
153
|
+
*/
|
|
154
|
+
(e: 'cancel'): void;
|
|
155
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import CkUserMessage from './CkUserMessage.vue';
|
|
2
2
|
import { render, screen } from '@testing-library/vue';
|
|
3
3
|
|
|
4
|
+
const CONTENT_SELECTOR = '.ck-user-message__content';
|
|
5
|
+
const CLAMPED_CLASS = 'ck-user-message__content--clamped';
|
|
6
|
+
|
|
4
7
|
describe('CkUserMessage', () => {
|
|
5
8
|
it('displays the message text from prop', () => {
|
|
6
9
|
render(CkUserMessage, { props: { message: 'Hello from tests' } });
|
|
@@ -17,4 +20,26 @@ describe('CkUserMessage', () => {
|
|
|
17
20
|
expect(screen.getByText('Custom slot content')).toBeInTheDocument();
|
|
18
21
|
expect(screen.queryByText('Fallback text')).not.toBeInTheDocument();
|
|
19
22
|
});
|
|
23
|
+
|
|
24
|
+
describe('collapsedLines clamping', () => {
|
|
25
|
+
it('does not clamp or render a toggle when collapsedLines is omitted', () => {
|
|
26
|
+
const { container } = render(CkUserMessage, {
|
|
27
|
+
props: { message: 'A message' },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const content = container.querySelector(CONTENT_SELECTOR);
|
|
31
|
+
expect(content).not.toHaveClass(CLAMPED_CLASS);
|
|
32
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('clamps the content to the requested number of lines', () => {
|
|
36
|
+
const { container } = render(CkUserMessage, {
|
|
37
|
+
props: { message: 'A long message', collapsedLines: 3 },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const content = container.querySelector<HTMLElement>(CONTENT_SELECTOR);
|
|
41
|
+
expect(content).toHaveClass(CLAMPED_CLASS);
|
|
42
|
+
expect(content?.style.getPropertyValue('--ck-collapsed-lines')).toBe('3');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
20
45
|
});
|
|
@@ -11,6 +11,11 @@ const meta: Meta<typeof CkUserMessage> = {
|
|
|
11
11
|
description:
|
|
12
12
|
'The message text to display. Can also be provided via the default slot.',
|
|
13
13
|
},
|
|
14
|
+
collapsedLines: {
|
|
15
|
+
control: { type: 'number', min: 0 },
|
|
16
|
+
description:
|
|
17
|
+
'When > 0, clamp the message to this many lines with a fade and a "Show more" / "Show less" toggle (shown only when the content overflows). 0 disables clamping.',
|
|
18
|
+
},
|
|
14
19
|
},
|
|
15
20
|
decorators: [
|
|
16
21
|
() => ({
|
|
@@ -61,6 +66,22 @@ import { CkUserMessage } from '@aleph-alpha/chat-kit';
|
|
|
61
66
|
</CkUserMessage>
|
|
62
67
|
</template>`;
|
|
63
68
|
|
|
69
|
+
const collapsibleMessage =
|
|
70
|
+
'Lorem ipsum dolor sit amet consectetur. Scelerisque justo pharetra enim eros risus. ' +
|
|
71
|
+
'Urna dictumst ultrices mattis tristique lacus in in. Elit sed in a pellentesque purus ' +
|
|
72
|
+
'lectus quis. Massa facilisis eget etiam maecenas eget libero iaculis consectetur. ' +
|
|
73
|
+
'Curabitur vivamus a nulla cras venenatis nec ultrices donec. Dui tempus posuere nulla ' +
|
|
74
|
+
'in id dictumst id congue odio. At lacus elit sit amet. Sodales lorem lacus faucibus ' +
|
|
75
|
+
'condimentum lectus auctor.';
|
|
76
|
+
|
|
77
|
+
const collapsibleTemplateSource = `<script setup lang="ts">
|
|
78
|
+
import { CkUserMessage } from '@aleph-alpha/chat-kit';
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<CkUserMessage :collapsed-lines="4" message="..." />
|
|
83
|
+
</template>`;
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* Standard user message bubble populated via the `message` prop. Use as the
|
|
66
87
|
* default way to render the user turn in a conversation.
|
|
@@ -110,3 +131,16 @@ export const WithSlotContent: Story = {
|
|
|
110
131
|
}),
|
|
111
132
|
parameters: { docs: { source: { code: withSlotContentTemplateSource } } },
|
|
112
133
|
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Long message clamped to 4 lines. The last visible line fades out and a
|
|
137
|
+
* centered "Show more" / "Show less" toggle expands and collapses the text.
|
|
138
|
+
* The toggle only appears when the content actually overflows the clamp.
|
|
139
|
+
*/
|
|
140
|
+
export const Collapsible: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
message: collapsibleMessage,
|
|
143
|
+
collapsedLines: 4,
|
|
144
|
+
},
|
|
145
|
+
parameters: { docs: { source: { code: collapsibleTemplateSource } } },
|
|
146
|
+
};
|
|
@@ -1,19 +1,129 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
defaultChatKitLabels,
|
|
4
|
+
useChatKitLabels,
|
|
5
|
+
} from '../../../handlers/useChatKitLabels';
|
|
2
6
|
import type { CkUserMessageProps } from './types';
|
|
7
|
+
import {
|
|
8
|
+
computed,
|
|
9
|
+
nextTick,
|
|
10
|
+
onBeforeUnmount,
|
|
11
|
+
onMounted,
|
|
12
|
+
ref,
|
|
13
|
+
watch,
|
|
14
|
+
} from 'vue';
|
|
3
15
|
|
|
4
16
|
defineOptions({ name: 'CkUserMessage' });
|
|
5
17
|
|
|
6
|
-
defineProps<CkUserMessageProps>()
|
|
18
|
+
const props = withDefaults(defineProps<CkUserMessageProps>(), {
|
|
19
|
+
message: undefined,
|
|
20
|
+
collapsedLines: 0,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const labels = useChatKitLabels();
|
|
24
|
+
|
|
25
|
+
// Resolution order: injected label, English default.
|
|
26
|
+
const showMoreLabel = computed(
|
|
27
|
+
() =>
|
|
28
|
+
labels.value.userMessageShowMore ??
|
|
29
|
+
defaultChatKitLabels.userMessageShowMore!,
|
|
30
|
+
);
|
|
31
|
+
const showLessLabel = computed(
|
|
32
|
+
() =>
|
|
33
|
+
labels.value.userMessageShowLess ??
|
|
34
|
+
defaultChatKitLabels.userMessageShowLess!,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** Clamping is opt-in: only active when a positive `collapsedLines` is given. */
|
|
38
|
+
const clampEnabled = computed(() => (props.collapsedLines ?? 0) > 0);
|
|
39
|
+
|
|
40
|
+
const contentRef = ref<HTMLElement | null>(null);
|
|
41
|
+
const expanded = ref(false);
|
|
42
|
+
const isOverflowing = ref(false);
|
|
43
|
+
|
|
44
|
+
function measure() {
|
|
45
|
+
const el = contentRef.value;
|
|
46
|
+
if (!el || !clampEnabled.value || expanded.value) return;
|
|
47
|
+
isOverflowing.value = el.scrollHeight - el.clientHeight > 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toggle() {
|
|
51
|
+
expanded.value = !expanded.value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
55
|
+
|
|
56
|
+
onMounted(() => {
|
|
57
|
+
nextTick(measure);
|
|
58
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
59
|
+
resizeObserver = new ResizeObserver(() => measure());
|
|
60
|
+
if (contentRef.value) resizeObserver.observe(contentRef.value);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
onBeforeUnmount(() => resizeObserver?.disconnect());
|
|
65
|
+
|
|
66
|
+
// New content (or a changed clamp) re-evaluates from the collapsed state.
|
|
67
|
+
watch([() => props.message, () => props.collapsedLines], () => {
|
|
68
|
+
expanded.value = false;
|
|
69
|
+
nextTick(measure);
|
|
70
|
+
});
|
|
7
71
|
</script>
|
|
8
72
|
|
|
9
73
|
<template>
|
|
10
|
-
<div
|
|
11
|
-
class="bg-background-surface-primary inline-flex max-w-[600px] flex-col items-end justify-start gap-4 rounded-xl p-6"
|
|
12
|
-
>
|
|
74
|
+
<div class="inline-flex max-w-[536px] flex-col items-end justify-start gap-1">
|
|
13
75
|
<div
|
|
14
|
-
|
|
76
|
+
ref="contentRef"
|
|
77
|
+
class="ck-user-message__content text-content-on-surface-muted min-w-0 max-w-full self-stretch text-lg leading-7 font-medium break-words whitespace-pre-wrap [overflow-wrap:anywhere]"
|
|
78
|
+
:class="{
|
|
79
|
+
'ck-user-message__content--clamped': clampEnabled && !expanded,
|
|
80
|
+
'ck-user-message__content--faded':
|
|
81
|
+
clampEnabled && !expanded && isOverflowing,
|
|
82
|
+
}"
|
|
83
|
+
:style="
|
|
84
|
+
clampEnabled ? { '--ck-collapsed-lines': collapsedLines } : undefined
|
|
85
|
+
"
|
|
15
86
|
>
|
|
16
87
|
<slot>{{ message }}</slot>
|
|
17
88
|
</div>
|
|
89
|
+
<button
|
|
90
|
+
v-if="clampEnabled && isOverflowing"
|
|
91
|
+
type="button"
|
|
92
|
+
:aria-expanded="expanded"
|
|
93
|
+
class="ck-user-message__toggle label-12-medium text-content-on-surface-primary bg-background-surface-floating focus-visible:ring-ring self-center rounded-3xl px-3 py-2 focus-visible:ring-2 focus-visible:outline-none"
|
|
94
|
+
@click="toggle"
|
|
95
|
+
>
|
|
96
|
+
{{ expanded ? showLessLabel : showMoreLabel }}
|
|
97
|
+
</button>
|
|
18
98
|
</div>
|
|
19
99
|
</template>
|
|
100
|
+
|
|
101
|
+
<style scoped>
|
|
102
|
+
/* Floating pill per design — the inset white + soft drop shadow aren't
|
|
103
|
+
expressible with the design-system shadow utilities, so set them here. */
|
|
104
|
+
.ck-user-message__toggle {
|
|
105
|
+
box-shadow:
|
|
106
|
+
0 2px 2px 0 rgba(0, 0, 0, 0.05),
|
|
107
|
+
0 0 4px 0 #fff inset;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* text-lg has a 1.75rem (28px) line-height; clamp height = lines × line-height. */
|
|
111
|
+
.ck-user-message__content--clamped {
|
|
112
|
+
max-height: calc(var(--ck-collapsed-lines) * 1.75rem);
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Fade the last visible line to low opacity. */
|
|
117
|
+
.ck-user-message__content--faded {
|
|
118
|
+
-webkit-mask-image: linear-gradient(
|
|
119
|
+
to bottom,
|
|
120
|
+
#000 calc(100% - 1.75rem),
|
|
121
|
+
transparent 100%
|
|
122
|
+
);
|
|
123
|
+
mask-image: linear-gradient(
|
|
124
|
+
to bottom,
|
|
125
|
+
#000 calc(100% - 1.75rem),
|
|
126
|
+
transparent 100%
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Layout for a single user turn in a conversation. Renders the message text
|
|
3
|
+
* right-aligned in the muted on-surface color with a max-width; supports a
|
|
4
|
+
* default slot for custom inline content. When `collapsedLines` is set, long
|
|
5
|
+
* messages are clamped with a fade and a "Show more" / "Show less" toggle.
|
|
5
6
|
*
|
|
6
7
|
* @category Messages
|
|
7
|
-
* @useCases render user message
|
|
8
|
-
* @keywords user, message,
|
|
8
|
+
* @useCases render user message, conversation turn from user, chat input preview, right-aligned message, collapsible long message
|
|
9
|
+
* @keywords user, message, chat, conversation, turn, collapse, show more
|
|
9
10
|
* @related CkAgentMessage, CkConversation
|
|
10
11
|
*/
|
|
11
12
|
export interface CkUserMessageProps {
|
|
12
13
|
/** Text content of the user message. Rendered inside the bubble when no default slot is provided. */
|
|
13
14
|
message?: string;
|
|
15
|
+
/**
|
|
16
|
+
* When greater than `0`, collapse the message to at most this many lines.
|
|
17
|
+
* The last visible line fades out and a "Show more" / "Show less" toggle is
|
|
18
|
+
* revealed only when the content actually overflows. Omit or pass `0`
|
|
19
|
+
* (the default) to render the full message with no clamping — preserving the
|
|
20
|
+
* original behavior for existing consumers.
|
|
21
|
+
*/
|
|
22
|
+
collapsedLines?: number;
|
|
14
23
|
}
|