@gradio/multimodaltextbox 0.10.22-dev.0 → 0.11.0-dev.2

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.
@@ -1,247 +1,332 @@
1
- <script>import {
2
- onMount,
3
- beforeUpdate,
4
- afterUpdate,
5
- createEventDispatcher,
6
- tick
7
- } from "svelte";
8
- import { text_area_resize, resize } from "../shared/utils";
9
- import { BlockTitle } from "@gradio/atoms";
10
- import { Upload } from "@gradio/upload";
11
- import { Image } from "@gradio/image/shared";
12
- import {
13
- Clear,
14
- File,
15
- Music,
16
- Paperclip,
17
- Video,
18
- Send,
19
- Square,
20
- Microphone
21
- } from "@gradio/icons";
22
- import InteractiveAudio from "../../audio/interactive/InteractiveAudio.svelte";
23
- export let value = {
24
- text: "",
25
- files: []
26
- };
27
- export let value_is_output = false;
28
- export let lines = 1;
29
- export let i18n;
30
- export let placeholder = "Type here...";
31
- export let disabled = false;
32
- export let label;
33
- export let info = void 0;
34
- export let show_label = true;
35
- export let max_lines;
36
- export let submit_btn = null;
37
- export let stop_btn = null;
38
- export let rtl = false;
39
- export let autofocus = false;
40
- export let text_align = void 0;
41
- export let autoscroll = true;
42
- export let root;
43
- export let file_types_string = null;
44
- export let max_file_size = null;
45
- export let upload;
46
- export let stream_handler;
47
- export let file_count = "multiple";
48
- export let max_plain_text_length = 1e3;
49
- export let waveform_settings;
50
- export let waveform_options = {
51
- show_recording_waveform: true
52
- };
53
- export let sources_string = "upload";
54
- export let active_source = null;
55
- export let html_attributes = null;
56
- let upload_component;
57
- let el;
58
- let can_scroll;
59
- let previous_scroll_top = 0;
60
- let user_has_scrolled_up = false;
61
- export let dragging = false;
62
- let uploading = false;
63
- let oldValue = value?.text ?? "";
64
- let recording = false;
65
- $: sources = sources_string.split(",").map((s) => s.trim()).filter((s) => s === "upload" || s === "microphone");
66
- $: file_types = file_types_string ? file_types_string.split(",").map((s) => s.trim()) : null;
67
- $: dispatch("drag", dragging);
68
- let mic_audio = null;
69
- let full_container;
70
- $: if (oldValue !== value.text) {
71
- dispatch("change", value);
72
- oldValue = value.text;
73
- }
74
- $: if (value === null) value = { text: "", files: [] };
75
- $: value, el && lines !== max_lines && resize(el, lines, max_lines);
76
- const dispatch = createEventDispatcher();
77
- beforeUpdate(() => {
78
- can_scroll = el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100;
79
- });
80
- const scroll = () => {
81
- if (can_scroll && autoscroll && !user_has_scrolled_up) {
82
- el.scrollTo(0, el.scrollHeight);
83
- }
84
- };
85
- async function handle_change() {
86
- dispatch("change", value);
87
- if (!value_is_output) {
88
- dispatch("input");
89
- }
90
- }
91
- onMount(() => {
92
- if (autofocus && el !== null) {
93
- el.focus();
94
- }
95
- });
96
- const after_update = () => {
97
- if (can_scroll && autoscroll) {
98
- scroll();
99
- }
100
- if (autofocus && el) {
101
- el.focus();
102
- }
103
- };
104
- afterUpdate(after_update);
105
- function handle_select(event) {
106
- const target = event.target;
107
- const text = target.value;
108
- const index = [
109
- target.selectionStart,
110
- target.selectionEnd
111
- ];
112
- dispatch("select", { value: text.substring(...index), index });
113
- }
114
- async function handle_keypress(e) {
115
- await tick();
116
- if (e.key === "Enter" && e.shiftKey && lines > 1) {
117
- e.preventDefault();
118
- dispatch("submit");
119
- } else if (e.key === "Enter" && !e.shiftKey && lines === 1 && max_lines >= 1) {
120
- e.preventDefault();
121
- dispatch("submit");
122
- active_source = null;
123
- if (mic_audio) {
124
- value.files.push(mic_audio);
125
- value = value;
126
- mic_audio = null;
127
- }
128
- }
129
- }
130
- function handle_scroll(event) {
131
- const target = event.target;
132
- const current_scroll_top = target.scrollTop;
133
- if (current_scroll_top < previous_scroll_top) {
134
- user_has_scrolled_up = true;
135
- }
136
- previous_scroll_top = current_scroll_top;
137
- const max_scroll_top = target.scrollHeight - target.clientHeight;
138
- const user_has_scrolled_to_bottom = current_scroll_top >= max_scroll_top;
139
- if (user_has_scrolled_to_bottom) {
140
- user_has_scrolled_up = false;
141
- }
142
- }
143
- async function handle_upload({
144
- detail
145
- }) {
146
- handle_change();
147
- if (Array.isArray(detail)) {
148
- for (let file of detail) {
149
- value.files.push(file);
150
- }
151
- value = value;
152
- } else {
153
- value.files.push(detail);
154
- value = value;
155
- }
156
- await tick();
157
- dispatch("change", value);
158
- dispatch("upload", detail);
159
- }
160
- function remove_thumbnail(event, index) {
161
- handle_change();
162
- event.stopPropagation();
163
- value.files.splice(index, 1);
164
- value = value;
165
- }
166
- function handle_upload_click() {
167
- upload_component.open_upload();
168
- }
169
- function handle_stop() {
170
- dispatch("stop");
171
- }
172
- function handle_submit() {
173
- dispatch("submit");
174
- active_source = null;
175
- if (mic_audio) {
176
- value.files.push(mic_audio);
177
- value = value;
178
- mic_audio = null;
179
- }
180
- }
181
- async function handle_paste(event) {
182
- if (!event.clipboardData) return;
183
- const items = event.clipboardData.items;
184
- const text = event.clipboardData.getData("text");
185
- if (text && text.length > max_plain_text_length) {
186
- event.preventDefault();
187
- const file = new window.File([text], "pasted_text.txt", {
188
- type: "text/plain",
189
- lastModified: Date.now()
190
- });
191
- if (upload_component) {
192
- upload_component.load_files([file]);
193
- }
194
- return;
195
- }
196
- for (let index in items) {
197
- const item = items[index];
198
- if (item.kind === "file" && item.type.includes("image")) {
199
- const blob = item.getAsFile();
200
- if (blob) upload_component.load_files([blob]);
201
- }
202
- }
203
- }
204
- function handle_dragenter(event) {
205
- event.preventDefault();
206
- dragging = true;
207
- }
208
- function handle_dragleave(event) {
209
- event.preventDefault();
210
- const rect = full_container.getBoundingClientRect();
211
- const { clientX, clientY } = event;
212
- if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) {
213
- dragging = false;
214
- }
215
- }
216
- function handle_drop(event) {
217
- event.preventDefault();
218
- dragging = false;
219
- if (event.dataTransfer && event.dataTransfer.files) {
220
- const files = Array.from(event.dataTransfer.files);
221
- if (file_types) {
222
- const valid_files = files.filter((file) => {
223
- return file_types.some((type) => {
224
- if (type.startsWith(".")) {
225
- return file.name.toLowerCase().endsWith(type.toLowerCase());
226
- }
227
- return file.type.match(new RegExp(type.replace("*", ".*")));
228
- });
229
- });
230
- const invalid_files = files.length - valid_files.length;
231
- if (invalid_files > 0) {
232
- dispatch(
233
- "error",
234
- `${invalid_files} file(s) were rejected. Accepted formats: ${file_types.join(", ")}`
235
- );
236
- }
237
- if (valid_files.length > 0) {
238
- upload_component.load_files(valid_files);
239
- }
240
- } else {
241
- upload_component.load_files(files);
242
- }
243
- }
244
- }
1
+ <script lang="ts">
2
+ import {
3
+ onMount,
4
+ beforeUpdate,
5
+ afterUpdate,
6
+ createEventDispatcher,
7
+ tick
8
+ } from "svelte";
9
+ import { text_area_resize, resize } from "../shared/utils";
10
+ import { BlockTitle } from "@gradio/atoms";
11
+ import { Upload } from "@gradio/upload";
12
+ import { Image } from "@gradio/image/shared";
13
+ import type { I18nFormatter } from "js/core/src/gradio_helper";
14
+ import type { FileData, Client } from "@gradio/client";
15
+ import type { WaveformOptions } from "@gradio/audio";
16
+ import {
17
+ Clear,
18
+ File,
19
+ Music,
20
+ Paperclip,
21
+ Video,
22
+ ArrowUp,
23
+ Square,
24
+ Microphone
25
+ } from "@gradio/icons";
26
+ import type { SelectData } from "@gradio/utils";
27
+ import { BaseInteractiveAudio as InteractiveAudio } from "@gradio/audio";
28
+ import type { InputHTMLAttributes } from "./types";
29
+
30
+ export let value: { text: string; files: FileData[] } = {
31
+ text: "",
32
+ files: []
33
+ };
34
+
35
+ export let value_is_output = false;
36
+ export let lines = 1;
37
+ export let i18n: I18nFormatter;
38
+ export let placeholder = "Type here...";
39
+ export let disabled = false;
40
+ export let label: string;
41
+ export let info: string | undefined = undefined;
42
+ export let show_label = true;
43
+ export let max_lines: number;
44
+ export let submit_btn: string | boolean | null = null;
45
+ export let stop_btn: string | boolean | null = null;
46
+ export let rtl = false;
47
+ export let autofocus = false;
48
+ export let text_align: "left" | "right" | undefined = undefined;
49
+ export let autoscroll = true;
50
+ export let root: string;
51
+ export let file_types_string: string | null = null;
52
+ export let max_file_size: number | null = null;
53
+ export let upload: Client["upload"];
54
+ export let stream_handler: Client["stream"];
55
+ export let file_count: "single" | "multiple" | "directory" = "multiple";
56
+ export let max_plain_text_length = 1000;
57
+ export let waveform_settings: Record<string, any>;
58
+ export let waveform_options: WaveformOptions = {
59
+ show_recording_waveform: true
60
+ };
61
+ export let sources_string:
62
+ | "upload"
63
+ | "upload,microphone"
64
+ | "microphone"
65
+ | "microphone,upload" = "upload";
66
+ export let active_source: "microphone" | null = null;
67
+ export let html_attributes: InputHTMLAttributes | null = null;
68
+ export let upload_promise: Promise<any> | null = null;
69
+
70
+ let upload_component: Upload;
71
+ let el: HTMLTextAreaElement | HTMLInputElement;
72
+ let can_scroll: boolean;
73
+ let previous_scroll_top = 0;
74
+ let user_has_scrolled_up = false;
75
+ export let dragging = false;
76
+ let uploading = false;
77
+ // value can be null in multimodalchatinterface when loading a deep link
78
+ let oldValue = value?.text ?? "";
79
+ let recording = false;
80
+
81
+ $: sources = sources_string
82
+ .split(",")
83
+ .map((s) => s.trim())
84
+ .filter((s) => s === "upload" || s === "microphone") as (
85
+ | "upload"
86
+ | "microphone"
87
+ )[];
88
+ $: file_types = file_types_string
89
+ ? file_types_string.split(",").map((s) => s.trim())
90
+ : null;
91
+ $: dispatch("drag", dragging);
92
+ $: show_upload =
93
+ sources &&
94
+ sources.includes("upload") &&
95
+ !(file_count === "single" && value.files.length > 0);
96
+ let mic_audio: FileData | null = null;
97
+
98
+ let full_container: HTMLDivElement;
99
+
100
+ $: if (oldValue !== value.text) {
101
+ dispatch("change", value);
102
+ oldValue = value.text;
103
+ }
104
+
105
+ $: if (value === null) value = { text: "", files: [] };
106
+ $: (value, el && lines !== max_lines && resize(el, lines, max_lines));
107
+
108
+ const dispatch = createEventDispatcher<{
109
+ change: typeof value;
110
+ submit: undefined;
111
+ stop: undefined;
112
+ blur: undefined;
113
+ select: SelectData;
114
+ input: undefined;
115
+ focus: undefined;
116
+ drag: boolean;
117
+ upload: FileData[] | FileData;
118
+ clear: undefined;
119
+ load: FileData[] | FileData;
120
+ error: string;
121
+ start_recording: undefined;
122
+ pause_recording: undefined;
123
+ stop_recording: undefined;
124
+ }>();
125
+
126
+ beforeUpdate(() => {
127
+ can_scroll = el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100;
128
+ });
129
+
130
+ const scroll = (): void => {
131
+ if (can_scroll && autoscroll && !user_has_scrolled_up) {
132
+ el.scrollTo(0, el.scrollHeight);
133
+ }
134
+ };
135
+
136
+ async function handle_change(): Promise<void> {
137
+ dispatch("change", value);
138
+ if (!value_is_output) {
139
+ dispatch("input");
140
+ }
141
+ }
142
+
143
+ onMount(() => {
144
+ if (autofocus && el !== null) {
145
+ el.focus();
146
+ }
147
+ });
148
+
149
+ const after_update = (): void => {
150
+ if (can_scroll && autoscroll) {
151
+ scroll();
152
+ }
153
+ if (autofocus && el) {
154
+ el.focus();
155
+ }
156
+ };
157
+
158
+ afterUpdate(after_update);
159
+
160
+ function handle_select(event: Event): void {
161
+ const target: HTMLTextAreaElement | HTMLInputElement = event.target as
162
+ | HTMLTextAreaElement
163
+ | HTMLInputElement;
164
+ const text = target.value;
165
+ const index: [number, number] = [
166
+ target.selectionStart as number,
167
+ target.selectionEnd as number
168
+ ];
169
+ dispatch("select", { value: text.substring(...index), index: index });
170
+ }
171
+
172
+ async function handle_keypress(e: KeyboardEvent): Promise<void> {
173
+ if (e.key === "Enter" && e.shiftKey && lines > 1) {
174
+ e.preventDefault();
175
+ await tick();
176
+ dispatch("submit");
177
+ } else if (
178
+ e.key === "Enter" &&
179
+ !e.shiftKey &&
180
+ lines === 1 &&
181
+ max_lines >= 1
182
+ ) {
183
+ e.preventDefault();
184
+ await tick();
185
+ dispatch("submit");
186
+ active_source = null;
187
+ add_mic_audio_to_files();
188
+ }
189
+ }
190
+
191
+ function handle_scroll(event: Event): void {
192
+ const target = event.target as HTMLElement;
193
+ const current_scroll_top = target.scrollTop;
194
+ if (current_scroll_top < previous_scroll_top) {
195
+ user_has_scrolled_up = true;
196
+ }
197
+ previous_scroll_top = current_scroll_top;
198
+
199
+ const max_scroll_top = target.scrollHeight - target.clientHeight;
200
+ const user_has_scrolled_to_bottom = current_scroll_top >= max_scroll_top;
201
+ if (user_has_scrolled_to_bottom) {
202
+ user_has_scrolled_up = false;
203
+ }
204
+ }
205
+
206
+ async function handle_upload({
207
+ detail
208
+ }: CustomEvent<FileData>): Promise<void> {
209
+ handle_change();
210
+ if (Array.isArray(detail)) {
211
+ for (let file of detail) {
212
+ value.files.push(file);
213
+ }
214
+ value = value;
215
+ } else {
216
+ value.files.push(detail);
217
+ value = value;
218
+ }
219
+ await tick();
220
+ dispatch("change", value);
221
+ dispatch("upload", detail);
222
+ }
223
+
224
+ function remove_thumbnail(event: MouseEvent, index: number): void {
225
+ handle_change();
226
+ event.stopPropagation();
227
+ value.files.splice(index, 1);
228
+ value = value;
229
+ }
230
+
231
+ function handle_upload_click(): void {
232
+ upload_component.open_upload();
233
+ }
234
+
235
+ function handle_stop(): void {
236
+ dispatch("stop");
237
+ }
238
+
239
+ function add_mic_audio_to_files(): void {
240
+ if (mic_audio) {
241
+ value.files.push(mic_audio);
242
+ value = value;
243
+ mic_audio = null;
244
+ }
245
+ }
246
+
247
+ function handle_submit(): void {
248
+ dispatch("submit");
249
+ active_source = null;
250
+ add_mic_audio_to_files();
251
+ }
252
+
253
+ async function handle_paste(event: ClipboardEvent): Promise<void> {
254
+ if (!event.clipboardData) return;
255
+ const items = event.clipboardData.items;
256
+ const text = event.clipboardData.getData("text");
257
+
258
+ if (text && text.length > max_plain_text_length) {
259
+ event.preventDefault();
260
+ const file = new window.File([text], "pasted_text.txt", {
261
+ type: "text/plain",
262
+ lastModified: Date.now()
263
+ });
264
+ if (upload_component) {
265
+ upload_component.load_files([file]);
266
+ }
267
+ return;
268
+ }
269
+
270
+ for (let index in items) {
271
+ const item = items[index];
272
+ if (item.kind === "file" && item.type.includes("image")) {
273
+ const blob = item.getAsFile();
274
+ if (blob) upload_component.load_files([blob]);
275
+ }
276
+ }
277
+ }
278
+
279
+ function handle_dragenter(event: DragEvent): void {
280
+ event.preventDefault();
281
+ dragging = true;
282
+ }
283
+
284
+ function handle_dragleave(event: DragEvent): void {
285
+ event.preventDefault();
286
+ const rect = full_container.getBoundingClientRect();
287
+ const { clientX, clientY } = event;
288
+ if (
289
+ clientX <= rect.left ||
290
+ clientX >= rect.right ||
291
+ clientY <= rect.top ||
292
+ clientY >= rect.bottom
293
+ ) {
294
+ dragging = false;
295
+ }
296
+ }
297
+
298
+ function handle_drop(event: DragEvent): void {
299
+ event.preventDefault();
300
+ dragging = false;
301
+ if (event.dataTransfer && event.dataTransfer.files) {
302
+ const files = Array.from(event.dataTransfer.files);
303
+
304
+ if (file_types) {
305
+ const valid_files = files.filter((file) => {
306
+ return file_types.some((type) => {
307
+ if (type.startsWith(".")) {
308
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
309
+ }
310
+ return file.type.match(new RegExp(type.replace("*", ".*")));
311
+ });
312
+ });
313
+
314
+ const invalid_files = files.length - valid_files.length;
315
+ if (invalid_files > 0) {
316
+ dispatch(
317
+ "error",
318
+ `${invalid_files} file(s) were rejected. Accepted formats: ${file_types.join(", ")}`
319
+ );
320
+ }
321
+
322
+ if (valid_files.length > 0) {
323
+ upload_component.load_files(valid_files);
324
+ }
325
+ } else {
326
+ upload_component.load_files(files);
327
+ }
328
+ }
329
+ }
245
330
  </script>
