@aleph-alpha/chat-kit 2.3.0 → 3.1.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-DYTizPsB.js → CkInput.vue_vue_type_script_setup_true_lang-DKzc21Yp.js} +239 -154
- package/dist/chat-kit.css +33 -6
- package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
- package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
- package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/defaultMarkdownClasses.d.ts.map +1 -1
- package/dist/components/base/CkModelSelector/CkModelSelector.vue.d.ts.map +1 -1
- package/dist/components/base/CkModelSelector/types.d.ts +1 -1
- package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +48 -5
- package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts.map +1 -1
- package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts +7 -6
- package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptInput/types.d.ts +11 -12
- package/dist/components/base/CkPromptInput/types.d.ts.map +1 -1
- package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.d.ts +18 -0
- package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.d.ts.map +1 -0
- package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.vue.d.ts +18 -0
- package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkPromptMenuButton/index.d.ts +3 -0
- package/dist/components/base/CkPromptMenuButton/index.d.ts.map +1 -0
- package/dist/components/base/CkPromptMenuButton/types.d.ts +30 -0
- package/dist/components/base/CkPromptMenuButton/types.d.ts.map +1 -0
- package/dist/components/base/{CkPromptActionButton/CkPromptActionButton.stories.d.ts → CkPromptSendButton/CkPromptSendButton.stories.d.ts} +5 -5
- package/dist/components/base/CkPromptSendButton/CkPromptSendButton.stories.d.ts.map +1 -0
- package/dist/components/base/{CkPromptActionButton/CkPromptActionButton.vue.d.ts → CkPromptSendButton/CkPromptSendButton.vue.d.ts} +3 -3
- package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts.map +1 -0
- package/dist/components/base/CkPromptSendButton/index.d.ts +3 -0
- package/dist/components/base/CkPromptSendButton/index.d.ts.map +1 -0
- package/dist/components/base/CkPromptSendButton/types.d.ts +34 -0
- package/dist/components/base/CkPromptSendButton/types.d.ts.map +1 -0
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts +1 -1
- package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
- package/dist/components/base/CkPromptStopButton/types.d.ts +6 -4
- package/dist/components/base/CkPromptStopButton/types.d.ts.map +1 -1
- package/dist/components/base/icons/CkSendIcon.vue.d.ts.map +1 -1
- package/dist/components/base/index.d.ts +3 -1
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +1 -1
- package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +1 -1
- package/dist/components/composed/CkInput/CkInput.vue.d.ts +9 -8
- package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +1 -1
- package/dist/components/composed/CkInput/types.d.ts +2 -7
- package/dist/components/composed/CkInput/types.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 +9 -8
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +6 -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 +11 -10
- package/dist/{useChatKitLabels-ZutacPad.js → useChatKitLabels-CLsWWwgG.js} +1 -0
- package/package.json +1 -1
- package/src/components/base/CkAgentMessage/CkAgentMessage.vue +2 -2
- package/src/components/base/CkConversationLayout/CkConversationLayout.vue +3 -2
- package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue +2 -7
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +36 -4
- package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +2 -1
- package/src/components/base/CkModelSelector/CkModelSelector.vue +8 -3
- package/src/components/base/CkModelSelector/types.ts +1 -1
- package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +206 -11
- package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +1068 -43
- package/src/components/base/CkPromptInput/CkPromptInput.vue +137 -28
- package/src/components/base/CkPromptInput/types.ts +11 -12
- package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.spec.ts +62 -0
- package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.ts +75 -0
- package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.vue +26 -0
- package/src/components/base/CkPromptMenuButton/index.ts +2 -0
- package/src/components/base/CkPromptMenuButton/types.ts +29 -0
- package/src/components/base/{CkPromptActionButton/CkPromptActionButton.spec.ts → CkPromptSendButton/CkPromptSendButton.spec.ts} +11 -11
- package/src/components/base/CkPromptSendButton/CkPromptSendButton.stories.ts +117 -0
- package/src/components/base/{CkPromptActionButton/CkPromptActionButton.vue → CkPromptSendButton/CkPromptSendButton.vue} +4 -4
- package/src/components/base/CkPromptSendButton/index.ts +2 -0
- package/src/components/base/CkPromptSendButton/types.ts +33 -0
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +5 -5
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +1 -1
- package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +3 -2
- package/src/components/base/CkPromptStopButton/types.ts +6 -4
- package/src/components/base/icons/CkSendIcon.vue +3 -16
- package/src/components/base/icons/CkStopIcon.vue +1 -1
- package/src/components/base/index.ts +3 -1
- package/src/components/composed/CkInput/CkInput.stories.ts +0 -4
- package/src/components/composed/CkInput/CkInput.vue +7 -5
- package/src/components/composed/CkInput/types.ts +2 -7
- package/src/components/index.ts +3 -1
- package/src/handlers/useChatKitLabels.ts +7 -0
- package/src/index.ts +3 -1
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts.map +0 -1
- package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts.map +0 -1
- package/dist/components/base/CkPromptActionButton/index.d.ts +0 -3
- package/dist/components/base/CkPromptActionButton/index.d.ts.map +0 -1
- package/dist/components/base/CkPromptActionButton/types.d.ts +0 -33
- package/dist/components/base/CkPromptActionButton/types.d.ts.map +0 -1
- package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +0 -126
- package/src/components/base/CkPromptActionButton/index.ts +0 -2
- package/src/components/base/CkPromptActionButton/types.ts +0 -32
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CkPromptInputProps } from './types';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
nextTick,
|
|
6
|
+
onMounted,
|
|
7
|
+
onUpdated,
|
|
8
|
+
ref,
|
|
9
|
+
useId,
|
|
10
|
+
useSlots,
|
|
11
|
+
useTemplateRef,
|
|
12
|
+
watch,
|
|
13
|
+
} from 'vue';
|
|
4
14
|
|
|
5
15
|
defineOptions({ name: 'CkPromptInput' });
|
|
6
16
|
|
|
17
|
+
// Pixel height of one rendered line — must match the textarea's `leading-6`
|
|
18
|
+
// (24 px). Used to derive how many lines the current content occupies.
|
|
19
|
+
const LINE_HEIGHT = 24;
|
|
20
|
+
|
|
21
|
+
// Cap the textarea at 6 visible lines; content beyond that scrolls inside.
|
|
22
|
+
const MAX_LINES = 6;
|
|
23
|
+
const MAX_HEIGHT = LINE_HEIGHT * MAX_LINES;
|
|
24
|
+
|
|
7
25
|
const props = withDefaults(defineProps<CkPromptInputProps>(), {
|
|
8
26
|
modelValue: '',
|
|
9
27
|
placeholder: undefined,
|
|
10
28
|
disabled: false,
|
|
11
|
-
maxHeight: 200,
|
|
12
29
|
ariaLabel: undefined,
|
|
13
30
|
});
|
|
14
31
|
|
|
@@ -19,21 +36,74 @@ const emit = defineEmits<{
|
|
|
19
36
|
|
|
20
37
|
const textareaId = useId();
|
|
21
38
|
const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef');
|
|
39
|
+
const slots = useSlots();
|
|
40
|
+
|
|
41
|
+
// Latches to `true` once content spans 2+ lines (newline OR soft-wrap) and
|
|
42
|
+
// stays there until the field is emptied. Without the latch, switching to the
|
|
43
|
+
// wider two-row layout would let the text fit on one line again, flip us
|
|
44
|
+
// back, and re-wrap — an infinite toggle loop.
|
|
45
|
+
const isMultiLine = ref(false);
|
|
22
46
|
|
|
23
47
|
const isEmpty = computed(() => props.modelValue.trim() === '');
|
|
24
48
|
const isSubmitDisabled = computed(() => props.disabled || isEmpty.value);
|
|
49
|
+
// `useSlots()` is reactive for *static* slot provision (the parent always
|
|
50
|
+
// passes `#toolbar-top` and merely changes its inner content) but NOT when
|
|
51
|
+
// the parent flips slot presence via `<template v-if="…" #toolbar-top>`.
|
|
52
|
+
// That parent-side v-if is our actual use case (file-pill row appears once
|
|
53
|
+
// `files.length > 0`), so sync the boolean via the post-render hook —
|
|
54
|
+
// onUpdated fires after every parent re-render, after slots are reconciled.
|
|
55
|
+
const hasToolbarTop = ref(typeof slots['toolbar-top'] === 'function');
|
|
56
|
+
function syncSlotState(): void {
|
|
57
|
+
const next = typeof slots['toolbar-top'] === 'function';
|
|
58
|
+
if (next !== hasToolbarTop.value) hasToolbarTop.value = next;
|
|
59
|
+
}
|
|
60
|
+
onMounted(syncSlotState);
|
|
61
|
+
onUpdated(syncSlotState);
|
|
62
|
+
|
|
63
|
+
// Compact form uses the larger 48 px corner radius; once a second row appears
|
|
64
|
+
// (multi-line text OR toolbar-top filled with chips/attachments), the radius
|
|
65
|
+
// drops to 24 px per Solea spec.
|
|
66
|
+
const hasMultipleRows = computed(
|
|
67
|
+
() => isMultiLine.value || hasToolbarTop.value,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const gridAreas = computed(() => {
|
|
71
|
+
const top = hasToolbarTop.value ? '"top top top" ' : '';
|
|
72
|
+
return isMultiLine.value
|
|
73
|
+
? `${top}"input input input" "left . right"`
|
|
74
|
+
: `${top}"left input right"`;
|
|
75
|
+
});
|
|
25
76
|
|
|
26
77
|
function adjustHeight(): void {
|
|
27
78
|
const el = textareaRef.value;
|
|
28
79
|
if (!el) return;
|
|
80
|
+
|
|
81
|
+
if (props.modelValue === '') {
|
|
82
|
+
isMultiLine.value = false;
|
|
83
|
+
el.style.height = '';
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Collapse to natural content height first so the next scrollHeight read
|
|
88
|
+
// reflects what the content needs, not what the inline height is forcing.
|
|
29
89
|
el.style.height = 'auto';
|
|
30
|
-
|
|
90
|
+
const measured = el.scrollHeight;
|
|
91
|
+
el.style.height = `${Math.min(measured, MAX_HEIGHT)}px`;
|
|
92
|
+
|
|
93
|
+
// Latch only on a *real* second line: either explicit newline OR scrollHeight
|
|
94
|
+
// ≥ 2 × line-height. Using `Math.round` on a borderline 36 px (sub-pixel
|
|
95
|
+
// emoji/CJK rendering on Chromium/Linux) would falsely latch a single line.
|
|
96
|
+
const hasExplicitNewline = props.modelValue.includes('\n');
|
|
97
|
+
if (
|
|
98
|
+
!isMultiLine.value &&
|
|
99
|
+
(hasExplicitNewline || measured >= LINE_HEIGHT * 2)
|
|
100
|
+
) {
|
|
101
|
+
isMultiLine.value = true;
|
|
102
|
+
}
|
|
31
103
|
}
|
|
32
104
|
|
|
33
105
|
function onInput(event: Event): void {
|
|
34
|
-
|
|
35
|
-
emit('update:modelValue', value);
|
|
36
|
-
adjustHeight();
|
|
106
|
+
emit('update:modelValue', (event.target as HTMLTextAreaElement).value);
|
|
37
107
|
}
|
|
38
108
|
|
|
39
109
|
function submit(): void {
|
|
@@ -56,10 +126,9 @@ function focus(): void {
|
|
|
56
126
|
|
|
57
127
|
watch(
|
|
58
128
|
() => props.modelValue,
|
|
59
|
-
() =>
|
|
60
|
-
nextTick(adjustHeight);
|
|
61
|
-
},
|
|
129
|
+
() => nextTick(adjustHeight),
|
|
62
130
|
);
|
|
131
|
+
onMounted(adjustHeight);
|
|
63
132
|
|
|
64
133
|
defineExpose({ focus, submit });
|
|
65
134
|
</script>
|
|
@@ -67,38 +136,78 @@ defineExpose({ focus, submit });
|
|
|
67
136
|
<template>
|
|
68
137
|
<div class="flex flex-col gap-2">
|
|
69
138
|
<form
|
|
70
|
-
class="
|
|
139
|
+
:class="[
|
|
140
|
+
'ck-prompt-form border-border-surface-default bg-background-input-strong [&:has(textarea:focus)]:border-border-surface-focus-ring [&:has(textarea:focus)]:ring-border-surface-focus-ring/40 min-h-17 grid items-center gap-x-0 gap-y-5 border p-4 transition-[border-radius] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] [&:has(textarea:focus)]:ring-2',
|
|
141
|
+
hasMultipleRows ? 'rounded-2xl' : 'rounded-3xl',
|
|
142
|
+
]"
|
|
143
|
+
:style="{
|
|
144
|
+
gridTemplateColumns: 'auto 1fr auto',
|
|
145
|
+
gridTemplateAreas: gridAreas,
|
|
146
|
+
}"
|
|
71
147
|
@submit.prevent="submit"
|
|
72
148
|
>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
149
|
+
<div v-if="hasToolbarTop" style="grid-area: top">
|
|
150
|
+
<slot name="toolbar-top" />
|
|
151
|
+
</div>
|
|
152
|
+
<label v-if="ariaLabel" :for="textareaId" class="sr-only">
|
|
153
|
+
{{ ariaLabel }}
|
|
154
|
+
</label>
|
|
155
|
+
<div :class="[!isMultiLine && 'pr-2']" style="grid-area: left">
|
|
156
|
+
<slot name="toolbar-left" />
|
|
157
|
+
</div>
|
|
76
158
|
<textarea
|
|
77
159
|
:id="textareaId"
|
|
78
160
|
ref="textareaRef"
|
|
79
|
-
rows="
|
|
161
|
+
rows="1"
|
|
80
162
|
:value="modelValue"
|
|
81
163
|
:placeholder="placeholder"
|
|
82
164
|
:disabled="disabled"
|
|
83
|
-
|
|
165
|
+
style="grid-area: input"
|
|
166
|
+
:class="[
|
|
167
|
+
'ck-prompt-textarea text-content-on-surface-primary placeholder-content-on-surface-muted w-full min-w-0 resize-none overflow-y-auto bg-transparent py-0 font-sans text-base font-normal leading-6 max-h-36 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60',
|
|
168
|
+
isMultiLine ? 'pl-3 pr-3' : 'px-0',
|
|
169
|
+
]"
|
|
84
170
|
@input="onInput"
|
|
85
171
|
@keydown="onKeydown"
|
|
86
172
|
/>
|
|
87
|
-
<div class="
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
name="toolbar-right"
|
|
95
|
-
:submit="submit"
|
|
96
|
-
:disabled="isSubmitDisabled"
|
|
97
|
-
:is-empty="isEmpty"
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
173
|
+
<div :class="[!isMultiLine && 'pl-4']" style="grid-area: right">
|
|
174
|
+
<slot
|
|
175
|
+
name="toolbar-right"
|
|
176
|
+
:submit="submit"
|
|
177
|
+
:disabled="isSubmitDisabled"
|
|
178
|
+
:is-empty="isEmpty"
|
|
179
|
+
/>
|
|
100
180
|
</div>
|
|
101
181
|
</form>
|
|
102
182
|
<slot name="footer" />
|
|
103
183
|
</div>
|
|
104
184
|
</template>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
/* Solea two-layer drop shadow — warm grey + subtle orange tint. Matches the
|
|
188
|
+
`shadow/md/{1,2}` tokens from the design system; the generic `shadow-md`
|
|
189
|
+
utility from preset-wind4 uses a cold black-based shadow instead. */
|
|
190
|
+
.ck-prompt-form {
|
|
191
|
+
box-shadow:
|
|
192
|
+
0 4px 6px -2px rgba(112, 95, 90, 0.1),
|
|
193
|
+
0 6px 14px -2px rgba(248, 94, 43, 0.05);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Slim styled scrollbar for the multi-line textarea — matches Figma's 4 px
|
|
197
|
+
muted-default track. Firefox uses the native `scrollbar-*` properties;
|
|
198
|
+
Chromium/WebKit needs the `::-webkit-scrollbar` pseudo-elements. */
|
|
199
|
+
.ck-prompt-textarea {
|
|
200
|
+
scrollbar-width: thin;
|
|
201
|
+
scrollbar-color: rgba(166, 141, 133, 0.3) transparent;
|
|
202
|
+
}
|
|
203
|
+
.ck-prompt-textarea::-webkit-scrollbar {
|
|
204
|
+
width: 4px;
|
|
205
|
+
}
|
|
206
|
+
.ck-prompt-textarea::-webkit-scrollbar-track {
|
|
207
|
+
background: transparent;
|
|
208
|
+
}
|
|
209
|
+
.ck-prompt-textarea::-webkit-scrollbar-thumb {
|
|
210
|
+
background: rgba(166, 141, 133, 0.3);
|
|
211
|
+
border-radius: 9999px;
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Low-level prompt input:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Low-level prompt input: an auto-growing textarea (1–6 lines, then internal
|
|
3
|
+
* scroll) with `toolbar-top` / `toolbar-left` / `toolbar-right` / `footer`
|
|
4
|
+
* slots and keyboard submission (Enter to submit, Shift+Enter for newline).
|
|
5
|
+
* The form layout adapts between a compact single-row variant (large 3xl
|
|
6
|
+
* radius) and a stacked multi-row variant (2xl radius) automatically — driven
|
|
7
|
+
* by content height and whether `toolbar-top` is filled. Use when you need
|
|
8
|
+
* full control over the prompt layout; for a turnkey chat input use `CkInput`
|
|
9
|
+
* instead.
|
|
6
10
|
*
|
|
7
11
|
* @category Form Inputs
|
|
8
|
-
* @useCases raw chat prompt textarea, custom prompt toolbar, auto-growing
|
|
9
|
-
* @keywords prompt, input, textarea, chat,
|
|
10
|
-
* @related CkInput,
|
|
12
|
+
* @useCases raw chat prompt textarea, custom prompt toolbar, auto-growing compose box, file-attachment chip row, multi-line chat input, keyboard submit
|
|
13
|
+
* @keywords prompt, input, textarea, chat, compose, submit, v-model, auto-grow, multi-line, toolbar
|
|
14
|
+
* @related CkInput, CkPromptSendButton, CkPromptStopButton, CkPromptMenuButton, CkPromptInputDisclaimer, CkFileItemsList
|
|
11
15
|
*/
|
|
12
16
|
export interface CkPromptInputProps {
|
|
13
17
|
/**
|
|
@@ -22,11 +26,6 @@ export interface CkPromptInputProps {
|
|
|
22
26
|
* @default false
|
|
23
27
|
*/
|
|
24
28
|
disabled?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Maximum height (in px) before the textarea becomes scrollable.
|
|
27
|
-
* @default 200
|
|
28
|
-
*/
|
|
29
|
-
maxHeight?: number;
|
|
30
29
|
/** Accessible label for the textarea, hidden visually but read by assistive tech. */
|
|
31
30
|
ariaLabel?: string;
|
|
32
31
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import CkPromptMenuButton from './CkPromptMenuButton.vue';
|
|
2
|
+
import { render, screen } from '@testing-library/vue';
|
|
3
|
+
|
|
4
|
+
describe('CkPromptMenuButton', () => {
|
|
5
|
+
it('renders a default plus icon when the slot is empty', () => {
|
|
6
|
+
const { container } = render(CkPromptMenuButton, {
|
|
7
|
+
props: { ariaLabel: 'Open prompt menu' },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// Lucide's icon component renders an <svg> — assert at least one is present.
|
|
11
|
+
expect(container.querySelector('svg')).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('renders icon slot content when provided', () => {
|
|
15
|
+
render(CkPromptMenuButton, {
|
|
16
|
+
props: { ariaLabel: 'Open prompt menu' },
|
|
17
|
+
slots: { default: '<svg data-testid="icon"></svg>' },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(screen.getByTestId('icon')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('passes ariaLabel to the underlying button', () => {
|
|
24
|
+
render(CkPromptMenuButton, {
|
|
25
|
+
props: { ariaLabel: 'Open prompt menu' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(
|
|
29
|
+
screen.getByRole('button', { name: 'Open prompt menu' }),
|
|
30
|
+
).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('passes disabled state to the underlying button', () => {
|
|
34
|
+
render(CkPromptMenuButton, {
|
|
35
|
+
props: { ariaLabel: 'Open prompt menu', disabled: true },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(
|
|
39
|
+
screen.getByRole('button', { name: 'Open prompt menu' }),
|
|
40
|
+
).toBeDisabled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('defaults type to button', () => {
|
|
44
|
+
render(CkPromptMenuButton, {
|
|
45
|
+
props: { ariaLabel: 'Open prompt menu' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(
|
|
49
|
+
screen.getByRole('button', { name: 'Open prompt menu' }),
|
|
50
|
+
).toHaveAttribute('type', 'button');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders as a pill (rounded-full)', () => {
|
|
54
|
+
render(CkPromptMenuButton, {
|
|
55
|
+
props: { ariaLabel: 'Open prompt menu' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(
|
|
59
|
+
screen.getByRole('button', { name: 'Open prompt menu' }).className,
|
|
60
|
+
).toContain('!rounded-full');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import CkPromptMenuButton from './CkPromptMenuButton.vue';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof CkPromptMenuButton> = {
|
|
5
|
+
title: 'Base/CkPromptMenuButton',
|
|
6
|
+
component: CkPromptMenuButton,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
ariaLabel: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
description:
|
|
12
|
+
'Accessible label for the button. Required because the button is icon-only.',
|
|
13
|
+
},
|
|
14
|
+
disabled: {
|
|
15
|
+
control: 'boolean',
|
|
16
|
+
description: 'Whether the button is disabled.',
|
|
17
|
+
},
|
|
18
|
+
type: {
|
|
19
|
+
control: 'radio',
|
|
20
|
+
options: ['button', 'submit'],
|
|
21
|
+
description: 'Native button type. Use `submit` inside a <form>.',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
() => ({
|
|
26
|
+
template:
|
|
27
|
+
'<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof meta>;
|
|
34
|
+
|
|
35
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
36
|
+
import { CkPromptMenuButton } from '@aleph-alpha/chat-kit';
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<CkPromptMenuButton aria-label="Open prompt menu" />
|
|
41
|
+
</template>`;
|
|
42
|
+
|
|
43
|
+
const disabledTemplateSource = `<script setup lang="ts">
|
|
44
|
+
import { CkPromptMenuButton } from '@aleph-alpha/chat-kit';
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<CkPromptMenuButton aria-label="Open prompt menu" disabled />
|
|
49
|
+
</template>`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default menu-trigger button — ghost variant with the built-in plus icon.
|
|
53
|
+
* Drop into a `CkPromptInput` left toolbar slot, wrap with `UiDropdownMenu`
|
|
54
|
+
* to expose the actual prompt actions (attach files, attach folders, toggle
|
|
55
|
+
* tools, etc.).
|
|
56
|
+
*/
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
render: () => ({
|
|
59
|
+
components: { CkPromptMenuButton },
|
|
60
|
+
template: `<CkPromptMenuButton aria-label="Open prompt menu" />`,
|
|
61
|
+
}),
|
|
62
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Disabled state — use while the menu's actions are unavailable (e.g. while
|
|
67
|
+
* the previous request is still in flight).
|
|
68
|
+
*/
|
|
69
|
+
export const Disabled: Story = {
|
|
70
|
+
render: () => ({
|
|
71
|
+
components: { CkPromptMenuButton },
|
|
72
|
+
template: `<CkPromptMenuButton aria-label="Open prompt menu" disabled />`,
|
|
73
|
+
}),
|
|
74
|
+
parameters: { docs: { source: { code: disabledTemplateSource } } },
|
|
75
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CkPromptMenuButtonProps } from './types';
|
|
3
|
+
import { UiButton, UiIcon } from '@aleph-alpha/ui-library';
|
|
4
|
+
|
|
5
|
+
defineOptions({ name: 'CkPromptMenuButton' });
|
|
6
|
+
|
|
7
|
+
withDefaults(defineProps<CkPromptMenuButtonProps>(), {
|
|
8
|
+
disabled: false,
|
|
9
|
+
type: 'button',
|
|
10
|
+
});
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<UiButton
|
|
15
|
+
v-bind="{ ariaLabel }"
|
|
16
|
+
variant="ghost"
|
|
17
|
+
size="icon"
|
|
18
|
+
:type="type"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
class="!rounded-full"
|
|
21
|
+
>
|
|
22
|
+
<slot>
|
|
23
|
+
<UiIcon name="plus" :size="20" />
|
|
24
|
+
</slot>
|
|
25
|
+
</UiButton>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon-only menu trigger styled for the chat prompt area (ghost variant,
|
|
3
|
+
* pill-shaped). Sits in `CkPromptInput`'s `toolbar-left` slot as the entry
|
|
4
|
+
* point to a dropdown of prompt actions — attach files, attach folders,
|
|
5
|
+
* toggle tools, switch length, etc. Defaults to a `plus` icon when the slot
|
|
6
|
+
* is empty; pass any icon to override.
|
|
7
|
+
*
|
|
8
|
+
* @category Actions
|
|
9
|
+
* @useCases open prompt menu, attach files, add resources, toggle tools, prompt input toolbar
|
|
10
|
+
* @keywords prompt, menu, plus, add, button, icon, ghost, dropdown trigger, tools
|
|
11
|
+
* @related CkPromptInput, CkPromptSendButton, CkPromptStopButton, CkDropZoneOverlay, CkFileItemsList
|
|
12
|
+
*/
|
|
13
|
+
export interface CkPromptMenuButtonProps {
|
|
14
|
+
/**
|
|
15
|
+
* Accessible label for the button. **Required** because the button only
|
|
16
|
+
* renders an icon.
|
|
17
|
+
*/
|
|
18
|
+
ariaLabel: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the button is disabled.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Native button `type`.
|
|
26
|
+
* @default 'button'
|
|
27
|
+
*/
|
|
28
|
+
type?: 'button' | 'submit';
|
|
29
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import CkPromptSendButton from './CkPromptSendButton.vue';
|
|
2
2
|
import { render, screen } from '@testing-library/vue';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('CkPromptSendButton', () => {
|
|
5
5
|
it('renders icon slot content', () => {
|
|
6
|
-
render(
|
|
6
|
+
render(CkPromptSendButton, {
|
|
7
7
|
props: { ariaLabel: 'Send' },
|
|
8
8
|
slots: { default: '<svg data-testid="icon"></svg>' },
|
|
9
9
|
});
|
|
@@ -12,7 +12,7 @@ describe('CkPromptActionButton', () => {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('passes ariaLabel to the underlying button', () => {
|
|
15
|
-
render(
|
|
15
|
+
render(CkPromptSendButton, {
|
|
16
16
|
props: { ariaLabel: 'Send message' },
|
|
17
17
|
});
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ describe('CkPromptActionButton', () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('passes disabled state to the underlying button', () => {
|
|
25
|
-
render(
|
|
25
|
+
render(CkPromptSendButton, {
|
|
26
26
|
props: { ariaLabel: 'Send', disabled: true },
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ describe('CkPromptActionButton', () => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it('defaults type to button', () => {
|
|
33
|
-
render(
|
|
33
|
+
render(CkPromptSendButton, {
|
|
34
34
|
props: { ariaLabel: 'Send' },
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -40,13 +40,13 @@ describe('CkPromptActionButton', () => {
|
|
|
40
40
|
);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('
|
|
44
|
-
render(
|
|
43
|
+
it('renders as a pill (rounded-full)', () => {
|
|
44
|
+
render(CkPromptSendButton, {
|
|
45
45
|
props: { ariaLabel: 'Send' },
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
expect(screen.getByRole('button', { name: 'Send' }).className).toContain(
|
|
49
|
+
'!rounded-full',
|
|
50
|
+
);
|
|
51
51
|
});
|
|
52
52
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import CkPromptSendButton from './CkPromptSendButton.vue';
|
|
2
|
+
import CkSendIcon from '../icons/CkSendIcon.vue';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof CkPromptSendButton> = {
|
|
6
|
+
title: 'Base/CkPromptSendButton',
|
|
7
|
+
component: CkPromptSendButton,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
ariaLabel: {
|
|
11
|
+
control: 'text',
|
|
12
|
+
description:
|
|
13
|
+
'Accessible label for the button. Required because the button is icon-only.',
|
|
14
|
+
},
|
|
15
|
+
disabled: {
|
|
16
|
+
control: 'boolean',
|
|
17
|
+
description: 'Whether the button is disabled.',
|
|
18
|
+
},
|
|
19
|
+
loading: {
|
|
20
|
+
control: 'boolean',
|
|
21
|
+
description:
|
|
22
|
+
'Shows a spinner instead of the slotted icon and blocks clicks.',
|
|
23
|
+
},
|
|
24
|
+
type: {
|
|
25
|
+
control: 'radio',
|
|
26
|
+
options: ['button', 'submit'],
|
|
27
|
+
description: 'Native button type. Use `submit` inside a <form>.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
decorators: [
|
|
31
|
+
() => ({
|
|
32
|
+
template:
|
|
33
|
+
'<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof meta>;
|
|
40
|
+
|
|
41
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
42
|
+
import { CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<CkPromptSendButton aria-label="Send message">
|
|
47
|
+
<CkSendIcon />
|
|
48
|
+
</CkPromptSendButton>
|
|
49
|
+
</template>`;
|
|
50
|
+
|
|
51
|
+
const disabledTemplateSource = `<script setup lang="ts">
|
|
52
|
+
import { CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<CkPromptSendButton aria-label="Send message" disabled>
|
|
57
|
+
<CkSendIcon />
|
|
58
|
+
</CkPromptSendButton>
|
|
59
|
+
</template>`;
|
|
60
|
+
|
|
61
|
+
const loadingTemplateSource = `<script setup lang="ts">
|
|
62
|
+
import { CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<CkPromptSendButton aria-label="Send message" loading>
|
|
67
|
+
<CkSendIcon />
|
|
68
|
+
</CkPromptSendButton>
|
|
69
|
+
</template>`;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default send button. Drop into a `CkPromptInput`'s `toolbar-right` slot to
|
|
73
|
+
* submit the prompt.
|
|
74
|
+
*/
|
|
75
|
+
export const Default: Story = {
|
|
76
|
+
render: () => ({
|
|
77
|
+
components: { CkPromptSendButton, CkSendIcon },
|
|
78
|
+
template: `
|
|
79
|
+
<CkPromptSendButton aria-label="Send message">
|
|
80
|
+
<CkSendIcon />
|
|
81
|
+
</CkPromptSendButton>
|
|
82
|
+
`,
|
|
83
|
+
}),
|
|
84
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Disabled state — use when the input is empty or a submit is otherwise
|
|
89
|
+
* blocked (e.g. rate-limited).
|
|
90
|
+
*/
|
|
91
|
+
export const Disabled: Story = {
|
|
92
|
+
render: () => ({
|
|
93
|
+
components: { CkPromptSendButton, CkSendIcon },
|
|
94
|
+
template: `
|
|
95
|
+
<CkPromptSendButton aria-label="Send message" disabled>
|
|
96
|
+
<CkSendIcon />
|
|
97
|
+
</CkPromptSendButton>
|
|
98
|
+
`,
|
|
99
|
+
}),
|
|
100
|
+
parameters: { docs: { source: { code: disabledTemplateSource } } },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Loading state — shows a spinner and blocks clicks. Use while the previous
|
|
105
|
+
* message is still streaming or the request is in flight.
|
|
106
|
+
*/
|
|
107
|
+
export const Loading: Story = {
|
|
108
|
+
render: () => ({
|
|
109
|
+
components: { CkPromptSendButton, CkSendIcon },
|
|
110
|
+
template: `
|
|
111
|
+
<CkPromptSendButton aria-label="Send message" loading>
|
|
112
|
+
<CkSendIcon />
|
|
113
|
+
</CkPromptSendButton>
|
|
114
|
+
`,
|
|
115
|
+
}),
|
|
116
|
+
parameters: { docs: { source: { code: loadingTemplateSource } } },
|
|
117
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { CkPromptSendButtonProps } from './types';
|
|
3
3
|
import { UiButton } from '@aleph-alpha/ui-library';
|
|
4
4
|
|
|
5
|
-
defineOptions({ name: '
|
|
5
|
+
defineOptions({ name: 'CkPromptSendButton' });
|
|
6
6
|
|
|
7
|
-
withDefaults(defineProps<
|
|
7
|
+
withDefaults(defineProps<CkPromptSendButtonProps>(), {
|
|
8
8
|
disabled: false,
|
|
9
9
|
loading: false,
|
|
10
10
|
type: 'button',
|
|
@@ -18,7 +18,7 @@ withDefaults(defineProps<CkPromptActionButtonProps>(), {
|
|
|
18
18
|
:type="type"
|
|
19
19
|
:disabled="disabled"
|
|
20
20
|
:loading="loading"
|
|
21
|
-
class="!
|
|
21
|
+
class="!rounded-full"
|
|
22
22
|
>
|
|
23
23
|
<slot />
|
|
24
24
|
</UiButton>
|