@gradio/multimodaltextbox 0.11.3 → 0.11.5

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,13 +1,7 @@
1
1
  <script lang="ts">
2
- import {
3
- onMount,
4
- beforeUpdate,
5
- afterUpdate,
6
- createEventDispatcher,
7
- tick
8
- } from "svelte";
2
+ import { tick } from "svelte";
9
3
  import { text_area_resize, resize } from "../shared/utils";
10
- import { BlockTitle } from "@gradio/atoms";
4
+ import { BlockTitle, ScrollFade } from "@gradio/atoms";
11
5
  import { Upload } from "@gradio/upload";
12
6
  import { Image } from "@gradio/image/shared";
13
7
  import type { I18nFormatter } from "js/core/src/gradio_helper";
@@ -24,112 +18,170 @@
24
18
  Microphone,
25
19
  Check
26
20
  } from "@gradio/icons";
27
- import type { SelectData } from "@gradio/utils";
28
- import { BaseInteractiveAudio as InteractiveAudio } from "@gradio/audio";
21
+ import { should_show_scroll_fade, type SelectData } from "@gradio/utils";
29
22
  import {
30
23
  MinimalAudioPlayer,
31
24
  MinimalAudioRecorder
32
25
  } from "@gradio/audio/shared";
33
26
  import type { InputHTMLAttributes } from "./types";
34
27
 
35
- export let value: { text: string; files: FileData[] } = {
36
- text: "",
37
- files: []
38
- };
39
-
40
- export let value_is_output = false;
41
- export let lines = 1;
42
- export let i18n: I18nFormatter;
43
- export let placeholder = "";
44
- export let disabled = false;
45
- export let label: string;
46
- export let info: string | undefined = undefined;
47
- export let show_label = true;
48
- export let max_lines: number;
49
- export let submit_btn: string | boolean | null = null;
50
- export let stop_btn: string | boolean | null = null;
51
- export let rtl = false;
52
- export let autofocus = false;
53
- export let text_align: "left" | "right" | undefined = undefined;
54
- export let autoscroll = true;
55
- export let root: string;
56
- export let file_types_string: string | null = null;
57
- export let max_file_size: number | null = null;
58
- export let upload: Client["upload"];
59
- export let stream_handler: Client["stream"];
60
- export let file_count: "single" | "multiple" | "directory" = "multiple";
61
- export let max_plain_text_length = 1000;
62
- export let waveform_settings: Record<string, any>;
63
- export let waveform_options: WaveformOptions = {
64
- show_recording_waveform: true
65
- };
66
- export let sources_string:
67
- | "upload"
68
- | "upload,microphone"
69
- | "microphone"
70
- | "microphone,upload" = "upload";
71
- export let active_source: "microphone" | null = null;
72
- export let html_attributes: InputHTMLAttributes | null = null;
73
- export let upload_promise: Promise<any> | null = null;
28
+ let {
29
+ value = $bindable(),
30
+ value_is_output = false,
31
+ lines = 1,
32
+ i18n: _i18n,
33
+ placeholder = "",
34
+ disabled = false,
35
+ label,
36
+ info = undefined,
37
+ show_label = true,
38
+ max_lines,
39
+ submit_btn = null,
40
+ stop_btn = null,
41
+ rtl = false,
42
+ autofocus = false,
43
+ text_align = undefined,
44
+ autoscroll = true,
45
+ root,
46
+ file_types_string = null,
47
+ max_file_size = null,
48
+ upload,
49
+ stream_handler,
50
+ file_count = "multiple",
51
+ max_plain_text_length = 1000,
52
+ waveform_settings,
53
+ waveform_options: _waveform_options = { show_recording_waveform: true },
54
+ sources_string = "upload",
55
+ active_source = $bindable<"microphone" | null>(),
56
+ html_attributes = null,
57
+ upload_promise = $bindable<Promise<any> | null>(),
58
+ dragging = $bindable<boolean>(),
59
+ onchange,
60
+ onsubmit,
61
+ onstop,
62
+ onblur,
63
+ onselect,
64
+ oninput,
65
+ onfocus,
66
+ ondrag,
67
+ onupload,
68
+ onclear,
69
+ onload: _onload,
70
+ onerror,
71
+ onstop_recording
72
+ }: {
73
+ value?: { text: string; files: FileData[] };
74
+ value_is_output?: boolean;
75
+ lines?: number;
76
+ i18n: I18nFormatter;
77
+ placeholder?: string;
78
+ disabled?: boolean;
79
+ label: string;
80
+ info?: string | undefined;
81
+ show_label?: boolean;
82
+ max_lines: number;
83
+ submit_btn?: string | boolean | null;
84
+ stop_btn?: string | boolean | null;
85
+ rtl?: boolean;
86
+ autofocus?: boolean;
87
+ text_align?: "left" | "right" | undefined;
88
+ autoscroll?: boolean;
89
+ root: string;
90
+ file_types_string?: string | null;
91
+ max_file_size?: number | null;
92
+ upload: Client["upload"];
93
+ stream_handler: Client["stream"];
94
+ file_count?: "single" | "multiple" | "directory";
95
+ max_plain_text_length?: number;
96
+ waveform_settings: Record<string, any>;
97
+ waveform_options?: WaveformOptions;
98
+ sources_string?:
99
+ | "upload"
100
+ | "upload,microphone"
101
+ | "microphone"
102
+ | "microphone,upload";
103
+ active_source?: "microphone" | null;
104
+ html_attributes?: InputHTMLAttributes | null;
105
+ upload_promise?: Promise<any> | null;
106
+ dragging?: boolean;
107
+ onchange?: (value: { text: string; files: FileData[] }) => void;
108
+ onsubmit?: () => void;
109
+ onstop?: () => void;
110
+ onblur?: () => void;
111
+ onselect?: (data: SelectData) => void;
112
+ oninput?: () => void;
113
+ onfocus?: () => void;
114
+ ondrag?: (dragging: boolean) => void;
115
+ onupload?: (files: FileData[] | FileData) => void;
116
+ onclear?: () => void;
117
+ onload?: (files: FileData[] | FileData) => void;
118
+ onerror?: (error: string) => void;
119
+ onstop_recording?: () => void;
120
+ } = $props();
74
121
 