246
331
 
247
332
  <div
@@ -256,80 +341,42 @@ function handle_drop(event) {
256
341
  aria-label="Multimedia input field"
257
342
  >
258
343
  <BlockTitle {show_label} {info} {rtl}>{label}</BlockTitle>
259
- {#if value.files.length > 0 || uploading}
260
- <div
261
- class="thumbnails scroll-hide"
262
- aria-label="Uploaded files"
263
- data-testid="container_el"
264
- style="display: {value.files.length > 0 || uploading ? 'flex' : 'none'};"
265
- >
266
- {#each value.files as file, index}
267
- <span role="listitem" aria-label="File thumbnail">
268
- <button class="thumbnail-item thumbnail-small">
269
- <button
270
- class:disabled
271
- class="delete-button"
272
- on:click={(event) => remove_thumbnail(event, index)}
273
- ><Clear /></button
274
- >
275
- {#if file.mime_type && file.mime_type.includes("image")}
276
- <Image
277
- src={file.url}
278
- title={null}
279
- alt=""
280
- loading="lazy"
281
- class={"thumbnail-image"}
282
- />
283
- {:else if file.mime_type && file.mime_type.includes("audio")}
284
- <Music />
285
- {:else if file.mime_type && file.mime_type.includes("video")}
286
- <Video />
287
- {:else}
288
- <File />
289
- {/if}
290
- </button>
291
- </span>
292
- {/each}
293
- {#if uploading}
294
- <div class="loader" role="status" aria-label="Uploading"></div>
295
- {/if}
296
- </div>
297
- {/if}
298
- {#if sources && sources.includes("microphone") && active_source === "microphone"}
299
- <InteractiveAudio
300
- on:change={({ detail }) => {
301
- if (detail !== null) {
302
- mic_audio = detail;
303
- }
304
- }}
305
- on:clear={() => {
306
- active_source = null;
307
- }}
308
- on:start_recording={() => dispatch("start_recording")}
309
- on:pause_recording={() => dispatch("pause_recording")}
310
- on:stop_recording={() => dispatch("stop_recording")}
311
- sources={["microphone"]}
312
- class_name="compact-audio"
313
- {recording}
314
- {waveform_settings}
315
- {waveform_options}
316
- {i18n}
317
- {active_source}
318
- {upload}
319
- {stream_handler}
320
- stream_every={1}
321
- editable={true}
322
- {label}
323
- {root}
324
- loop={false}
325
- show_label={false}
326
- buttons={[]}
327
- dragging={false}
328
- />
329
- {/if}
330
344
  <div class="input-container">
331
- {#if sources && sources.includes("upload") && !(file_count === "single" && value.files.length > 0)}
345
+ {#if sources && sources.includes("microphone") && active_source === "microphone"}
346
+ <InteractiveAudio
347
+ on:change={({ detail }) => {
348
+ if (detail !== null) {
349
+ mic_audio = detail;
350
+ }
351
+ }}
352
+ on:clear={() => {
353
+ active_source = null;
354
+ }}
355
+ on:start_recording={() => dispatch("start_recording")}
356
+ on:pause_recording={() => dispatch("pause_recording")}
357
+ on:stop_recording={() => dispatch("stop_recording")}
358
+ sources={["microphone"]}
359
+ class_name="compact-audio"
360
+ {recording}
361
+ {waveform_settings}
362
+ {waveform_options}
363
+ {i18n}
364
+ {active_source}
365
+ {upload}
366
+ {stream_handler}
367
+ stream_every={1}
368
+ editable={true}
369
+ {label}
370
+ {root}
371
+ loop={false}
372
+ show_label={false}
373
+ buttons={[]}
374
+ dragging={false}
375
+ />
376
+ {/if}
377
+ {#if show_upload}
332
378
  <Upload
379
+ bind:upload_promise
333
380
  bind:this={upload_component}
334
381
  on:load={handle_upload}
335
382
  {file_count}
@@ -345,87 +392,147 @@ function handle_drop(event) {
345
392
  {upload}
346
393
  {stream_handler}
347
394
  />
348
- <button
349
- data-testid="upload-button"
350
- class="upload-button"
351
- {disabled}
352
- on:click={disabled ? undefined : handle_upload_click}
353
- ><Paperclip /></button
354
- >
355
395
  {/if}
356
- {#if sources && sources.includes("microphone")}
357
- <button
358
- data-testid="microphone-button"
359
- class="microphone-button"
360
- class:recording
361
- {disabled}
362
- on:click={disabled
363
- ? undefined
364
- : () => {
396
+
397
+ <div
398
+ class="input-wrapper"
399
+ class:has-files={value.files.length > 0 || uploading}
400
+ >
401
+ {#if value.files.length > 0 || uploading || show_upload}
402
+ <div
403
+ class="thumbnails"
404
+ aria-label="Uploaded files"
405
+ data-testid="container_el"
406
+ >
407
+ {#if show_upload}
408
+ <button
409
+ data-testid="upload-button"
410
+ class="upload-button mobile-thumbnail-add"
411
+ {disabled}
412
+ on:click={handle_upload_click}
413
+ aria-label="Upload a file"
414
+ >
415
+ <Paperclip />
416
+ </button>
417
+ {/if}
418
+ {#each value.files as file, index}
419
+ <span
420
+ class="thumbnail-wrapper"
421
+ role="listitem"
422
+ aria-label="File thumbnail"
423
+ >
424
+ <div class="thumbnail-item thumbnail-small">
425
+ {#if file.mime_type && file.mime_type.includes("image")}
426
+ <Image
427
+ src={file.url}
428
+ restProps={{
429
+ title: null,
430
+ alt: "",
431
+ loading: "lazy",
432
+ class: "thumbnail-image"
433
+ }}
434
+ />
435
+ {:else if file.mime_type && file.mime_type.includes("audio")}
436
+ <Music />
437
+ {:else if file.mime_type && file.mime_type.includes("video")}
438
+ <Video />
439
+ {:else}
440
+ <File />
441
+ {/if}
442
+ <button
443
+ class="delete-button"
444
+ on:click={(event) => remove_thumbnail(event, index)}
445
+ aria-label="Remove file"
446
+ >
447
+ <Clear />
448
+ </button>
449
+ </div>
450
+ </span>
451
+ {/each}
452
+ {#if uploading}
453
+ <div class="loader" role="status" aria-label="Uploading"></div>
454
+ {/if}
455
+ </div>
456
+ {/if}
457
+
458
+ <div class="input-row">
459
+ <textarea
460
+ data-testid="textbox"
461
+ use:text_area_resize={{
462
+ text: value.text,
463
+ lines: lines,
464
+ max_lines: max_lines
465
+ }}
466
+ class:no-label={!show_label}
467
+ dir={rtl ? "rtl" : "ltr"}
468
+ bind:value={value.text}
469
+ bind:this={el}
470
+ {placeholder}
471
+ rows={lines}
472
+ {disabled}
473
+ on:keypress={handle_keypress}
474
+ on:blur
475
+ on:select={handle_select}
476
+ on:focus
477
+ on:scroll={handle_scroll}
478
+ on:paste={handle_paste}
479
+ style={text_align ? "text-align: " + text_align : ""}
480
+ autocapitalize={html_attributes?.autocapitalize}
481
+ autocorrect={html_attributes?.autocorrect}
482
+ spellcheck={html_attributes?.spellcheck}
483
+ autocomplete={html_attributes?.autocomplete}
484
+ tabindex={html_attributes?.tabindex}
485
+ enterkeyhint={html_attributes?.enterkeyhint}
486
+ lang={html_attributes?.lang}
487
+ />
488
+
489
+ {#if sources && sources.includes("microphone")}
490
+ <button
491
+ data-testid="microphone-button"
492
+ class="microphone-button"
493
+ class:recording
494
+ {disabled}
495
+ on:click={() => {
365
496
  active_source =
366
497
  active_source !== "microphone" ? "microphone" : null;
367
498
  }}
368
- >
369
- <Microphone />
370
- </button>
371
- {/if}
372
- <textarea
373
- data-testid="textbox"
374
- use:text_area_resize={{
375
- text: value.text,
376
- lines: lines,
377
- max_lines: max_lines
378
- }}
379
- class="scroll-hide"
380
- class:no-label={!show_label}
381
- dir={rtl ? "rtl" : "ltr"}
382
- bind:value={value.text}
383
- bind:this={el}
384
- {placeholder}
385
- rows={lines}
386
- {disabled}
387
- on:keypress={handle_keypress}
388
- on:blur
389
- on:select={handle_select}
390
- on:focus
391
- on:scroll={handle_scroll}
392
- on:paste={handle_paste}
393
- style={text_align ? "text-align: " + text_align : ""}
394
- autocapitalize={html_attributes?.autocapitalize}
395
- autocorrect={html_attributes?.autocorrect}
396
- spellcheck={html_attributes?.spellcheck}
397
- autocomplete={html_attributes?.autocomplete}
398
- tabindex={html_attributes?.tabindex}
399
- enterkeyhint={html_attributes?.enterkeyhint}
400
- lang={html_attributes?.lang}
401
- />
402
- {#if submit_btn}
403
- <button
404
- class="submit-button"
405
- class:padded-button={submit_btn !== true}
406
- {disabled}
407
- on:click={disabled ? undefined : handle_submit}
408
- >
409
- {#if submit_btn === true}
410
- <Send />
411
- {:else}
412
- {submit_btn}
499
+ aria-label="Record audio"
500
+ >
501
+ <Microphone />
502
+ </button>
413
503
  {/if}
414
- </button>
415
- {/if}
416
- {#if stop_btn}
417
- <button
418
- class="stop-button"
419
- class:padded-button={stop_btn !== true}
420
- on:click={handle_stop}
421
- >
422
- {#if stop_btn === true}
423
- <Square fill={"none"} stroke_width={2.5} />
424
- {:else}
425
- {stop_btn}
504
+
505
+ {#if submit_btn}
506
+ <button
507
+ class="submit-button"
508
+ class:padded-button={submit_btn !== true}
509
+ {disabled}
510
+ on:click={handle_submit}
511
+ aria-label="Submit"
512
+ >
513
+ {#if submit_btn === true}
514
+ <ArrowUp />
515
+ {:else}
516
+ {submit_btn}
517
+ {/if}
518
+ </button>
426
519
  {/if}
427
- </button>
428
- {/if}
520
+ {#if stop_btn}
521
+ <button
522
+ class="stop-button"
523
+ class:padded-button={stop_btn !== true}
524
+ on:click={handle_stop}
525
+ aria-label="Stop"
526
+ >
527
+ {#if stop_btn === true}
528
+ <Square fill={"none"} stroke_width={2.5} />
529
+ {:else}
530
+ {stop_btn}
531
+ {/if}
532
+ </button>
533
+ {/if}
534
+ </div>
535
+ </div>
429
536
  </div>
430
537
  </div>
431
538
 
@@ -438,237 +545,299 @@ function handle_drop(event) {
438
545
  }
439
546
 
440
547
  .full-container.dragging {
441
- border: 1px solid var(--color-accent);
442
- border-radius: calc(var(--radius-sm) - 1px);
548
+ border-color: var(--color-accent);
549
+ border-radius: calc(var(--radius-sm) - var(--size-px));
443
550
  }
444
551
 
445
552
  .full-container.dragging::after {
446
553
  content: "";
447
554
  position: absolute;
448
- top: 0;
449
- left: 0;
450
- right: 0;
451
- bottom: 0;
555
+ inset: 0;
452
556
  pointer-events: none;
453
557
  }
454
558
 
455
559
  .input-container {
456
560
  display: flex;
457
561
  position: relative;
562
+ flex-direction: column;
563
+ gap: 0;
564
+ }
565
+
566
+ .input-wrapper {
567
+ display: flex;
568
+ position: relative;
569
+ flex-direction: column;
570
+ gap: 0;
571
+ background: var(--block-background-fill);
572
+ border-radius: var(--radius-xl);
573
+ padding: var(--spacing-sm);
574
+ align-items: flex-start;
575
+ min-height: auto;
576
+ }
577
+
578
+ .input-wrapper.has-files {
579
+ padding-top: var(--spacing-xs);
580
+ }
581
+
582
+ .input-row {
583
+ display: flex;
458
584
  align-items: flex-end;
585
+ gap: var(--spacing-sm);
586
+ width: 100%;
587
+ }
588
+
589
+ .thumbnails {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: var(--spacing-sm);
593
+ padding: var(--spacing-xs) 0;
594
+ margin-bottom: var(--spacing-xs);
595
+ overflow-x: auto;
596
+ overflow-y: hidden;
597
+ scrollbar-width: none;
598
+ -ms-overflow-style: none;
599
+ flex-wrap: nowrap;
600
+ -webkit-overflow-scrolling: touch;
601
+ scroll-behavior: smooth;
602
+ overflow-y: scroll;
603
+ width: 100%;
604
+ }
605
+
606
+ .thumbnails::-webkit-scrollbar,
607
+ .thumbnails::-webkit-scrollbar-track,
608
+ .thumbnails::-webkit-scrollbar-thumb {
609
+ display: none;
610
+ }
611
+
612
+ .thumbnails :global(img) {
613
+ width: var(--size-full);
614
+ height: var(--size-full);
615
+ object-fit: cover;
616
+ border-radius: var(--radius-md);
617
+ }
618
+
619
+ .thumbnail-wrapper {
620
+ position: relative;
621
+ flex-shrink: 0;
622
+ }
623
+
624
+ .thumbnail-item {
625
+ position: relative;
626
+ display: flex;
627
+ justify-content: center;
628
+ align-items: center;
629
+ border: var(--size-px) solid var(--border-color-primary);
630
+ border-radius: var(--radius-md);
631
+ background: var(--background-fill-secondary);
632
+ width: var(--size-full);
633
+ height: var(--size-full);
634
+ cursor: default;
635
+ padding: 0;
636
+ }
637
+
638
+ .thumbnail-small {
639
+ width: var(--size-10);
640
+ height: var(--size-10);
641
+ }
642
+
643
+ .thumbnail-item :global(svg) {
644
+ width: var(--size-5);
645
+ height: var(--size-5);
646
+ }
647
+
648
+ .delete-button {
649
+ position: absolute;
650
+ inset: 0;
651
+ display: flex;
652
+ justify-content: center;
653
+ align-items: center;
654
+ color: var(--button-primary-text-color);
655
+ background: rgba(0, 0, 0, 0.6);
656
+ backdrop-filter: var(--blur-xs);
657
+ border-radius: var(--radius-md);
658
+ padding: 0;
659
+ z-index: var(--layer-1);
660
+ opacity: 0;
661
+ transition: opacity 0.1s var(--easing-standard);
662
+ }
663
+
664
+ .delete-button:hover {
665
+ background: rgba(0, 0, 0, 0.8);
666
+ }
667
+
668
+ .delete-button :global(svg) {
669
+ width: var(--size-5);
670
+ height: var(--size-5);
671
+ }
672
+
673
+ .thumbnail-item:hover .delete-button {
674
+ opacity: 1;
459
675
  }
460
676
 
461
677
  textarea {
462
678
  flex-grow: 1;
463
679
  outline: none !important;
464
- background: var(--block-background-fill);
465
- padding: var(--input-padding);
680
+ background: transparent;
681
+ padding: 0;
466
682
  color: var(--body-text-color);
467
683
  font-weight: var(--input-text-weight);
468
684
  font-size: var(--input-text-size);
469
- line-height: var(--line-sm);
470
685
  border: none;
471
- margin-top: 0px;
472
- margin-bottom: 0px;
686
+ margin: 0;
473
687
  resize: none;
474
688
  position: relative;
475
- z-index: 1;
689
+ z-index: var(--layer-1);
476
690
  text-align: left;
691
+ min-height: var(--size-9);
692
+ }
693
+
694
+ textarea:disabled {
695
+ -webkit-opacity: 1;
696
+ opacity: 1;
477
697
  }
698
+
699
+ textarea::placeholder {
700
+ color: var(--input-placeholder-color);
701
+ }
702
+
478
703
  textarea[dir="rtl"] {
479
704
  text-align: right;
480
705
  }
481
706
 
482
707
  textarea[dir="rtl"] ~ .submit-button {
483
708
  order: -1;
484
- margin-left: 0;
485
- margin-right: var(--spacing-sm);
486
709
  }
487
710
 
488
711
  textarea[dir="rtl"] ~ .submit-button :global(svg) {
489
712
  transform: scaleX(-1);
490
713
  }
491
714
 
492
- textarea.no-label {
493
- padding-top: 5px;
494
- padding-bottom: 5px;
715
+ .mobile-thumbnail-add,
716
+ .microphone-button {
717
+ color: var(--body-text-color);
718
+ cursor: pointer;
719
+ padding: var(--spacing-sm);
720
+ display: flex;
721
+ justify-content: center;
722
+ align-items: center;
723
+ flex-shrink: 0;
724
+ border-radius: var(--radius-md);
725
+ transition:
726
+ background 0.2s var(--easing-standard),
727
+ border-color 0.2s var(--easing-standard);
728
+ }
729
+
730
+ .mobile-thumbnail-add {
731
+ width: var(--size-10);
732
+ height: var(--size-10);
495
733
  }
496
734
 
497
- textarea:disabled {
498
- -webkit-opacity: 1;
499
- opacity: 1;
735
+ .microphone-button {
736
+ width: var(--size-9);
737
+ height: var(--size-9);
500
738
  }
501
739
 
502
- textarea::placeholder {
503
- color: var(--input-placeholder-color);
740
+ .mobile-thumbnail-add:hover:not(:disabled),
741
+ .microphone-button:hover:not(:disabled) {
742
+ background: var(--button-secondary-background-fill-hover);
743
+ border-color: var(--border-color-primary);
744
+ }
745
+
746
+ .mobile-thumbnail-add:disabled,
747
+ .microphone-button:disabled {
748
+ opacity: 0.5;
749
+ cursor: not-allowed;
750
+ }
751
+
752
+ .mobile-thumbnail-add :global(svg),
753
+ .microphone-button :global(svg) {
754
+ width: var(--size-5);
755
+ height: var(--size-5);
504
756
  }
505
757
 
506
- .microphone-button,
507
- .upload-button,
508
758
  .submit-button,
509
759
  .stop-button {
510
- border: none;
511
- text-align: center;
512
- text-decoration: none;
513
- font-size: 14px;
760
+ border: var(--size-px) solid var(--border-color-primary);
761
+ background: var(--button-secondary-background-fill);
514
762
  cursor: pointer;
515
- border-radius: 15px;
516
- min-width: 30px;
517
- height: 30px;
518
- flex-shrink: 0;
519
763
  display: flex;
520
764
  justify-content: center;
521
765
  align-items: center;
766
+ flex-shrink: 0;
767
+ width: var(--size-9);
768
+ height: var(--size-9);
769
+ border-radius: var(--radius-md);
522
770
  z-index: var(--layer-1);
523
- margin-left: var(--spacing-sm);
524
- }
525
- .padded-button {
526
- padding: 0 10px;
527
- }
528
-
529
- .microphone-button,
530
- .stop-button,
531
- .upload-button,
532
- .submit-button {
533
- background: var(--button-secondary-background-fill);
771
+ transition:
772
+ background 0.2s var(--easing-standard),
773
+ border-color 0.2s var(--easing-standard);
534
774
  }
535
775
 
536
- .microphone-button:hover:not(:disabled),
537
- .stop-button:hover:not(:disabled),
538
- .upload-button:hover:not(:disabled),
539
- .submit-button:hover:not(:disabled) {
776
+ .submit-button:hover:not(:disabled),
777
+ .stop-button:hover:not(:disabled) {
540
778
  background: var(--button-secondary-background-fill-hover);
541
779
  }
542
780
 
543
- .microphone-button:disabled,
544
- .stop-button:disabled,
545
- .upload-button:disabled,
546
- .submit-button:disabled {
547
- background: var(--button-secondary-background-fill);
548
- cursor: not-allowed;
549
- }
550
- .microphone-button:active,
551
- .stop-button:active,
552
- .upload-button:active,
553
- .submit-button:active {
781
+ .submit-button:active:not(:disabled),
782
+ .stop-button:active:not(:disabled) {
554
783
  box-shadow: var(--button-shadow-active);
555
784
  }
556
785
 
557
- .submit-button :global(svg) {
558
- height: 22px;
559
- width: 22px;
560
- }
561
- .microphone-button :global(svg),
562
- .upload-button :global(svg) {
563
- height: 17px;
564
- width: 17px;
786
+ .submit-button:disabled,
787
+ .stop-button:disabled {
788
+ cursor: not-allowed;
565
789
  }
566
790
 
791
+ .submit-button :global(svg),
567
792
  .stop-button :global(svg) {
568
- height: 16px;
569
- width: 16px;
793
+ width: var(--size-5);
794
+ height: var(--size-5);
795
+ }
796
+
797
+ .padded-button {
798
+ padding: 0 var(--spacing-lg);
799
+ width: auto;
800
+ border-radius: var(--radius-xl);
570
801
  }
571
802
 
572
803
  .loader {
573
804
  display: flex;
574
805
  justify-content: center;
575
806
  align-items: center;
576
- --ring-color: transparent;
577
807
  position: relative;
578
- border: 5px solid #f3f3f3;
579
- border-top: 5px solid var(--color-accent);
580
- border-radius: 50%;
581
- width: 25px;
582
- height: 25px;
583
- animation: spin 2s linear infinite;
808
+ border: var(--size-1) solid var(--border-color-primary);
809
+ border-top-color: var(--color-accent);
810
+ border-radius: var(--radius-100);
811
+ width: var(--size-5);
812
+ height: var(--size-5);
813
+ animation: spin 1s linear infinite;
814
+ flex-shrink: 0;
584
815
  }
585
816
 
586
817
  @keyframes spin {
587
- 0% {
588
- transform: rotate(0deg);
589
- }
590
- 100% {
818
+ to {
591
819
  transform: rotate(360deg);
592
820
  }
593
821
  }
594
822
 
595
- .thumbnails :global(img) {
596
- width: var(--size-full);
597
- height: var(--size-full);
598
- object-fit: cover;
599
- border-radius: var(--radius-lg);
600
- }
601
-
602
- .thumbnails {
603
- display: flex;
604
- align-items: center;
605
- gap: var(--spacing-lg);
606
- overflow-x: scroll;
607
- padding-top: var(--spacing-sm);
608
- margin-bottom: 6px;
609
- }
610
-
611
- .thumbnail-item {
612
- display: flex;
613
- justify-content: center;
614
- align-items: center;
615
- --ring-color: transparent;
616
- position: relative;
617
- box-shadow:
618
- 0 0 0 2px var(--ring-color),
619
- var(--shadow-drop);
620
- border: 1px solid var(--border-color-primary);
621
- border-radius: var(--radius-lg);
622
- background: var(--background-fill-secondary);
623
- aspect-ratio: var(--ratio-square);
624
- width: var(--size-full);
625
- height: var(--size-full);
626
- cursor: default;
627
- }
628
-
629
- .thumbnail-small {
630
- flex: none;
631
- transform: scale(0.9);
632
- transition: 0.075s;
633
- width: var(--size-12);
634
- height: var(--size-12);
635
- }
636
-
637
- .thumbnail-item :global(svg) {
638
- width: 30px;
639
- height: 30px;
640
- }
641
-
642
- .delete-button {
643
- display: flex;
644
- justify-content: center;
645
- align-items: center;
646
- position: absolute;
647
- right: -7px;
648
- top: -7px;
649
- color: var(--button-secondary-text-color);
650
- background: var(--button-secondary-background-fill);
651
- border: none;
652
- text-align: center;
653
- text-decoration: none;
654
- font-size: 10px;
655
- cursor: pointer;
656
- border-radius: 50%;
657
- width: 20px;
658
- height: 20px;
659
- }
823
+ @media (max-width: 768px) {
824
+ .input-wrapper {
825
+ padding: var(--spacing-xs);
826
+ }
660
827
 
661
- .disabled {
662
- display: none;
663
- }
828
+ .thumbnails {
829
+ padding: var(--spacing-xs) 0;
830
+ margin-bottom: var(--spacing-md);
831
+ }
664
832
 
665
- .delete-button :global(svg) {
666
- width: 12px;
667
- height: 12px;
668
- }
833
+ .thumbnail-small,
834
+ .mobile-thumbnail-add {
835
+ width: var(--size-9);
836
+ height: var(--size-9);
837
+ }
669
838
 
670
- .delete-button:hover {
671
- filter: brightness(1.2);
672
- border: 0.8px solid var(--color-grey-500);
839
+ .thumbnail-item:active .delete-button {
840
+ opacity: 1;
841
+ }
673
842
  }
674
843
  </style>