@aleph-alpha/chat-kit 2.1.1 → 2.2.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 (37) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DPZedAQS.js → CkInput.vue_vue_type_script_setup_true_lang-CjfKRuZs.js} +291 -61
  2. package/dist/chat-kit.css +40 -5
  3. package/dist/components/base/CkDropZoneOverlay/CkDropZoneIllustration.vue.d.ts +4 -0
  4. package/dist/components/base/CkDropZoneOverlay/CkDropZoneIllustration.vue.d.ts.map +1 -0
  5. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.stories.d.ts +17 -0
  6. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.stories.d.ts.map +1 -0
  7. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts +31 -0
  8. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts.map +1 -0
  9. package/dist/components/base/CkDropZoneOverlay/index.d.ts +3 -0
  10. package/dist/components/base/CkDropZoneOverlay/index.d.ts.map +1 -0
  11. package/dist/components/base/CkDropZoneOverlay/types.d.ts +88 -0
  12. package/dist/components/base/CkDropZoneOverlay/types.d.ts.map +1 -0
  13. package/dist/components/base/CkFeedbackDialog/CkFeedbackDialog.vue.d.ts.map +1 -1
  14. package/dist/components/base/index.d.ts +2 -0
  15. package/dist/components/base/index.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +2 -2
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/components/index.js +19 -18
  19. package/dist/composables/fileUpload/index.d.ts.map +1 -1
  20. package/dist/composables/index.js +1 -1
  21. package/dist/{index-BPXg1Yf6.js → index-CfXEGjc7.js} +39 -19
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +21 -20
  25. package/package.json +1 -1
  26. package/src/components/base/CkDropZoneOverlay/CkDropZoneIllustration.vue +125 -0
  27. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.spec.ts +104 -0
  28. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.stories.ts +130 -0
  29. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue +272 -0
  30. package/src/components/base/CkDropZoneOverlay/index.ts +2 -0
  31. package/src/components/base/CkDropZoneOverlay/types.ts +93 -0
  32. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +22 -1
  33. package/src/components/base/index.ts +7 -0
  34. package/src/components/index.ts +5 -0
  35. package/src/composables/fileUpload/index.spec.ts +18 -7
  36. package/src/composables/fileUpload/index.ts +39 -19
  37. package/src/index.ts +5 -0
