@aleph-alpha/chat-kit 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DYTizPsB.js → CkInput.vue_vue_type_script_setup_true_lang-CwH5M5WD.js} +203 -141
  2. package/dist/chat-kit.css +33 -6
  3. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts.map +1 -1
  4. package/dist/components/base/CkModelSelector/types.d.ts +1 -1
  5. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +48 -5
  6. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts.map +1 -1
  7. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts +7 -6
  8. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
  9. package/dist/components/base/CkPromptInput/types.d.ts +11 -12
  10. package/dist/components/base/CkPromptInput/types.d.ts.map +1 -1
  11. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.d.ts +18 -0
  12. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.d.ts.map +1 -0
  13. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.vue.d.ts +18 -0
  14. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.vue.d.ts.map +1 -0
  15. package/dist/components/base/CkPromptMenuButton/index.d.ts +3 -0
  16. package/dist/components/base/CkPromptMenuButton/index.d.ts.map +1 -0
  17. package/dist/components/base/CkPromptMenuButton/types.d.ts +30 -0
  18. package/dist/components/base/CkPromptMenuButton/types.d.ts.map +1 -0
  19. package/dist/components/base/{CkPromptActionButton/CkPromptActionButton.stories.d.ts → CkPromptSendButton/CkPromptSendButton.stories.d.ts} +5 -5
  20. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.stories.d.ts.map +1 -0
  21. package/dist/components/base/{CkPromptActionButton/CkPromptActionButton.vue.d.ts → CkPromptSendButton/CkPromptSendButton.vue.d.ts} +3 -3
  22. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts.map +1 -0
  23. package/dist/components/base/CkPromptSendButton/index.d.ts +3 -0
  24. package/dist/components/base/CkPromptSendButton/index.d.ts.map +1 -0
  25. package/dist/components/base/CkPromptSendButton/types.d.ts +34 -0
  26. package/dist/components/base/CkPromptSendButton/types.d.ts.map +1 -0
  27. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts +1 -1
  28. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
  29. package/dist/components/base/CkPromptStopButton/types.d.ts +6 -4
  30. package/dist/components/base/CkPromptStopButton/types.d.ts.map +1 -1
  31. package/dist/components/base/icons/CkSendIcon.vue.d.ts.map +1 -1
  32. package/dist/components/base/index.d.ts +3 -1
  33. package/dist/components/base/index.d.ts.map +1 -1
  34. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +1 -1
  35. package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +1 -1
  36. package/dist/components/composed/CkInput/CkInput.vue.d.ts +9 -8
  37. package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +1 -1
  38. package/dist/components/composed/CkInput/types.d.ts +2 -7
  39. package/dist/components/composed/CkInput/types.d.ts.map +1 -1
  40. package/dist/components/index.d.ts +2 -2
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +9 -8
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +10 -9
  46. package/package.json +1 -1
  47. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue +2 -7
  48. package/src/components/base/CkModelSelector/types.ts +1 -1
  49. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +206 -11
  50. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +1068 -43
  51. package/src/components/base/CkPromptInput/CkPromptInput.vue +137 -28
  52. package/src/components/base/CkPromptInput/types.ts +11 -12
  53. package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.spec.ts +62 -0
  54. package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.ts +75 -0
  55. package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.vue +26 -0
  56. package/src/components/base/CkPromptMenuButton/index.ts +2 -0
  57. package/src/components/base/CkPromptMenuButton/types.ts +29 -0
  58. package/src/components/base/{CkPromptActionButton/CkPromptActionButton.spec.ts → CkPromptSendButton/CkPromptSendButton.spec.ts} +11 -11
  59. package/src/components/base/CkPromptSendButton/CkPromptSendButton.stories.ts +117 -0
  60. package/src/components/base/{CkPromptActionButton/CkPromptActionButton.vue → CkPromptSendButton/CkPromptSendButton.vue} +4 -4
  61. package/src/components/base/CkPromptSendButton/index.ts +2 -0
  62. package/src/components/base/CkPromptSendButton/types.ts +33 -0
  63. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +5 -5
  64. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +1 -1
  65. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +3 -2
  66. package/src/components/base/CkPromptStopButton/types.ts +6 -4
  67. package/src/components/base/icons/CkSendIcon.vue +3 -16
  68. package/src/components/base/icons/CkStopIcon.vue +1 -1
  69. package/src/components/base/index.ts +3 -1
  70. package/src/components/composed/CkInput/CkInput.stories.ts +0 -4
  71. package/src/components/composed/CkInput/CkInput.vue +7 -5
  72. package/src/components/composed/CkInput/types.ts +2 -7
  73. package/src/components/index.ts +3 -1
  74. package/src/index.ts +3 -1
  75. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts.map +0 -1
  76. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts.map +0 -1
  77. package/dist/components/base/CkPromptActionButton/index.d.ts +0 -3
  78. package/dist/components/base/CkPromptActionButton/index.d.ts.map +0 -1
  79. package/dist/components/base/CkPromptActionButton/types.d.ts +0 -33
  80. package/dist/components/base/CkPromptActionButton/types.d.ts.map +0 -1
  81. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +0 -126
  82. package/src/components/base/CkPromptActionButton/index.ts +0 -2
  83. package/src/components/base/CkPromptActionButton/types.ts +0 -32
@@ -1,6 +1,26 @@
1
1
  import CkPromptInput from './CkPromptInput.vue';
2
+ import CkPromptSendButton from '../CkPromptSendButton/CkPromptSendButton.vue';
3
+ import CkPromptStopButton from '../CkPromptStopButton/CkPromptStopButton.vue';
4
+ import CkPromptMenuButton from '../CkPromptMenuButton/CkPromptMenuButton.vue';
5
+ import CkPromptInputDisclaimer from '../CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue';
6
+ import CkModelSelector from '../CkModelSelector/CkModelSelector.vue';
7
+ import CkFileItemsList from '../CkFileItemsList/CkFileItemsList.vue';
8
+ import CkDropZoneOverlay from '../CkDropZoneOverlay/CkDropZoneOverlay.vue';
9
+ import CkSendIcon from '../icons/CkSendIcon.vue';
10
+ import CkStopIcon from '../icons/CkStopIcon.vue';
11
+ import {
12
+ UiDropdownMenu,
13
+ UiDropdownMenuTrigger,
14
+ UiDropdownMenuContent,
15
+ UiDropdownMenuItem,
16
+ UiIcon,
17
+ formatFileSize,
18
+ } from '@aleph-alpha/ui-library';
19
+ import type { CkFileEntry } from '../CkFileItemsList/types';
20
+ import type { CkModelOption } from '../CkModelSelector/types';
21
+ import type { CkDropZoneRejection } from '../CkDropZoneOverlay/types';
2
22
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
- import { ref } from 'vue';
23
+ import { computed, ref, useTemplateRef } from 'vue';
4
24
 