75
122
  let upload_component: Upload;
76
123
  let el: HTMLTextAreaElement | HTMLInputElement;
77
- let can_scroll: boolean;
78
- let previous_scroll_top = 0;
79
- let user_has_scrolled_up = false;
80
- export let dragging = false;
81
- let uploading = false;
82
- // value can be null in multimodalchatinterface when loading a deep link
83
- let oldValue = value?.text ?? "";
84
- let recording = false;
85
-
86
- $: sources = sources_string
87
- .split(",")
88
- .map((s) => s.trim())
89
- .filter((s) => s === "upload" || s === "microphone") as (
90
- | "upload"
91
- | "microphone"
92
- )[];
93
- $: file_types = file_types_string
94
- ? file_types_string.split(",").map((s) => s.trim())
95
- : null;
96
- $: dispatch("drag", dragging);
97
- $: show_upload =
124
+ let can_scroll = $state(false);
125
+ let previous_scroll_top = $state(0);
126
+ let user_has_scrolled_up = $state(false);
127
+ let show_fade = $state(false);
128
+ let uploading = $state(false);
129
+ let oldValue = $state(value?.text ?? "");
130
+ let recording = $state(false);
131
+ let mic_audio = $state<FileData | null>(null);
132
+ let full_container: HTMLDivElement;
133
+
134
+ let sources = $derived(
135
+ sources_string
136
+ .split(",")
137
+ .map((s) => s.trim())
138
+ .filter((s) => s === "upload" || s === "microphone") as (
139
+ | "upload"
140
+ | "microphone"
141
+ )[]
142
+ );
143
+
144
+ let file_types = $derived(
145
+ file_types_string ? file_types_string.split(",").map((s) => s.trim()) : null
146
+ );
147
+
148
+ let show_upload = $derived(
98
149
  sources &&
99
- sources.includes("upload") &&
100
- !(file_count === "single" && value.files.length > 0);
101
- let mic_audio: FileData | null = null;
150
+ sources.includes("upload") &&
151
+ !(file_count === "single" && value?.files?.length > 0)
152
+ );
102
153
 