@@ -0,0 +1,130 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
3
+ import CkDropZoneOverlay from './CkDropZoneOverlay.vue';
4
+ import type { CkDropZoneRejection } from './types';
5
+
6
+ const meta: Meta<typeof CkDropZoneOverlay> = {
7
+ title: 'Base/CkDropZoneOverlay',
8
+ component: CkDropZoneOverlay,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'Full-window drag-and-drop overlay. It listens for file drags anywhere on the page, reveals a teleported overlay, validates dropped files against `accept`, `maxFileSizeBytes` and `maxFiles`, then emits the accepted files. Drag a real file from your desktop onto the canvas to see it, or use the "Simulate drag" button to preview the overlay.',
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ accept: { control: 'text' },
21
+ maxFiles: { control: { type: 'number', min: 1, max: 20, step: 1 } },
22
+ disabled: { control: 'boolean' },
23
+ heading: { control: 'text' },
24
+ hint: { control: 'text' },
25
+ showErrors: { control: 'boolean' },
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+
31
+ type Story = StoryObj<typeof CkDropZoneOverlay>;
32
+
33
+ const usageSource = `<script setup lang="ts">
34
+ import { CkDropZoneOverlay } from '@aleph-alpha/chat-kit';
35
+
36
+ const MAX_FILES = 10;
37
+ const isStreaming = false;
38
+
39
+ function onFiles(files: File[]) {
40
+ // hand off to your upload pipeline
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <CkDropZoneOverlay
46
+ :accept="'.txt,.docx,.pdf,.md,.csv'"
47
+ :max-files="MAX_FILES"
48
+ :disabled="isStreaming"
49
+ hint="Add txt, docx, pdf, md, or csv"
50
+ @files="onFiles"
51
+ />
52
+ </template>`;
53
+
54
+ /**
55
+ * The overlay is invisible until a file drag enters the window. The "Simulate
56
+ * drag" button dispatches a synthetic `dragenter` so you can preview the
57
+ * overlay without an OS-level drag; dropping a real file emits `files`.
58
+ */
59
+ export const Default: Story = {
60
+ args: {
61
+ accept: '.txt,.docx,.pdf,.md,.csv',
62
+ maxFiles: 10,
63
+ hint: 'Add txt, docx, pdf, md, or csv',
64
+ },
65
+ render: (args) => ({
66
+ components: { CkDropZoneOverlay },
67
+ setup() {
68
+ const lastFiles = ref<string[]>([]);
69
+ const lastRejections = ref<string[]>([]);
70
+ function simulateDrag() {
71
+ const event = new Event('dragenter', { bubbles: true });
72
+ Object.defineProperty(event, 'dataTransfer', {
73
+ value: { types: ['Files'], files: [] },
74
+ });
75
+ window.dispatchEvent(event);
76
+ }
77
+ function onFiles(files: File[]) {
78
+ lastFiles.value = files.map((f) => f.name);
79
+ }
80
+ function onRejected(rejections: CkDropZoneRejection[]) {
81
+ lastRejections.value = rejections.map((r) => r.message);
82
+ }
83
+ return {
84
+ args,
85
+ lastFiles,
86
+ lastRejections,
87
+ simulateDrag,
88
+ onFiles,
89
+ onRejected,
90
+ };
91
+ },
92
+ template: `
93
+ <div class="min-h-[60vh] bg-background-surface-primary p-8 text-content-on-surface-primary">
94
+ <button
95
+ type="button"
96
+ class="rounded-lg bg-background-surface-secondary px-4 py-2 body-14-medium"
97
+ @click="simulateDrag"
98
+ >
99
+ Simulate drag
100
+ </button>
101
+ <p class="label-12-regular mt-4 text-content-on-surface-muted">
102
+ Last accepted: {{ lastFiles.length ? lastFiles.join(', ') : '—' }}
103
+ </p>
104
+ <p class="label-12-regular mt-1 text-content-on-surface-muted">
105
+ Last rejected: {{ lastRejections.length ? lastRejections.join(' · ') : '—' }}
106
+ </p>
107
+ <CkDropZoneOverlay
108
+ v-bind="args"
109
+ @files="onFiles"
110
+ @rejected="onRejected"
111
+ />
112
+ </div>
113
+ `,
114
+ }),
115
+ parameters: { docs: { source: { code: usageSource } } },
116
+ };
117
+
118
+ /**
119
+ * A single-file zone (`maxFiles="1"`) switches the default heading to its
120
+ * singular form and rejects extra files with a `too-many` reason.
121
+ */
122
+ export const SingleFile: Story = {
123
+ args: {
124
+ maxFiles: 1,
125
+ accept: '.pdf',
126
+ hint: 'Add a pdf',
127
+ },
128
+ render: Default.render,
129
+ parameters: { docs: { source: { code: usageSource } } },
130
+ };
@@ -0,0 +1,272 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
3
+
4
+ import CkDropZoneIllustration from './CkDropZoneIllustration.vue';
5
+ import type {
6
+ CkDropZoneOverlayEmits,
7
+ CkDropZoneOverlayProps,
8
+ CkDropZoneRejection,
9
+ } from './types';
10
+
11
+ defineOptions({
12
+ name: 'CkDropZoneOverlay',
13
+ });
14
+
15
+ const props = withDefaults(defineProps<CkDropZoneOverlayProps>(), {
16
+ accept: '',
17
+ maxFileSizeBytes: Number.POSITIVE_INFINITY,
18
+ maxFiles: Number.POSITIVE_INFINITY,
19
+ disabled: false,
20
+ heading: undefined,
21
+ hint: 'Add txt, docx, pdf, md, or csv',
22
+ ariaLabel: 'Drop files anywhere to attach',
23
+ showErrors: true,
24
+ errorTimeoutMs: 4000,
25
+ });
26
+
27
+ const emit = defineEmits<CkDropZoneOverlayEmits>();
28
+
29
+ const visible = ref(false);
30
+ /**
31
+ * `dragenter`/`dragleave` fire when crossing *any* element boundary, not just
32
+ * the page boundary. We count enters/leaves so the overlay stays open while
33
+ * the cursor moves over child elements and only closes when the drag truly
34
+ * leaves the window. See https://stackoverflow.com/a/21002544.
35
+ */
36
+ const dragCounter = ref(0);
37
+ const errors = ref<string[]>([]);
38
+ let errorTimer: ReturnType<typeof setTimeout> | null = null;
39
+
40
+ // --- accept parsing --------------------------------------------------------
41
+
42
+ interface ParsedAccept {
43
+ extensions: string[];
44
+ mimes: string[];
45
+ /** Wildcard MIME prefixes like `image/` (from `image/*`). */
46
+ wildcards: string[];
47
+ }
48
+
49
+ const parsedAccept = computed<ParsedAccept>(() => {
50
+ const extensions: string[] = [];
51
+ const mimes: string[] = [];
52
+ const wildcards: string[] = [];
53
+ for (const raw of props.accept.split(',')) {
54
+ const token = raw.trim().toLowerCase();
55
+ if (!token) continue;
56
+ if (token.startsWith('.')) {
57
+ extensions.push(token);
58
+ } else if (token.endsWith('/*')) {
59
+ wildcards.push(token.slice(0, -1));
60
+ } else {
61
+ mimes.push(token);
62
+ }
63
+ }
64
+ return { extensions, mimes, wildcards };
65
+ });
66
+
67
+ function fileMatchesAccept(file: File): boolean {
68
+ const { extensions, mimes, wildcards } = parsedAccept.value;
69
+ // Empty accept = allow everything.
70
+ if (!extensions.length && !mimes.length && !wildcards.length) return true;
71
+ const name = file.name.toLowerCase();
72
+ const type = file.type.toLowerCase();
73
+ if (extensions.some((e) => name.endsWith(e))) return true;
74
+ if (type && mimes.includes(type)) return true;
75
+ if (type && wildcards.some((w) => type.startsWith(w))) return true;
76
+ return false;
77
+ }
78
+
79
+ function formatBytes(bytes: number): string {
80
+ const mb = bytes / (1024 * 1024);
81
+ if (mb >= 1) return `${mb.toFixed(0)} MB`;
82
+ return `${Math.max(1, Math.round(bytes / 1024))} KB`;
83
+ }
84
+
85
+ const isSingle = computed(() => props.maxFiles === 1);
86
+
87
+ const resolvedHeading = computed(
88
+ () =>
89
+ props.heading ??
90
+ (isSingle.value
91
+ ? 'Drop a file here to add to chat'
92
+ : 'Drop files here to add to chat'),
93
+ );
94
+
95
+ // --- drag detection --------------------------------------------------------
96
+
97
+ function isFileDrag(e: DragEvent): boolean {
98
+ const types = e.dataTransfer?.types;
99
+ if (!types) return false;
100
+ // 'Files' is the standard token; Firefox also exposes 'application/x-moz-file'.
101
+ for (const t of types) {
102
+ if (t === 'Files' || t.toLowerCase().includes('file')) return true;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ function onWindowDragEnter(e: DragEvent) {
108
+ if (props.disabled || !isFileDrag(e)) return;
109
+ dragCounter.value += 1;
110
+ visible.value = true;
111
+ }
112
+
113
+ function onWindowDragOver(e: DragEvent) {
114
+ if (props.disabled || !visible.value || !isFileDrag(e)) return;
115
+ e.preventDefault();
116
+ }
117
+
118
+ function onWindowDragLeave() {
119
+ if (!visible.value) return;
120
+ dragCounter.value = Math.max(0, dragCounter.value - 1);
121
+ if (dragCounter.value === 0) hide();
122
+ }
123
+
124
+ function onWindowDrop(e: DragEvent) {
125
+ if (props.disabled || !visible.value || !isFileDrag(e)) return;
126
+ e.preventDefault();
127
+ hide();
128
+ }
129
+
130
+ function hide() {
131
+ dragCounter.value = 0;
132
+ visible.value = false;
133
+ }
134
+
135
+ // --- overlay drop handler --------------------------------------------------
136
+
137
+ function onOverlayDrop(e: DragEvent) {
138
+ e.preventDefault();
139
+ const dropped = Array.from(e.dataTransfer?.files ?? []);
140
+ hide();
141
+ if (!dropped.length) return;
142
+
143
+ const accepted: File[] = [];
144
+ const rejected: CkDropZoneRejection[] = [];
145
+
146
+ for (const file of dropped) {
147
+ if (!fileMatchesAccept(file)) {
148
+ rejected.push({
149
+ file,
150
+ reason: 'unsupported-type',
151
+ message: `"${file.name}" — unsupported file type`,
152
+ });
153
+ continue;
154
+ }
155
+ if (file.size > props.maxFileSizeBytes) {
156
+ rejected.push({
157
+ file,
158
+ reason: 'too-large',
159
+ message: `"${file.name}" — exceeds ${formatBytes(props.maxFileSizeBytes)} limit`,
160
+ });
161
+ continue;
162
+ }
163
+ accepted.push(file);
164
+ }
165
+
166
+ // Enforce the per-drop cap: anything beyond `maxFiles` is dropped with a
167
+ // single consolidated rejection so the user understands why.
168
+ let toEmit = accepted;
169
+ if (accepted.length > props.maxFiles) {
170
+ toEmit = accepted.slice(0, props.maxFiles);
171
+ rejected.push({
172
+ reason: 'too-many',
173
+ message: isSingle.value
174
+ ? 'Only one file at a time — kept the first.'
175
+ : `Only up to ${props.maxFiles} files at a time — kept the first ${props.maxFiles}.`,
176
+ });
177
+ }
178
+
179
+ if (rejected.length) {
180
+ emit('rejected', rejected);
181
+ if (props.showErrors) pushErrors(rejected.map((r) => r.message));
182
+ }
183
+ if (toEmit.length) emit('files', toEmit);
184
+ }
185
+
186
+ function pushErrors(messages: string[]) {
187
+ errors.value = [...errors.value, ...messages];
188
+ if (errorTimer) clearTimeout(errorTimer);
189
+ errorTimer = setTimeout(() => {
190
+ errors.value = [];
191
+ }, props.errorTimeoutMs);
192
+ }
193
+
194
+ // --- lifecycle -------------------------------------------------------------
195
+
196
+ onMounted(() => {
197
+ window.addEventListener('dragenter', onWindowDragEnter);
198
+ window.addEventListener('dragover', onWindowDragOver);
199
+ window.addEventListener('dragleave', onWindowDragLeave);
200
+ window.addEventListener('drop', onWindowDrop);
201
+ });
202
+
203
+ onBeforeUnmount(() => {
204
+ window.removeEventListener('dragenter', onWindowDragEnter);
205
+ window.removeEventListener('dragover', onWindowDragOver);
206
+ window.removeEventListener('dragleave', onWindowDragLeave);
207
+ window.removeEventListener('drop', onWindowDrop);
208
+ if (errorTimer) clearTimeout(errorTimer);
209
+ });
210
+ </script>
211
+
212
+ <template>
213
+ <Teleport to="body">
214
+ <Transition name="ck-drop-overlay">
215
+ <!-- The overlay IS the drop target; `role="dialog"` describes its
216
+ transient appearance for AT users, but the lint rule still flags
217
+ the drop handler as a "static element interaction". -->
218
+ <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
219
+ <div
220
+ v-if="visible"
221
+ data-slot="drop-zone-overlay"
222
+ class="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background-surface-secondary/80 backdrop-blur-sm p-6"
223
+ role="dialog"
224
+ aria-modal="false"
225
+ :aria-label="ariaLabel"
226
+ @dragover.prevent
227
+ @drop="onOverlayDrop"
228
+ >
229
+ <div
230
+ class="pointer-events-none flex h-full w-full flex-col items-center justify-center gap-4 text-content-on-surface-primary"
231
+ >
232
+ <!-- Replace the whole overlay surface (illustration + copy). -->
233
+ <slot>
234
+ <CkDropZoneIllustration class="h-36 w-52" />
235
+ <div class="flex flex-col items-center gap-1 text-center">
236
+ <p class="body-16-bold">{{ resolvedHeading }}</p>
237
+ <p v-if="hint" class="body-16-regular">{{ hint }}</p>
238
+ </div>
239
+ </slot>
240
+ </div>
241
+ </div>
242
+ </Transition>
243
+
244
+ <Transition name="ck-drop-overlay">
245
+ <ul
246
+ v-if="showErrors && errors.length"
247
+ data-slot="drop-zone-errors"
248
+ class="pointer-events-none fixed bottom-6 left-1/2 z-50 max-w-[36rem] -translate-x-1/2 flex list-inside list-disc flex-col gap-1 rounded-md border border-border-surface-default bg-background-surface-default p-3 text-sm text-content-on-surface-secondary"
249
+ role="alert"
250
+ >
251
+ <li v-for="(msg, i) in errors" :key="i">{{ msg }}</li>
252
+ </ul>
253
+ </Transition>
254
+ </Teleport>
255
+ </template>
256
+
257
+ <style scoped>
258
+ .ck-drop-overlay-enter-active,
259
+ .ck-drop-overlay-leave-active {
260
+ transition: opacity 150ms ease;
261
+ }
262
+ .ck-drop-overlay-enter-from,
263
+ .ck-drop-overlay-leave-to {
264
+ opacity: 0;
265
+ }
266
+ @media (prefers-reduced-motion: reduce) {
267
+ .ck-drop-overlay-enter-active,
268
+ .ck-drop-overlay-leave-active {
269
+ transition: none;
270
+ }
271
+ }
272
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as CkDropZoneOverlay } from './CkDropZoneOverlay.vue';
2
+ export type * from './types';
@@ -0,0 +1,93 @@
1
+ /** Why a dropped file was rejected. */
2
+ export type CkDropZoneRejectionReason =
3
+ | 'unsupported-type'
4
+ | 'too-large'
5
+ | 'too-many';
6
+
7
+ export interface CkDropZoneRejection {
8
+ /** The offending file, when the rejection concerns a specific file. */
9
+ file?: File;
10
+ /** Machine-readable reason, for consumers building their own messaging. */
11
+ reason: CkDropZoneRejectionReason;
12
+ /** Default human-readable (English) message used by the built-in toast. */
13
+ message: string;
14
+ }
15
+
16
+ /**
17
+ * Full-window drag-and-drop overlay. Listens for file drags anywhere on the
18
+ * page, reveals a teleported overlay, validates dropped files against `accept`,
19
+ * `maxFileSizeBytes` and `maxFiles`, and emits the accepted files.
20
+ *
21
+ * Purely presentational and self-contained: it owns its own drag state and has
22
+ * no knowledge of the host app. All limits and copy are configurable, and
23
+ * rejection handling can be delegated to the consumer via the `rejected` event.
24
+ *
25
+ * @category Form Inputs
26
+ * @useCases page-level file attachment, drag-and-drop upload, chat composer
27
+ * @keywords drag and drop, drop zone, file upload, overlay, attachments
28
+ * @related CkFileItemsList, CkPromptInput
29
+ */
30
+ export interface CkDropZoneOverlayProps {
31
+ /**
32
+ * HTML `accept` string used to validate dropped files (extensions, MIME
33
+ * types and `type/*` wildcards). Empty string accepts everything.
34
+ * @default ''
35
+ */
36
+ accept?: string;
37
+ /**
38
+ * Reject files larger than this many bytes.
39
+ * @default Number.POSITIVE_INFINITY
40
+ */
41
+ maxFileSizeBytes?: number;
42
+ /**
43
+ * Maximum number of files accepted per drop. Excess files are dropped and
44
+ * reported as a single rejection.
45
+ * @default Number.POSITIVE_INFINITY
46
+ */
47
+ maxFiles?: number;
48
+ /**
49
+ * When true, the overlay never appears and window drag events are ignored,
50
+ * so the page behaves normally (e.g. while a response is streaming or the
51
+ * attachment limit is already reached).
52
+ * @default false
53
+ */
54
+ disabled?: boolean;
55
+ /**
56
+ * Heading text. Defaults to a sensible singular/plural string derived from
57
+ * `maxFiles` (e.g. "Drop a file here to add to chat" vs "Drop files here to
58
+ * add to chat").
59
+ */
60
+ heading?: string;
61
+ /**
62
+ * Secondary hint shown under the heading (e.g. supported formats / limits).
63
+ * Pass an empty string to hide it.
64
+ * @default 'Add txt, docx, pdf, md, or csv'
65
+ */
66
+ hint?: string;
67
+ /**
68
+ * `aria-label` for the overlay dialog.
69
+ * @default 'Drop files anywhere to attach'
70
+ */
71
+ ariaLabel?: string;
72
+ /**
73
+ * Render the built-in error toast for rejected files. Set to `false` to take
74
+ * over messaging via the `rejected` event.
75
+ * @default true
76
+ */
77
+ showErrors?: boolean;
78
+ /**
79
+ * How long the built-in error toast stays visible, in milliseconds.
80
+ * @default 4000
81
+ */
82
+ errorTimeoutMs?: number;
83
+ }
84
+
85
+ export interface CkDropZoneOverlayEmits {
86
+ /** Files that passed validation, capped to `maxFiles`. */
87
+ (event: 'files', files: File[]): void;
88
+ /**
89
+ * Rejected files / drops. Emitted alongside the built-in toast so consumers
90
+ * can integrate their own notification system (and disable `showErrors`).
91
+ */
92
+ (event: 'rejected', rejections: CkDropZoneRejection[]): void;
93
+ }
@@ -215,7 +215,7 @@ function handleSubmit(): void {
215
215
  v-for="reason in reasons"
216
216
  :key="reason.code"
217
217
  :value="reason.code"
218
- class="ck-feedback-dialog__chip"
218
+ class="ck-feedback-dialog__chip shadow-none"
219
219
  >
220
220
  {{ reason.label }}
221
221
  </UiToggleGroupItem>
@@ -229,6 +229,7 @@ function handleSubmit(): void {
229
229
  :id="freeTextId"
230
230
  v-model="freeText"
231
231
  name="ck-feedback-dialog-free-text"
232
+ class="ck-feedback-dialog__freetext"
232
233
  :placeholder="freeTextPlaceholder"
233
234
  :disabled="disabled || submitting"
234
235
  :maxlength="maxFreeTextLength"
@@ -279,6 +280,26 @@ function handleSubmit(): void {
279
280
  line-height: 1rem;
280
281
  }
281
282
 
283
+ /* Selected chip: soft-active treatment (per Figma). Token vars keep it
284
+ * theme-aware; scoped here because the data-[state=on]:bg-* utility isn't
285
+ * emitted by the consumer's UnoCSS build. The :hover selector holds the
286
+ * styling when hovering a selected chip (the outline variant ships a
287
+ * data-[state=on]:hover override of equal specificity otherwise). */
288
+ .ck-feedback-dialog__chip[data-state='on'],
289
+ .ck-feedback-dialog__chip[data-state='on']:hover {
290
+ border-color: oklch(var(--accent-default));
291
+ background-color: oklch(var(--background-accent-soft));
292
+ color: oklch(var(--content-on-accent-soft));
293
+ }
294
+
295
+ /*
296
+ * The free-text field stays vertically resizable, but cap its height so
297
+ * dragging the resize handle can't push it past the dialog bounds.
298
+ */
299
+ .ck-feedback-dialog__freetext {
300
+ max-height: 11rem;
301
+ }
302
+
282
303
  .ck-feedback-dialog__response :deep(p),
283
304
  .ck-feedback-dialog__response :deep(li) {
284
305
  line-height: 1.5rem;
@@ -3,6 +3,13 @@ export { CkAgentMessage } from './CkAgentMessage';
3
3
  export { CkConversationLayout } from './CkConversationLayout';
4
4
  export type { CkConversationLayoutProps } from './CkConversationLayout';
5
5
  export { CkCopyMessageActionButton } from './CkCopyMessageActionButton';
6
+ export { CkDropZoneOverlay } from './CkDropZoneOverlay';
7
+ export type {
8
+ CkDropZoneOverlayProps,
9
+ CkDropZoneOverlayEmits,
10
+ CkDropZoneRejection,
11
+ CkDropZoneRejectionReason,
12
+ } from './CkDropZoneOverlay';
6
13
  export { CkFeedbackButton } from './CkFeedbackButton';
7
14
  export type {
8
15
  CkFeedbackButtonProps,
@@ -3,6 +3,7 @@ export {
3
3
  CkAgentMessage,
4
4
  CkConversationLayout,
5
5
  CkCopyMessageActionButton,
6
+ CkDropZoneOverlay,
6
7
  CkFeedbackButton,
7
8
  CkFeedbackDialog,
8
9
  CkFileItemsList,
@@ -21,6 +22,10 @@ export {
21
22
  } from './base';
22
23
  export type {
23
24
  CkConversationLayoutProps,
25
+ CkDropZoneOverlayProps,
26
+ CkDropZoneOverlayEmits,
27
+ CkDropZoneRejection,
28
+ CkDropZoneRejectionReason,
24
29
  CkFeedbackButtonProps,
25
30
  CkFeedbackDialogEmits,
26
31
  CkFeedbackDialogProps,
@@ -63,11 +63,11 @@ describe('fileUpload index', () => {
63
63
  expect(workflowConfig.workflow_name).toBe('DocumentAI');
64
64
  });
65
65
 
66
- it('configures workflowConfig definition', () => {
66
+ it('does not send the legacy workflow definition field', () => {
67
+ // The search-stores API dropped the redundant `definition` field
68
+ // (it duplicated `workflow_name`); sending it now breaks store creation.
67
69
  const { workflowConfig } = defaultDocumentUploadConfig;
68
- expect(workflowConfig.definition).toEqual({
69
- workflow_name: 'DocumentAI',
70
- });
70
+ expect(workflowConfig.definition).toBeUndefined();
71
71
  });
72
72
 
73
73
  it('configures initialization parameters', () => {
@@ -83,7 +83,16 @@ describe('fileUpload index', () => {
83
83
  .parameters as Record<string, unknown>;
84
84
  const extraction = params.document_extraction as Record<string, unknown>;
85
85
 
86
- expect(extraction.pdf_reader).toEqual({ kind: 'pypdf', batch_size: 10 });
86
+ expect(extraction.pdf_reader).toEqual({
87
+ kind: 'vision',
88
+ batch_size: 10,
89
+ ignore_headers_footers: true,
90
+ max_tokens: 16384,
91
+ model: 'gemini-2.5-flash',
92
+ retries: 3,
93
+ timeout: 120,
94
+ workers: 4,
95
+ });
87
96
  expect(extraction.docx_reader).toEqual({
88
97
  kind: 'docx',
89
98
  batch_size: 10,
@@ -121,10 +130,12 @@ describe('fileUpload index', () => {
121
130
  unknown
122
131
  >;
123
132
  expect(chunkExtraction.chunker).toEqual({
124
- kind: 'naive',
133
+ kind: 'recursive',
125
134
  batch_size: 1,
126
135
  include_image_descriptions: false,
127
- chunk_size: 200,
136
+ chunk_overlap: 200,
137
+ chunk_size: 1200,
138
+ separators: ['\n\n', '\n', ' ', '.', ',', ''],
128
139
  });
129
140
  });
130
141