5
25
  const meta: Meta<typeof CkPromptInput> = {
6
26
  title: 'Base/CkPromptInput',
@@ -16,10 +36,6 @@ const meta: Meta<typeof CkPromptInput> = {
16
36
  description: 'Placeholder shown when the textarea is empty.',
17
37
  },
18
38
  disabled: { control: 'boolean', description: 'Disables the textarea.' },
19
- maxHeight: {
20
- control: 'number',
21
- description: 'Max height (px) before the textarea becomes scrollable.',
22
- },
23
39
  ariaLabel: {
24
40
  control: 'text',
25
41
  description: 'Accessible label for the textarea.',
@@ -37,7 +53,7 @@ export default meta;
37
53
  type Story = StoryObj<typeof meta>;
38
54
 
39
55
  const defaultTemplateSource = `<script setup lang="ts">
40
- import { CkPromptInput } from '@aleph-alpha/chat-kit';
56
+ import { CkPromptInput, CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
41
57
  import { ref } from 'vue';
42
58
 
43
59
  const text = ref('');
@@ -48,11 +64,33 @@ const text = ref('');
48
64
  v-model="text"
49
65
  placeholder="Type a message..."
50
66
  aria-label="Message input"
51
- />
67
+ >
68
+ <template #toolbar-right="{ submit, disabled }">
69
+ <CkPromptSendButton
70
+ aria-label="Send message"
71
+ :disabled="disabled"
72
+ @click="submit"
73
+ >
74
+ <CkSendIcon />
75
+ </CkPromptSendButton>
76
+ </template>
77
+ </CkPromptInput>
52
78
  </template>`;
53
79
 
54
80
  const withToolbarSlotsTemplateSource = `<script setup lang="ts">
55
- import { CkPromptInput } from '@aleph-alpha/chat-kit';
81
+ import {
82
+ CkPromptInput,
83
+ CkPromptSendButton,
84
+ CkPromptMenuButton,
85
+ CkSendIcon,
86
+ } from '@aleph-alpha/chat-kit';
87
+ import {
88
+ UiDropdownMenu,
89
+ UiDropdownMenuTrigger,
90
+ UiDropdownMenuContent,
91
+ UiDropdownMenuItem,
92
+ UiIcon,
93
+ } from '@aleph-alpha/ui-library';
56
94
  import { ref } from 'vue';
57
95
 
58
96
  const text = ref('');
@@ -65,18 +103,41 @@ const text = ref('');
65
103
  aria-label="Message input"
66
104
  >
67
105
  <template #toolbar-left>
68
- <button class="h-9 rounded-md border px-3 text-sm">Options</button>
106
+ <UiDropdownMenu>
107
+ <UiDropdownMenuTrigger as-child>
108
+ <CkPromptMenuButton aria-label="Open prompt menu" />
109
+ </UiDropdownMenuTrigger>
110
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
111
+ <UiDropdownMenuItem>
112
+ <UiIcon name="file" :size="16" />
113
+ Add documents
114
+ </UiDropdownMenuItem>
115
+ <UiDropdownMenuItem>
116
+ <UiIcon name="folder" :size="16" />
117
+ Add folders
118
+ </UiDropdownMenuItem>
119
+ </UiDropdownMenuContent>
120
+ </UiDropdownMenu>
69
121
  </template>
70
122
  <template #toolbar-right="{ submit, disabled }">
71
- <button :disabled="disabled" class="size-9 rounded-md bg-brand-bg-brand" @click="submit">
72
- Send
73
- </button>
123
+ <CkPromptSendButton
124
+ aria-label="Send message"
125
+ :disabled="disabled"
126
+ @click="submit"
127
+ >
128
+ <CkSendIcon />
129
+ </CkPromptSendButton>
74
130
  </template>
75
131
  </CkPromptInput>
76
132
  </template>`;
77
133
 
78
134
  const withFooterSlotTemplateSource = `<script setup lang="ts">
79
- import { CkPromptInput } from '@aleph-alpha/chat-kit';
135
+ import {
136
+ CkPromptInput,
137
+ CkPromptSendButton,
138
+ CkPromptInputDisclaimer,
139
+ CkSendIcon,
140
+ } from '@aleph-alpha/chat-kit';
80
141
  import { ref } from 'vue';
81
142
 
82
143
  const text = ref('');
@@ -84,16 +145,101 @@ const text = ref('');
84
145
 
85
146
  <template>
86
147
  <CkPromptInput v-model="text" placeholder="Type a message..." aria-label="Message input">
148
+ <template #toolbar-right="{ submit, disabled }">
149
+ <CkPromptSendButton
150
+ aria-label="Send message"
151
+ :disabled="disabled"
152
+ @click="submit"
153
+ >
154
+ <CkSendIcon />
155
+ </CkPromptSendButton>
156
+ </template>
87
157
  <template #footer>
88
- <p class="text-center text-xs text-core-content-tertiary">
89
- Custom footer content via slot
90
- </p>
158
+ <CkPromptInputDisclaimer
159
+ text="AI can hallucinate confident-sounding but false information. Verify before you act on it."
160
+ />
161
+ </template>
162
+ </CkPromptInput>
163
+ </template>`;
164
+
165
+ const withFooterControlsTemplateSource = `<script setup lang="ts">
166
+ import { CkPromptInput, CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
167
+ import { ref, computed } from 'vue';
168
+
169
+ const text = ref('');
170
+ const maxChars = 500;
171
+ const charsLeft = computed(() => maxChars - text.value.length);
172
+ </script>
173
+
174
+ <template>
175
+ <CkPromptInput v-model="text" placeholder="Type a message..." aria-label="Message input">
176
+ <template #toolbar-right="{ submit, disabled }">
177
+ <CkPromptSendButton
178
+ aria-label="Send message"
179
+ :disabled="disabled"
180
+ @click="submit"
181
+ >
182
+ <CkSendIcon />
183
+ </CkPromptSendButton>
184
+ </template>
185
+ <template #footer>
186
+ <div class="flex items-center justify-between text-xs text-content-on-surface-muted">
187
+ <span>
188
+ Press <kbd class="rounded border px-1 py-0.5">Shift</kbd>
189
+ + <kbd class="rounded border px-1 py-0.5">Enter</kbd> for newline
190
+ </span>
191
+ <span :class="{ 'text-red-500': charsLeft < 0 }">
192
+ {{ charsLeft }} / {{ maxChars }}
193
+ </span>
194
+ </div>
195
+ </template>
196
+ </CkPromptInput>
197
+ </template>`;
198
+
199
+ const withToolbarTopSlotTemplateSource = `<script setup lang="ts">
200
+ import {
201
+ CkPromptInput,
202
+ CkPromptSendButton,
203
+ CkFileItemsList,
204
+ CkSendIcon,
205
+ type CkFileEntry,
206
+ } from '@aleph-alpha/chat-kit';
207
+ import { ref } from 'vue';
208
+
209
+ const text = ref('');
210
+ const files = ref<CkFileEntry[]>([
211
+ { id: 'a', filename: 'Invoice.pdf', typeLabel: 'PDF', sizeLabel: '12 MB' },
212
+ { id: 'b', filename: 'Data.csv', typeLabel: 'CSV', sizeLabel: '10 MB' },
213
+ ]);
214
+ function removeFile(id: CkFileEntry['id']) {
215
+ files.value = files.value.filter((f) => f.id !== id);
216
+ }
217
+ </script>
218
+
219
+ <template>
220
+ <CkPromptInput v-model="text" placeholder="Type a message..." aria-label="Message input">
221
+ <template #toolbar-top>
222
+ <CkFileItemsList
223
+ :files="files"
224
+ is-removable
225
+ list-label="Attached files"
226
+ @remove="removeFile"
227
+ />
228
+ </template>
229
+ <template #toolbar-right="{ submit, disabled }">
230
+ <CkPromptSendButton
231
+ aria-label="Send message"
232
+ :disabled="disabled"
233
+ @click="submit"
234
+ >
235
+ <CkSendIcon />
236
+ </CkPromptSendButton>
91
237
  </template>
92
238
  </CkPromptInput>
93
239
  </template>`;
94
240
 
95
241
  const disabledTemplateSource = `<script setup lang="ts">
96
- import { CkPromptInput } from '@aleph-alpha/chat-kit';
242
+ import { CkPromptInput, CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
97
243
  import { ref } from 'vue';
98
244
 
99
245
  const text = ref('');
@@ -105,7 +251,17 @@ const text = ref('');
105
251
  disabled
106
252
  placeholder="Input is disabled"
107
253
  aria-label="Disabled input"
108
- />
254
+ >
255
+ <template #toolbar-right="{ submit, disabled }">
256
+ <CkPromptSendButton
257
+ aria-label="Send message"
258
+ :disabled="disabled"
259
+ @click="submit"
260
+ >
261
+ <CkSendIcon />
262
+ </CkPromptSendButton>
263
+ </template>
264
+ </CkPromptInput>
109
265
  </template>`;
110
266
 
111
267
  /**
@@ -114,7 +270,7 @@ const text = ref('');
114
270
  */
115
271
  export const Default: Story = {
116
272
  render: () => ({
117
- components: { CkPromptInput },
273
+ components: { CkPromptInput, CkPromptSendButton, CkSendIcon },
118
274
  setup() {
119
275
  const text = ref('');
120
276
  return { text };
@@ -124,19 +280,41 @@ export const Default: Story = {
124
280
  v-model="text"
125
281
  placeholder="Type a message..."
126
282
  aria-label="Message input"
127
- />
283
+ >
284
+ <template #toolbar-right="{ submit, disabled }">
285
+ <CkPromptSendButton
286
+ aria-label="Send message"
287
+ :disabled="disabled"
288
+ @click="submit"
289
+ >
290
+ <CkSendIcon />
291
+ </CkPromptSendButton>
292
+ </template>
293
+ </CkPromptInput>
128
294
  `,
129
295
  }),
130
296
  parameters: { docs: { source: { code: defaultTemplateSource } } },
131
297
  };
132
298
 
133
299
  /**
134
- * Demonstrates both `toolbar-left` and `toolbar-right` slots. The right slot
135
- * receives `{ submit, disabled, isEmpty }` to wire up a custom send button.
300
+ * Both toolbar slots filled with the real chat-kit primitives: the left slot
301
+ * hosts a `CkPromptMenuButton` wrapped in `UiDropdownMenu` (the canonical "+"
302
+ * trigger pattern with file/folder actions), the right slot a
303
+ * `CkPromptSendButton`. Mirrors Figma's "Drawer to add resources or tools".
136
304
  */
137
305
  export const WithToolbarSlots: Story = {
138
306
  render: () => ({
139
- components: { CkPromptInput },
307
+ components: {
308
+ CkPromptInput,
309
+ CkPromptSendButton,
310
+ CkPromptMenuButton,
311
+ CkSendIcon,
312
+ UiDropdownMenu,
313
+ UiDropdownMenuTrigger,
314
+ UiDropdownMenuContent,
315
+ UiDropdownMenuItem,
316
+ UiIcon,
317
+ },
140
318
  setup() {
141
319
  const text = ref('');
142
320
  return { text };
@@ -148,22 +326,30 @@ export const WithToolbarSlots: Story = {
148
326
  aria-label="Message input"
149
327
  >
150
328
  <template #toolbar-left>
151
- <button
152
- class="flex items-center gap-1 rounded-md border border-solid px-3 h-9 text-sm"
153
- style="border-color: rgba(183, 180, 164, 0.3);"
154
- >
155
- Options
156
- </button>
329
+ <UiDropdownMenu>
330
+ <UiDropdownMenuTrigger as-child>
331
+ <CkPromptMenuButton aria-label="Open prompt menu" />
332
+ </UiDropdownMenuTrigger>
333
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
334
+ <UiDropdownMenuItem>
335
+ <UiIcon name="file" :size="16" />
336
+ Add documents
337
+ </UiDropdownMenuItem>
338
+ <UiDropdownMenuItem>
339
+ <UiIcon name="folder" :size="16" />
340
+ Add folders
341
+ </UiDropdownMenuItem>
342
+ </UiDropdownMenuContent>
343
+ </UiDropdownMenu>
157
344
  </template>
158
345
  <template #toolbar-right="{ submit, disabled }">
159
- <button
346
+ <CkPromptSendButton
347
+ aria-label="Send message"
160
348
  :disabled="disabled"
161
- class="flex items-center justify-center size-9 rounded-md disabled:opacity-50"
162
- style="background-color: #c9e400; color: #1d1d1b;"
163
349
  @click="submit"
164
350
  >
165
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/></svg>
166
- </button>
351
+ <CkSendIcon />
352
+ </CkPromptSendButton>
167
353
  </template>
168
354
  </CkPromptInput>
169
355
  `,
@@ -172,13 +358,19 @@ export const WithToolbarSlots: Story = {
172
358
  };
173
359
 
174
360
  /**
175
- * Uses the `footer` slot to render helper text (disclaimer, keyboard hint,
176
- * etc.) directly below the input form. Use when you want a custom footer
177
- * instead of `CkPromptInputDisclaimer`.
361
+ * `footer` slot filled with the canonical `CkPromptInputDisclaimer` —
362
+ * the AI-hallucination warning matching the Figma design system. For
363
+ * arbitrary custom footer content (helpers, counters, etc.) see the
364
+ * `WithFooterControls` story below.
178
365
  */
179
366
  export const WithFooterSlot: Story = {
180
367
  render: () => ({
181
- components: { CkPromptInput },
368
+ components: {
369
+ CkPromptInput,
370
+ CkPromptSendButton,
371
+ CkPromptInputDisclaimer,
372
+ CkSendIcon,
373
+ },
182
374
  setup() {
183
375
  const text = ref('');
184
376
  return { text };
@@ -189,10 +381,19 @@ export const WithFooterSlot: Story = {
189
381
  placeholder="Type a message..."
190
382
  aria-label="Message input"
191
383
  >
384
+ <template #toolbar-right="{ submit, disabled }">
385
+ <CkPromptSendButton
386
+ aria-label="Send message"
387
+ :disabled="disabled"
388
+ @click="submit"
389
+ >
390
+ <CkSendIcon />
391
+ </CkPromptSendButton>
392
+ </template>
192
393
  <template #footer>
193
- <p class="text-core-content-tertiary text-center text-xs leading-3.5">
194
- Custom footer content via slot
195
- </p>
394
+ <CkPromptInputDisclaimer
395
+ text="AI can hallucinate confident-sounding but false information. Verify before you act on it."
396
+ />
196
397
  </template>
197
398
  </CkPromptInput>
198
399
  `,
@@ -200,13 +401,827 @@ export const WithFooterSlot: Story = {
200
401
  parameters: { docs: { source: { code: withFooterSlotTemplateSource } } },
201
402
  };
202
403
 
404
+ /**
405
+ * Footer slot used for interactive helpers — keyboard hint on the left and a
406
+ * live character counter on the right. Demonstrates that the footer can hold
407
+ * structured content, not just disclaimer text.
408
+ */
409
+ export const WithFooterControls: Story = {
410
+ render: () => ({
411
+ components: { CkPromptInput, CkPromptSendButton, CkSendIcon },
412
+ setup() {
413
+ const text = ref('');
414
+ const maxChars = 500;
415
+ const charsLeft = computed(() => maxChars - text.value.length);
416
+ return { text, maxChars, charsLeft };
417
+ },
418
+ template: `
419
+ <CkPromptInput
420
+ v-model="text"
421
+ placeholder="Type a message..."
422
+ aria-label="Message input"
423
+ >
424
+ <template #toolbar-right="{ submit, disabled }">
425
+ <CkPromptSendButton
426
+ aria-label="Send message"
427
+ :disabled="disabled"
428
+ @click="submit"
429
+ >
430
+ <CkSendIcon />
431
+ </CkPromptSendButton>
432
+ </template>
433
+ <template #footer>
434
+ <div class="text-content-on-surface-muted flex items-center justify-between text-xs">
435
+ <span>
436
+ Press
437
+ <kbd class="border-border-surface-default rounded border px-1 py-0.5 font-mono">Shift</kbd>
438
+ +
439
+ <kbd class="border-border-surface-default rounded border px-1 py-0.5 font-mono">Enter</kbd>
440
+ for newline
441
+ </span>
442
+ <span :class="{ 'text-red-500': charsLeft < 0 }">
443
+ {{ charsLeft }} / {{ maxChars }}
444
+ </span>
445
+ </div>
446
+ </template>
447
+ </CkPromptInput>
448
+ `,
449
+ }),
450
+ parameters: { docs: { source: { code: withFooterControlsTemplateSource } } },
451
+ };
452
+
453
+ /**
454
+ * `toolbar-top` slot rendered with the canonical `CkFileItemsList` primitive.
455
+ * Two attached files visible as inline chips. Adding the top row also flips
456
+ * the form to the multi-row layout (corner radius drops from 3xl to 2xl).
457
+ */
458
+ export const WithToolbarTopSlot: Story = {
459
+ render: () => ({
460
+ components: {
461
+ CkPromptInput,
462
+ CkPromptSendButton,
463
+ CkFileItemsList,
464
+ CkSendIcon,
465
+ },
466
+ setup() {
467
+ const text = ref('');
468
+ const files = ref<CkFileEntry[]>([
469
+ {
470
+ id: 'a',
471
+ filename: 'Invoice.pdf',
472
+ typeLabel: 'PDF',
473
+ sizeLabel: '12 MB',
474
+ },
475
+ { id: 'b', filename: 'Data.csv', typeLabel: 'CSV', sizeLabel: '10 MB' },
476
+ ]);
477
+ function removeFile(id: CkFileEntry['id']) {
478
+ files.value = files.value.filter((f) => f.id !== id);
479
+ }
480
+ return { text, files, removeFile };
481
+ },
482
+ template: `
483
+ <CkPromptInput
484
+ v-model="text"
485
+ placeholder="Type a message..."
486
+ aria-label="Message input"
487
+ >
488
+ <template #toolbar-top>
489
+ <CkFileItemsList
490
+ :files="files"
491
+ is-removable
492
+ list-label="Attached files"
493
+ @remove="removeFile"
494
+ />
495
+ </template>
496
+ <template #toolbar-right="{ submit, disabled }">
497
+ <CkPromptSendButton
498
+ aria-label="Send message"
499
+ :disabled="disabled"
500
+ @click="submit"
501
+ >
502
+ <CkSendIcon />
503
+ </CkPromptSendButton>
504
+ </template>
505
+ </CkPromptInput>
506
+ `,
507
+ }),
508
+ parameters: { docs: { source: { code: withToolbarTopSlotTemplateSource } } },
509
+ };
510
+
511
+ const withUploadAndModelSelectionTemplateSource = `<script setup lang="ts">
512
+ import {
513
+ CkPromptInput,
514
+ CkPromptSendButton,
515
+ CkPromptMenuButton,
516
+ CkPromptInputDisclaimer,
517
+ CkModelSelector,
518
+ CkFileItemsList,
519
+ CkDropZoneOverlay,
520
+ CkSendIcon,
521
+ type CkFileEntry,
522
+ type CkModelOption,
523
+ type CkDropZoneRejection,
524
+ } from '@aleph-alpha/chat-kit';
525
+ import {
526
+ UiDropdownMenu,
527
+ UiDropdownMenuTrigger,
528
+ UiDropdownMenuContent,
529
+ UiDropdownMenuItem,
530
+ UiIcon,
531
+ formatFileSize,
532
+ } from '@aleph-alpha/ui-library';
533
+ import { ref, useTemplateRef } from 'vue';
534
+
535
+ const text = ref('');
536
+ const files = ref<CkFileEntry[]>([]);
537
+ const selectedModelId = ref<string | null>('phoenix');
538
+ const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef');
539
+ const folderInputRef = useTemplateRef<HTMLInputElement>('folderInputRef');
540
+
541
+ const models: CkModelOption[] = [
542
+ { id: 'phoenix', label: 'Phoenix' },
543
+ { id: 'pharia-1-llm-7b', label: 'Pharia 1 LLM 7B' },
544
+ { id: 'pharia-1-llm-13b', label: 'Pharia 1 LLM 13B' },
545
+ ];
546
+
547
+ function appendFiles(picked: File[]): void {
548
+ const next = picked.map((file) => ({
549
+ id: \`\${file.name}-\${file.size}-\${file.lastModified}\`,
550
+ filename: file.name,
551
+ typeLabel: file.name.split('.').pop()?.toUpperCase() ?? 'FILE',
552
+ sizeLabel: formatFileSize(file.size),
553
+ }));
554
+ files.value = [...files.value, ...next];
555
+ }
556
+
557
+ function onFilePick(event: Event): void {
558
+ const input = event.target as HTMLInputElement;
559
+ appendFiles(Array.from(input.files ?? []));
560
+ input.value = '';
561
+ }
562
+
563
+ function onRemoveFile(id: CkFileEntry['id']): void {
564
+ files.value = files.value.filter((file) => file.id !== id);
565
+ }
566
+
567
+ function onRejected(rejections: CkDropZoneRejection[]): void {
568
+ console.warn('Rejected files', rejections);
569
+ }
570
+
571
+ function onSubmit(value: string): void {
572
+ console.log('submit', { text: value, files: files.value, model: selectedModelId.value });
573
+ text.value = '';
574
+ files.value = [];
575
+ }
576
+ </script>
577
+
578
+ <template>
579
+ <CkDropZoneOverlay
580
+ accept=".pdf,.txt,.md,.csv,.docx"
581
+ :max-files="10"
582
+ :max-file-size-bytes="20 * 1024 * 1024"
583
+ @files="appendFiles"
584
+ @rejected="onRejected"
585
+ />
586
+ <input
587
+ ref="fileInputRef"
588
+ type="file"
589
+ multiple
590
+ accept=".pdf,.txt,.md,.csv,.docx"
591
+ class="sr-only"
592
+ @change="onFilePick"
593
+ />
594
+ <input
595
+ ref="folderInputRef"
596
+ type="file"
597
+ multiple
598
+ webkitdirectory
599
+ class="sr-only"
600
+ @change="onFilePick"
601
+ />
602
+ <CkPromptInput
603
+ v-model="text"
604
+ placeholder="Ask me anything"
605
+ aria-label="Chat message input"
606
+ @submit="onSubmit"
607
+ >
608
+ <template v-if="files.length" #toolbar-top>
609
+ <CkFileItemsList
610
+ :files="files"
611
+ is-removable
612
+ list-label="Attached files"
613
+ @remove="onRemoveFile"
614
+ />
615
+ </template>
616
+ <template #toolbar-left>
617
+ <UiDropdownMenu>
618
+ <UiDropdownMenuTrigger as-child>
619
+ <CkPromptMenuButton aria-label="Open prompt menu" />
620
+ </UiDropdownMenuTrigger>
621
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
622
+ <UiDropdownMenuItem @select="fileInputRef?.click()">
623
+ <UiIcon name="file" :size="16" />
624
+ Add documents
625
+ </UiDropdownMenuItem>
626
+ <UiDropdownMenuItem @select="folderInputRef?.click()">
627
+ <UiIcon name="folder" :size="16" />
628
+ Add folders
629
+ </UiDropdownMenuItem>
630
+ </UiDropdownMenuContent>
631
+ </UiDropdownMenu>
632
+ </template>
633
+ <template #toolbar-right="{ submit, disabled }">
634
+ <div class="flex items-center gap-3">
635
+ <CkModelSelector
636
+ :models="models"
637
+ v-model:selected-model-id="selectedModelId"
638
+ />
639
+ <CkPromptSendButton
640
+ aria-label="Send message"
641
+ :disabled="disabled"
642
+ @click="submit"
643
+ >
644
+ <CkSendIcon />
645
+ </CkPromptSendButton>
646
+ </div>
647
+ </template>
648
+ <template #footer>
649
+ <CkPromptInputDisclaimer
650
+ text="AI can hallucinate confident-sounding but false information. Verify before you act on it."
651
+ />
652
+ </template>
653
+ </CkPromptInput>
654
+ </template>`;
655
+
656
+ /**
657
+ * End-to-end composition matching the Figma "Prompt field" spec: file
658
+ * attachments via the `+` button and drag-and-drop, model selection next to
659
+ * the send button, and a hallucination disclaimer in the footer. File chips
660
+ * collapse to a popover once more than three are attached (`CkFileItemsList`
661
+ * default).
662
+ */
663
+ export const WithUploadAndModelSelection: Story = {
664
+ render: () => ({
665
+ components: {
666
+ CkPromptInput,
667
+ CkPromptSendButton,
668
+ CkPromptMenuButton,
669
+ CkPromptInputDisclaimer,
670
+ CkModelSelector,
671
+ CkFileItemsList,
672
+ CkDropZoneOverlay,
673
+ CkSendIcon,
674
+ UiDropdownMenu,
675
+ UiDropdownMenuTrigger,
676
+ UiDropdownMenuContent,
677
+ UiDropdownMenuItem,
678
+ UiIcon,
679
+ },
680
+ setup() {
681
+ const text = ref('');
682
+ const files = ref<CkFileEntry[]>([]);
683
+ const selectedModelId = ref<string | null>('phoenix');
684
+ const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef');
685
+ const folderInputRef = useTemplateRef<HTMLInputElement>('folderInputRef');
686
+
687
+ const models: CkModelOption[] = [
688
+ { id: 'phoenix', label: 'Phoenix' },
689
+ { id: 'pharia-1-llm-7b', label: 'Pharia 1 LLM 7B' },
690
+ { id: 'pharia-1-llm-13b', label: 'Pharia 1 LLM 13B' },
691
+ ];
692
+
693
+ function appendFiles(picked: File[]): void {
694
+ const next: CkFileEntry[] = picked.map((file) => ({
695
+ id: `${file.name}-${file.size}-${file.lastModified}`,
696
+ filename: file.name,
697
+ typeLabel: file.name.split('.').pop()?.toUpperCase() ?? 'FILE',
698
+ sizeLabel: formatFileSize(file.size),
699
+ }));
700
+ files.value = [...files.value, ...next];
701
+ }
702
+
703
+ function onFilePick(event: Event): void {
704
+ const input = event.target as HTMLInputElement;
705
+ appendFiles(Array.from(input.files ?? []));
706
+ input.value = '';
707
+ }
708
+
709
+ function onRemoveFile(id: CkFileEntry['id']): void {
710
+ files.value = files.value.filter((file) => file.id !== id);
711
+ }
712
+
713
+ function onRejected(rejections: CkDropZoneRejection[]): void {
714
+ // Surfaced by the built-in toast; consumer-side logging only.
715
+ // eslint-disable-next-line no-console
716
+ console.warn('Rejected files', rejections);
717
+ }
718
+
719
+ function onSubmit(value: string): void {
720
+ // eslint-disable-next-line no-console
721
+ console.log('submit', {
722
+ text: value,
723
+ files: files.value,
724
+ model: selectedModelId.value,
725
+ });
726
+ text.value = '';
727
+ files.value = [];
728
+ }
729
+
730
+ return {
731
+ text,
732
+ files,
733
+ models,
734
+ selectedModelId,
735
+ fileInputRef,
736
+ folderInputRef,
737
+ appendFiles,
738
+ onFilePick,
739
+ onRemoveFile,
740
+ onRejected,
741
+ onSubmit,
742
+ };
743
+ },
744
+ template: `
745
+ <CkDropZoneOverlay
746
+ accept=".pdf,.txt,.md,.csv,.docx"
747
+ :max-files="10"
748
+ :max-file-size-bytes="20 * 1024 * 1024"
749
+ @files="appendFiles"
750
+ @rejected="onRejected"
751
+ />
752
+ <input
753
+ ref="fileInputRef"
754
+ type="file"
755
+ multiple
756
+ accept=".pdf,.txt,.md,.csv,.docx"
757
+ class="sr-only"
758
+ @change="onFilePick"
759
+ />
760
+ <input
761
+ ref="folderInputRef"
762
+ type="file"
763
+ multiple
764
+ webkitdirectory
765
+ class="sr-only"
766
+ @change="onFilePick"
767
+ />
768
+ <CkPromptInput
769
+ v-model="text"
770
+ placeholder="Ask me anything"
771
+ aria-label="Chat message input"
772
+ @submit="onSubmit"
773
+ >
774
+ <template v-if="files.length" #toolbar-top>
775
+ <CkFileItemsList
776
+ :files="files"
777
+ is-removable
778
+ list-label="Attached files"
779
+ @remove="onRemoveFile"
780
+ />
781
+ </template>
782
+ <template #toolbar-left>
783
+ <UiDropdownMenu>
784
+ <UiDropdownMenuTrigger as-child>
785
+ <CkPromptMenuButton aria-label="Open prompt menu" />
786
+ </UiDropdownMenuTrigger>
787
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
788
+ <UiDropdownMenuItem @select="fileInputRef?.click()">
789
+ <UiIcon name="file" :size="16" />
790
+ Add documents
791
+ </UiDropdownMenuItem>
792
+ <UiDropdownMenuItem @select="folderInputRef?.click()">
793
+ <UiIcon name="folder" :size="16" />
794
+ Add folders
795
+ </UiDropdownMenuItem>
796
+ </UiDropdownMenuContent>
797
+ </UiDropdownMenu>
798
+ </template>
799
+ <template #toolbar-right="{ submit, disabled }">
800
+ <div class="flex items-center gap-3">
801
+ <CkModelSelector
802
+ :models="models"
803
+ v-model:selected-model-id="selectedModelId"
804
+ />
805
+ <CkPromptSendButton
806
+ aria-label="Send message"
807
+ :disabled="disabled"
808
+ @click="submit"
809
+ >
810
+ <CkSendIcon />
811
+ </CkPromptSendButton>
812
+ </div>
813
+ </template>
814
+ <template #footer>
815
+ <CkPromptInputDisclaimer
816
+ text="AI can hallucinate confident-sounding but false information. Verify before you act on it."
817
+ />
818
+ </template>
819
+ </CkPromptInput>
820
+ `,
821
+ }),
822
+ parameters: {
823
+ docs: { source: { code: withUploadAndModelSelectionTemplateSource } },
824
+ },
825
+ };
826
+
827
+ const withMultiLineContentTemplateSource = `<script setup lang="ts">
828
+ import { CkPromptInput, CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
829
+ import { ref } from 'vue';
830
+
831
+ const text = ref(
832
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\\n' +
833
+ 'Second line forces the layout to stack: textarea spans the full width,\\n' +
834
+ 'toolbar drops to a dedicated row, form radius shrinks to 2xl.'
835
+ );
836
+ </script>
837
+
838
+ <template>
839
+ <CkPromptInput v-model="text" aria-label="Message input">
840
+ <template #toolbar-right="{ submit, disabled }">
841
+ <CkPromptSendButton
842
+ aria-label="Send message"
843
+ :disabled="disabled"
844
+ @click="submit"
845
+ >
846
+ <CkSendIcon />
847
+ </CkPromptSendButton>
848
+ </template>
849
+ </CkPromptInput>
850
+ </template>`;
851
+
852
+ /**
853
+ * Pre-filled with multi-line text so the multi-row layout is visible at
854
+ * mount: textarea spans the full width on its own row, toolbar drops below,
855
+ * and the form's corner radius shrinks from `rounded-3xl` to `rounded-2xl`.
856
+ */
857
+ export const WithMultiLineContent: Story = {
858
+ render: () => ({
859
+ components: { CkPromptInput, CkPromptSendButton, CkSendIcon },
860
+ setup() {
861
+ const text = ref(
862
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n' +
863
+ 'Second line forces the layout to stack: textarea spans the full width,\n' +
864
+ 'toolbar drops to a dedicated row, form radius shrinks to 2xl.',
865
+ );
866
+ return { text };
867
+ },
868
+ template: `
869
+ <CkPromptInput v-model="text" aria-label="Message input">
870
+ <template #toolbar-right="{ submit, disabled }">
871
+ <CkPromptSendButton
872
+ aria-label="Send message"
873
+ :disabled="disabled"
874
+ @click="submit"
875
+ >
876
+ <CkSendIcon />
877
+ </CkPromptSendButton>
878
+ </template>
879
+ </CkPromptInput>
880
+ `,
881
+ }),
882
+ parameters: {
883
+ docs: { source: { code: withMultiLineContentTemplateSource } },
884
+ },
885
+ };
886
+
887
+ const withCollapsedFileChipsTemplateSource = `<script setup lang="ts">
888
+ import {
889
+ CkPromptInput,
890
+ CkPromptSendButton,
891
+ CkPromptMenuButton,
892
+ CkFileItemsList,
893
+ CkSendIcon,
894
+ type CkFileEntry,
895
+ } from '@aleph-alpha/chat-kit';
896
+ import {
897
+ UiDropdownMenu,
898
+ UiDropdownMenuTrigger,
899
+ UiDropdownMenuContent,
900
+ UiDropdownMenuItem,
901
+ UiIcon,
902
+ } from '@aleph-alpha/ui-library';
903
+ import { ref } from 'vue';
904
+
905
+ const text = ref('');
906
+ const files = ref<CkFileEntry[]>([
907
+ { id: '1', filename: 'Invoice.pdf', typeLabel: 'PDF', sizeLabel: '12 MB' },
908
+ { id: '2', filename: 'Data.csv', typeLabel: 'CSV', sizeLabel: '10 MB' },
909
+ { id: '3', filename: 'Report.docx', typeLabel: 'DOCX', sizeLabel: '4 MB' },
910
+ { id: '4', filename: 'Notes.md', typeLabel: 'MD', sizeLabel: '76 KB' },
911
+ { id: '5', filename: 'schema.json', typeLabel: 'JSON', sizeLabel: '36 KB' },
912
+ ]);
913
+ function removeFile(id: CkFileEntry['id']) {
914
+ files.value = files.value.filter((f) => f.id !== id);
915
+ }
916
+ </script>
917
+
918
+ <template>
919
+ <CkPromptInput v-model="text" aria-label="Message input">
920
+ <template #toolbar-top>
921
+ <CkFileItemsList
922
+ :files="files"
923
+ is-removable
924
+ list-label="Attached files"
925
+ @remove="removeFile"
926
+ />
927
+ </template>
928
+ <template #toolbar-left>
929
+ <UiDropdownMenu>
930
+ <UiDropdownMenuTrigger as-child>
931
+ <CkPromptMenuButton aria-label="Open prompt menu" />
932
+ </UiDropdownMenuTrigger>
933
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
934
+ <UiDropdownMenuItem>
935
+ <UiIcon name="file" :size="16" />
936
+ Add documents
937
+ </UiDropdownMenuItem>
938
+ <UiDropdownMenuItem>
939
+ <UiIcon name="folder" :size="16" />
940
+ Add folders
941
+ </UiDropdownMenuItem>
942
+ </UiDropdownMenuContent>
943
+ </UiDropdownMenu>
944
+ </template>
945
+ <template #toolbar-right="{ submit, disabled }">
946
+ <CkPromptSendButton
947
+ aria-label="Send message"
948
+ :disabled="disabled"
949
+ @click="submit"
950
+ >
951
+ <CkSendIcon />
952
+ </CkPromptSendButton>
953
+ </template>
954
+ </CkPromptInput>
955
+ </template>`;
956
+
957
+ /**
958
+ * Five attached files — past the default `maxInline=3` threshold of
959
+ * `CkFileItemsList`, so the list collapses to a single popover trigger
960
+ * labeled "5 files". Click it to inspect / remove the individual items.
961
+ * The toolbar-left `+` menu stays available so users can keep adding more.
962
+ */
963
+ export const WithCollapsedFileChips: Story = {
964
+ render: () => ({
965
+ components: {
966
+ CkPromptInput,
967
+ CkPromptSendButton,
968
+ CkPromptMenuButton,
969
+ CkFileItemsList,
970
+ CkSendIcon,
971
+ UiDropdownMenu,
972
+ UiDropdownMenuTrigger,
973
+ UiDropdownMenuContent,
974
+ UiDropdownMenuItem,
975
+ UiIcon,
976
+ },
977
+ setup() {
978
+ const text = ref('');
979
+ const files = ref<CkFileEntry[]>([
980
+ {
981
+ id: '1',
982
+ filename: 'Invoice.pdf',
983
+ typeLabel: 'PDF',
984
+ sizeLabel: '12 MB',
985
+ },
986
+ { id: '2', filename: 'Data.csv', typeLabel: 'CSV', sizeLabel: '10 MB' },
987
+ {
988
+ id: '3',
989
+ filename: 'Report.docx',
990
+ typeLabel: 'DOCX',
991
+ sizeLabel: '4 MB',
992
+ },
993
+ { id: '4', filename: 'Notes.md', typeLabel: 'MD', sizeLabel: '76 KB' },
994
+ {
995
+ id: '5',
996
+ filename: 'schema.json',
997
+ typeLabel: 'JSON',
998
+ sizeLabel: '36 KB',
999
+ },
1000
+ ]);
1001
+ function removeFile(id: CkFileEntry['id']) {
1002
+ files.value = files.value.filter((f) => f.id !== id);
1003
+ }
1004
+ return { text, files, removeFile };
1005
+ },
1006
+ template: `
1007
+ <CkPromptInput v-model="text" aria-label="Message input">
1008
+ <template #toolbar-top>
1009
+ <CkFileItemsList
1010
+ :files="files"
1011
+ is-removable
1012
+ list-label="Attached files"
1013
+ @remove="removeFile"
1014
+ />
1015
+ </template>
1016
+ <template #toolbar-left>
1017
+ <UiDropdownMenu>
1018
+ <UiDropdownMenuTrigger as-child>
1019
+ <CkPromptMenuButton aria-label="Open prompt menu" />
1020
+ </UiDropdownMenuTrigger>
1021
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
1022
+ <UiDropdownMenuItem>
1023
+ <UiIcon name="file" :size="16" />
1024
+ Add documents
1025
+ </UiDropdownMenuItem>
1026
+ <UiDropdownMenuItem>
1027
+ <UiIcon name="folder" :size="16" />
1028
+ Add folders
1029
+ </UiDropdownMenuItem>
1030
+ </UiDropdownMenuContent>
1031
+ </UiDropdownMenu>
1032
+ </template>
1033
+ <template #toolbar-right="{ submit, disabled }">
1034
+ <CkPromptSendButton
1035
+ aria-label="Send message"
1036
+ :disabled="disabled"
1037
+ @click="submit"
1038
+ >
1039
+ <CkSendIcon />
1040
+ </CkPromptSendButton>
1041
+ </template>
1042
+ </CkPromptInput>
1043
+ `,
1044
+ }),
1045
+ parameters: {
1046
+ docs: { source: { code: withCollapsedFileChipsTemplateSource } },
1047
+ },
1048
+ };
1049
+
1050
+ const withUploadAndModelSelectionStreamingTemplateSource = `<script setup lang="ts">
1051
+ import {
1052
+ CkPromptInput,
1053
+ CkPromptStopButton,
1054
+ CkPromptMenuButton,
1055
+ CkPromptInputDisclaimer,
1056
+ CkModelSelector,
1057
+ CkFileItemsList,
1058
+ CkStopIcon,
1059
+ type CkFileEntry,
1060
+ type CkModelOption,
1061
+ } from '@aleph-alpha/chat-kit';
1062
+ import {
1063
+ UiDropdownMenu,
1064
+ UiDropdownMenuTrigger,
1065
+ UiDropdownMenuContent,
1066
+ UiDropdownMenuItem,
1067
+ UiIcon,
1068
+ } from '@aleph-alpha/ui-library';
1069
+ import { ref } from 'vue';
1070
+
1071
+ const text = ref('Explain the difference between supervised and unsupervised learning.');
1072
+ const files = ref<CkFileEntry[]>([
1073
+ { id: 'a', filename: 'Invoice.pdf', typeLabel: 'PDF', sizeLabel: '12 MB' },
1074
+ { id: 'b', filename: 'Data.csv', typeLabel: 'CSV', sizeLabel: '10 MB' },
1075
+ ]);
1076
+ const selectedModelId = ref<string | null>('phoenix');
1077
+ const models: CkModelOption[] = [
1078
+ { id: 'phoenix', label: 'Phoenix' },
1079
+ { id: 'pharia-1-llm-7b', label: 'Pharia 1 LLM 7B' },
1080
+ ];
1081
+ function onStop() { /* abort streaming */ }
1082
+ </script>
1083
+
1084
+ <template>
1085
+ <CkPromptInput
1086
+ v-model="text"
1087
+ placeholder="Ask me anything"
1088
+ aria-label="Chat message input"
1089
+ disabled
1090
+ >
1091
+ <template #toolbar-top>
1092
+ <CkFileItemsList :files="files" list-label="Attached files" />
1093
+ </template>
1094
+ <template #toolbar-left>
1095
+ <UiDropdownMenu>
1096
+ <UiDropdownMenuTrigger as-child>
1097
+ <CkPromptMenuButton aria-label="Open prompt menu" disabled />
1098
+ </UiDropdownMenuTrigger>
1099
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
1100
+ <UiDropdownMenuItem disabled>
1101
+ <UiIcon name="file" :size="16" />
1102
+ Add documents
1103
+ </UiDropdownMenuItem>
1104
+ </UiDropdownMenuContent>
1105
+ </UiDropdownMenu>
1106
+ </template>
1107
+ <template #toolbar-right>
1108
+ <div class="flex items-center gap-3">
1109
+ <CkModelSelector v-model:selected-model-id="selectedModelId" :models="models" />
1110
+ <CkPromptStopButton aria-label="Stop generating" @click="onStop">
1111
+ <CkStopIcon />
1112
+ </CkPromptStopButton>
1113
+ </div>
1114
+ </template>
1115
+ <template #footer>
1116
+ <CkPromptInputDisclaimer
1117
+ text="AI can hallucinate confident-sounding but false information. Verify before you act on it."
1118
+ />
1119
+ </template>
1120
+ </CkPromptInput>
1121
+ </template>`;
1122
+
1123
+ /**
1124
+ * Streaming state mirroring the Figma "While Streaming" variant: the input
1125
+ * is `disabled`, the file chips and model selector remain visible (for
1126
+ * context), and `CkPromptStopButton` replaces `CkPromptSendButton` on the
1127
+ * right. Click the stop button to abort the in-flight assistant reply.
1128
+ */
1129
+ export const WithUploadAndModelSelectionStreaming: Story = {
1130
+ render: () => ({
1131
+ components: {
1132
+ CkPromptInput,
1133
+ CkPromptStopButton,
1134
+ CkPromptMenuButton,
1135
+ CkPromptInputDisclaimer,
1136
+ CkModelSelector,
1137
+ CkFileItemsList,
1138
+ CkStopIcon,
1139
+ UiDropdownMenu,
1140
+ UiDropdownMenuTrigger,
1141
+ UiDropdownMenuContent,
1142
+ UiDropdownMenuItem,
1143
+ UiIcon,
1144
+ },
1145
+ setup() {
1146
+ const text = ref(
1147
+ 'Explain the difference between supervised and unsupervised learning.',
1148
+ );
1149
+ const files = ref<CkFileEntry[]>([
1150
+ {
1151
+ id: 'a',
1152
+ filename: 'Invoice.pdf',
1153
+ typeLabel: 'PDF',
1154
+ sizeLabel: '12 MB',
1155
+ },
1156
+ { id: 'b', filename: 'Data.csv', typeLabel: 'CSV', sizeLabel: '10 MB' },
1157
+ ]);
1158
+ const selectedModelId = ref<string | null>('phoenix');
1159
+ const models: CkModelOption[] = [
1160
+ { id: 'phoenix', label: 'Phoenix' },
1161
+ { id: 'pharia-1-llm-7b', label: 'Pharia 1 LLM 7B' },
1162
+ ];
1163
+ function onStop() {
1164
+ // eslint-disable-next-line no-console
1165
+ console.log('stop');
1166
+ }
1167
+ return { text, files, selectedModelId, models, onStop };
1168
+ },
1169
+ template: `
1170
+ <CkPromptInput
1171
+ v-model="text"
1172
+ placeholder="Ask me anything"
1173
+ aria-label="Chat message input"
1174
+ disabled
1175
+ >
1176
+ <template #toolbar-top>
1177
+ <CkFileItemsList :files="files" list-label="Attached files" />
1178
+ </template>
1179
+ <template #toolbar-left>
1180
+ <UiDropdownMenu>
1181
+ <UiDropdownMenuTrigger as-child>
1182
+ <CkPromptMenuButton aria-label="Open prompt menu" disabled />
1183
+ </UiDropdownMenuTrigger>
1184
+ <UiDropdownMenuContent side="top" align="start" :side-offset="8">
1185
+ <UiDropdownMenuItem disabled>
1186
+ <UiIcon name="file" :size="16" />
1187
+ Add documents
1188
+ </UiDropdownMenuItem>
1189
+ </UiDropdownMenuContent>
1190
+ </UiDropdownMenu>
1191
+ </template>
1192
+ <template #toolbar-right>
1193
+ <div class="flex items-center gap-3">
1194
+ <CkModelSelector
1195
+ v-model:selected-model-id="selectedModelId"
1196
+ :models="models"
1197
+ />
1198
+ <CkPromptStopButton aria-label="Stop generating" @click="onStop">
1199
+ <CkStopIcon />
1200
+ </CkPromptStopButton>
1201
+ </div>
1202
+ </template>
1203
+ <template #footer>
1204
+ <CkPromptInputDisclaimer
1205
+ text="AI can hallucinate confident-sounding but false information. Verify before you act on it."
1206
+ />
1207
+ </template>
1208
+ </CkPromptInput>
1209
+ `,
1210
+ }),
1211
+ parameters: {
1212
+ docs: {
1213
+ source: { code: withUploadAndModelSelectionStreamingTemplateSource },
1214
+ },
1215
+ },
1216
+ };
1217
+
203
1218
  /**
204
1219
  * Disabled state — the textarea cannot be edited and Enter does not submit.
205
1220
  * Use while the server is processing the previous message.
206
1221
  */
207
1222
  export const Disabled: Story = {
208
1223
  render: () => ({
209
- components: { CkPromptInput },
1224
+ components: { CkPromptInput, CkPromptSendButton, CkSendIcon },
210
1225
  setup() {
211
1226
  const text = ref('');
212
1227
  return { text };
@@ -217,7 +1232,17 @@ export const Disabled: Story = {
217
1232
  disabled
218
1233
  placeholder="Input is disabled"
219
1234
  aria-label="Disabled input"
220
- />
1235
+ >
1236
+ <template #toolbar-right="{ submit, disabled }">
1237
+ <CkPromptSendButton
1238
+ aria-label="Send message"
1239
+ :disabled="disabled"
1240
+ @click="submit"
1241
+ >
1242
+ <CkSendIcon />
1243
+ </CkPromptSendButton>
1244
+ </template>
1245
+ </CkPromptInput>
221
1246
  `,
222
1247
  }),
223
1248
  parameters: { docs: { source: { code: disabledTemplateSource } } },