103
- let full_container: HTMLDivElement;
154
+ function update_fade(): void {
155
+ show_fade = should_show_scroll_fade(el);
156
+ }
157
+
158
+ $effect(() => {
159
+ if (el && value?.text !== undefined) {
160
+ tick().then(update_fade);
161
+ }
162
+ });
163
+
164
+ $effect(() => {
165
+ ondrag?.(dragging);
166
+ });
104
167
 
105
- $: if (oldValue !== value.text) {
106
- dispatch("change", value);
107
- oldValue = value.text;
108
- }
109
-
110
- $: if (value === null) value = { text: "", files: [] };
111
- $: (value, el && lines !== max_lines && resize(el, lines, max_lines));
112
-
113
- const dispatch = createEventDispatcher<{
114
- change: typeof value;
115
- submit: undefined;
116
- stop: undefined;
117
- blur: undefined;
118
- select: SelectData;
119
- input: undefined;
120
- focus: undefined;
121
- drag: boolean;
122
- upload: FileData[] | FileData;
123
- clear: undefined;
124
- load: FileData[] | FileData;
125
- error: string;
126
- start_recording: undefined;
127
- pause_recording: undefined;
128
- stop_recording: undefined;
129
- }>();
130
-
131
- beforeUpdate(() => {
132
- can_scroll = el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100;
168
+ $effect(() => {
169
+ if (value && oldValue !== value.text) {
170
+ onchange?.(value);
171
+ oldValue = value.text;
172
+ }
173
+ });
174
+
175
+ $effect(() => {
176
+ if (el && lines !== max_lines) {
177
+ resize(el, lines, max_lines);
178
+ }
179
+ });
180
+
181
+ $effect.pre(() => {
182
+ if (el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100) {
183
+ can_scroll = true;
184
+ }
133
185
  });
134
186
 
135
187
  const scroll = (): void => {
@@ -139,28 +191,24 @@
139
191
  };
140
192
 
141
193
  async function handle_change(): Promise<void> {
142
- dispatch("change", value);
194
+ onchange?.(value);
143
195
  if (!value_is_output) {
144
- dispatch("input");
196
+ oninput?.();
145
197
  }
146
198
  }
147
199
 
148
- onMount(() => {
149
- if (autofocus && el !== null) {
200
+ $effect(() => {
201
+ value;
202
+ if (autofocus && el) {
150
203
  el.focus();
151
204
  }
152
205
  });
153
206
 
154
- const after_update = (): void => {
207
+ $effect(() => {
155
208
  if (can_scroll && autoscroll) {
156
209
  scroll();
157
210
  }
158
- if (autofocus && el) {
159
- el.focus();
160
- }
161
- };
162
-
163
- afterUpdate(after_update);
211
+ });
164
212
 
165
213
  function handle_select(event: Event): void {
166
214
  const target: HTMLTextAreaElement | HTMLInputElement = event.target as
@@ -171,14 +219,14 @@
171
219
  target.selectionStart as number,
172
220
  target.selectionEnd as number
173
221
  ];
174
- dispatch("select", { value: text.substring(...index), index: index });
222
+ onselect?.({ value: text.substring(...index), index: index });
175
223
  }
176
224
 
177
225
  async function handle_keypress(e: KeyboardEvent): Promise<void> {
178
226
  if (e.key === "Enter" && e.shiftKey && lines > 1) {
179
227
  e.preventDefault();
180
228
  await tick();
181
- dispatch("submit");
229
+ onsubmit?.();
182
230
  } else if (
183
231
  e.key === "Enter" &&
184
232
  !e.shiftKey &&
@@ -189,7 +237,7 @@
189
237
  add_mic_audio_to_files();
190
238
  active_source = null;
191
239
  await tick();
192
- dispatch("submit");
240
+ onsubmit?.();
193
241
  }
194
242
  }
195
243
 
@@ -206,11 +254,10 @@
206
254
  if (user_has_scrolled_to_bottom) {
207
255
  user_has_scrolled_up = false;
208
256
  }
257
+ update_fade();
209
258
  }
210
259
 
211
- async function handle_upload({
212
- detail
213
- }: CustomEvent<FileData>): Promise<void> {
260
+ async function handle_upload(detail: FileData | FileData[]): Promise<void> {
214
261
  handle_change();
215
262
  if (Array.isArray(detail)) {
216
263
  for (let file of detail) {
@@ -222,8 +269,8 @@
222
269
  value = value;
223
270
  }
224
271
  await tick();
225
- dispatch("change", value);
226
- dispatch("upload", detail);
272
+ onchange?.(value);
273
+ onupload?.(detail);
227
274
  }
228
275
 
229
276
  function remove_thumbnail(event: MouseEvent, index: number): void {
@@ -238,7 +285,7 @@
238
285
  }
239
286
 
240
287
  function handle_stop(): void {
241
- dispatch("stop");
288
+ onstop?.();
242
289
  }
243
290
 
244
291
  function add_mic_audio_to_files(): void {
@@ -246,14 +293,14 @@
246
293
  value.files.push(mic_audio);
247
294
  value = value;
248
295
  mic_audio = null;
249
- dispatch("change", value);
296
+ onchange?.(value);
250
297
  }
251
298
  }
252
299
 
253
300
  function handle_submit(): void {
254
301
  add_mic_audio_to_files();
255
302
  active_source = null;
256
- dispatch("submit");
303
+ onsubmit?.();
257
304
  }
258
305
 
259
306
  async function handle_paste(event: ClipboardEvent): Promise<void> {
@@ -319,8 +366,7 @@
319
366
 
320
367
  const invalid_files = files.length - valid_files.length;
321
368
  if (invalid_files > 0) {
322
- dispatch(
323
- "error",
369
+ onerror?.(
324
370
  `${invalid_files} file(s) were rejected. Accepted formats: ${file_types.join(", ")}`
325
371
  );
326
372
  }
@@ -339,10 +385,10 @@
339
385
  class="full-container"
340
386
  class:dragging
341
387
  bind:this={full_container}
342
- on:dragenter={handle_dragenter}
343
- on:dragleave={handle_dragleave}
344
- on:dragover|preventDefault
345
- on:drop={handle_drop}
388
+ ondragenter={handle_dragenter}
389
+ ondragleave={handle_dragleave}
390
+ ondragover={(e) => e.preventDefault()}
391
+ ondrop={handle_drop}
346
392
  role="group"
347
393
  aria-label="Multimedia input field"
348
394
  >
@@ -360,22 +406,18 @@
360
406
  {root}
361
407
  {max_file_size}
362
408
  bind:upload_promise
363
- on:change={({ detail }) => {
364
- mic_audio = detail;
409
+ onchange={(audio_value) => {
410
+ mic_audio = audio_value;
365
411
  }}
366
- on:stop_recording={() => {
412
+ onstoprecording={() => {
367
413
  recording = false;
368
- dispatch("stop_recording");
414
+ onstop_recording?.();
369
415
  }}
370
- on:clear={() => {
371
- active_source = null;
372
- recording = false;
373
- mic_audio = null;
374
- }}
375
- on:error={({ detail }) => {
416
+ onclear={() => {
376
417
  active_source = null;
377
418
  recording = false;
378
419
  mic_audio = null;
420
+ onclear?.();
379
421
  }}
380
422
  />
381
423
  </div>
@@ -389,7 +431,7 @@
389
431
  <div class="action-buttons">
390
432
  <button
391
433
  class="confirm-button"
392
- on:click={() => {
434
+ onclick={() => {
393
435
  add_mic_audio_to_files();
394
436
  active_source = null;
395
437
  recording = false;
@@ -400,7 +442,7 @@
400
442
  </button>
401
443
  <button
402
444
  class="cancel-button"
403
- on:click={() => {
445
+ onclick={() => {
404
446
  active_source = null;
405
447
  recording = false;
406
448
  mic_audio = null;
@@ -418,7 +460,7 @@
418
460
  <Upload
419
461
  bind:upload_promise
420
462
  bind:this={upload_component}
421
- on:load={handle_upload}
463
+ onload={handle_upload}
422
464
  {file_count}
423
465
  filetype={file_types}
424
466
  {root}
@@ -427,7 +469,7 @@
427
469
  bind:uploading
428
470
  show_progress={false}
429
471
  disable_click={true}
430
- on:error
472
+ {onerror}
431
473
  hidden={true}
432
474
  {upload}
433
475
  {stream_handler}
@@ -436,9 +478,9 @@
436
478
 
437
479
  <div
438
480
  class="input-wrapper"
439
- class:has-files={value.files.length > 0 || uploading}
481
+ class:has-files={(value?.files?.length ?? 0) > 0 || uploading}
440
482
  >
441
- {#if value.files.length > 0 || uploading || show_upload}
483
+ {#if (value?.files?.length ?? 0) > 0 || uploading}
442
484
  <div
443
485
  class="thumbnails"
444
486
  aria-label="Uploaded files"
@@ -449,13 +491,13 @@
449
491
  data-testid="upload-button"
450
492
  class="upload-button thumbnail-add"
451
493
  {disabled}
452
- on:click={handle_upload_click}
494
+ onclick={handle_upload_click}
453
495
  aria-label="Upload a file"
454
496
  >
455
497
  <Paperclip />
456
498
  </button>
457
499
  {/if}
458
- {#each value.files as file, index}
500
+ {#each value?.files ?? [] as file, index}
459
501
  <span
460
502
  class="thumbnail-wrapper"
461
503
  role="listitem"
@@ -481,7 +523,7 @@
481
523
  {/if}
482
524
  <button
483
525
  class="delete-button"
484
- on:click={(event) => remove_thumbnail(event, index)}
526
+ onclick={(event) => remove_thumbnail(event, index)}
485
527
  aria-label="Remove file"
486
528
  >
487
529
  <Clear />
@@ -496,35 +538,49 @@
496
538
  {/if}
497
539
 
498
540
  <div class="input-row">
499
- <textarea
500
- data-testid="textbox"
501
- use:text_area_resize={{
502
- text: value.text,
503
- lines: lines,
504
- max_lines: max_lines
505
- }}
506
- class:no-label={!show_label}
507
- dir={rtl ? "rtl" : "ltr"}
508
- bind:value={value.text}
509
- bind:this={el}
510
- {placeholder}
511
- rows={lines}
512
- {disabled}
513
- on:keypress={handle_keypress}
514
- on:blur
515
- on:select={handle_select}
516
- on:focus
517
- on:scroll={handle_scroll}
518
- on:paste={handle_paste}
519
- style={text_align ? "text-align: " + text_align : ""}
520
- autocapitalize={html_attributes?.autocapitalize}
521
- autocorrect={html_attributes?.autocorrect}
522
- spellcheck={html_attributes?.spellcheck}
523
- autocomplete={html_attributes?.autocomplete}
524
- tabindex={html_attributes?.tabindex}
525
- enterkeyhint={html_attributes?.enterkeyhint}
526
- lang={html_attributes?.lang}
527
- />
541
+ {#if show_upload && (value?.files?.length ?? 0) === 0 && !uploading}
542
+ <button
543
+ data-testid="upload-button"
544
+ class="upload-button icon-button"
545
+ {disabled}
546
+ onclick={handle_upload_click}
547
+ aria-label="Upload a file"
548
+ >
549
+ <Paperclip />
550
+ </button>
551
+ {/if}
552
+ <div class="textarea-wrapper">
553
+ <textarea
554
+ data-testid="textbox"
555
+ use:text_area_resize={{
556
+ text: value.text,
557
+ lines: lines,
558
+ max_lines: max_lines
559
+ }}
560
+ class:no-label={!show_label}
561
+ dir={rtl ? "rtl" : "ltr"}
562
+ bind:value={value.text}
563
+ bind:this={el}
564
+ {placeholder}
565
+ rows={lines}
566
+ {disabled}
567
+ onkeypress={handle_keypress}
568
+ onblur={() => onblur?.()}
569
+ onselect={handle_select}
570
+ onfocus={() => onfocus?.()}
571
+ onscroll={handle_scroll}
572
+ onpaste={handle_paste}
573
+ style={text_align ? "text-align: " + text_align : ""}
574
+ autocapitalize={html_attributes?.autocapitalize}
575
+ autocorrect={html_attributes?.autocorrect}
576
+ spellcheck={html_attributes?.spellcheck}
577
+ autocomplete={html_attributes?.autocomplete}
578
+ tabindex={html_attributes?.tabindex}
579
+ enterkeyhint={html_attributes?.enterkeyhint}
580
+ lang={html_attributes?.lang}
581
+ />
582
+ <ScrollFade visible={show_fade} position="absolute" />
583
+ </div>
528
584
 
529
585
  {#if sources && sources.includes("microphone")}
530
586
  <button
@@ -532,7 +588,7 @@
532
588
  class="microphone-button"
533
589
  class:recording
534
590
  {disabled}
535
- on:click={async () => {
591
+ onclick={async () => {
536
592
  if (active_source !== "microphone") {
537
593
  active_source = "microphone";
538
594
  await tick();
@@ -554,7 +610,7 @@
554
610
  data-testid="submit-button"
555
611
  class:padded-button={submit_btn !== true}
556
612
  {disabled}
557
- on:click={handle_submit}
613
+ onclick={handle_submit}
558
614
  aria-label="Submit"
559
615
  >
560
616
  {#if submit_btn === true}
@@ -568,7 +624,7 @@
568
624
  <button
569
625
  class="stop-button"
570
626
  class:padded-button={stop_btn !== true}
571
- on:click={handle_stop}
627
+ onclick={handle_stop}
572
628
  aria-label="Stop"
573
629
  >
574
630
  {#if stop_btn === true}
@@ -617,7 +673,7 @@
617
673
  gap: 0;
618
674
  background: var(--block-background-fill);
619
675
  border-radius: var(--radius-xl);
620
- padding: var(--spacing-sm);
676
+ padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) 0;
621
677
  align-items: flex-start;
622
678
  min-height: auto;
623
679
  }
@@ -628,7 +684,7 @@
628
684
 
629
685
  .input-row {
630
686
  display: flex;
631
- align-items: flex-end;
687
+ align-items: flex-start;
632
688
  gap: var(--spacing-sm);
633
689
  width: 100%;
634
690
  }
@@ -724,7 +780,7 @@
724
780
  flex-grow: 1;
725
781
  outline: none !important;
726
782
  background: transparent;
727
- padding: 0;
783
+ padding: var(--spacing-sm) 0;
728
784
  color: var(--body-text-color);
729
785
  font-weight: var(--input-text-weight);
730
786
  font-size: var(--input-text-size);
@@ -734,7 +790,6 @@
734
790
  position: relative;
735
791
  z-index: var(--layer-1);
736
792
  text-align: left;
737
- min-height: var(--size-9);
738
793
  }
739
794
 
740
795
  textarea:disabled {
@@ -750,15 +805,12 @@
750
805
  text-align: right;
751
806
  }
752
807
 
753
- textarea[dir="rtl"] ~ .submit-button {
754
- order: -1;
755
- }
756
-
757
808
  textarea[dir="rtl"] ~ .submit-button :global(svg) {
758
809
  transform: scaleX(-1);
759
810
  }
760
811
 
761
- .microphone-button {
812
+ .microphone-button,
813
+ .icon-button {
762
814
  color: var(--body-text-color);
763
815
  cursor: pointer;
764
816
  padding: var(--spacing-sm);
@@ -796,21 +848,25 @@
796
848
  height: var(--size-5);
797
849
  }
798
850
 
799
- .microphone-button {
851
+ .microphone-button,
852
+ .icon-button {
800
853
  width: var(--size-9);
801
854
  height: var(--size-9);
802
855
  }
803
856
 
804
- .microphone-button:hover:not(:disabled) {
857
+ .microphone-button:hover:not(:disabled),
858
+ .icon-button:hover:not(:disabled) {
805
859
  background: var(--button-secondary-background-fill);
806
860
  }
807
861
 
808
- .microphone-button:disabled {
862
+ .microphone-button:disabled,
863
+ .icon-button:disabled {
809
864
  opacity: 0.5;
810
865
  cursor: not-allowed;
811
866
  }
812
867
 
813
- .microphone-button :global(svg) {
868
+ .microphone-button :global(svg),
869
+ .icon-button :global(svg) {
814
870
  width: var(--size-5);
815
871
  height: var(--size-5);
816
872
  }
@@ -1002,4 +1058,13 @@
1002
1058
  opacity: 1;
1003
1059
  }
1004
1060
  }
1061
+
1062
+ .textarea-wrapper {
1063
+ position: relative;
1064
+ flex-grow: 1;
1065
+ }
1066
+
1067
+ .textarea-wrapper textarea {
1068
+ width: 100%;
1069
+ }
1005
1070
  </style>