@gradio/multimodaltextbox 0.11.2 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @gradio/multimodaltextbox
2
2
 
3
+ ## 0.11.4
4
+
5
+ ### Fixes
6
+
7
+ - [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton!
8
+ - [#12779](https://github.com/gradio-app/gradio/pull/12779) [`ea2d3e9`](https://github.com/gradio-app/gradio/commit/ea2d3e985a8b42d188e551f517c5825c00790628) - Migrate Audio + Upload + Atoms to Svelte 5. Thanks @dawoodkhan82!
9
+ - [#12795](https://github.com/gradio-app/gradio/pull/12795) [`184968a`](https://github.com/gradio-app/gradio/commit/184968aa13636b5afe78f8ae443cd7fc29635bfa) - Add fade effect to overflowing text. Thanks @hannahblair!
10
+ - [#12594](https://github.com/gradio-app/gradio/pull/12594) [`c2f0f19`](https://github.com/gradio-app/gradio/commit/c2f0f1949d453bc850fa65e5b140067008a83e22) - add conditional rendering for the upload button in multimodal textbox. Thanks @hannahblair!
11
+
12
+ ### Dependency updates
13
+
14
+ - @gradio/statustracker@0.12.2
15
+ - @gradio/atoms@0.20.1
16
+ - @gradio/utils@0.11.2
17
+ - @gradio/icons@0.15.1
18
+ - @gradio/upload@0.17.4
19
+ - @gradio/client@2.0.3
20
+ - @gradio/image@0.25.1
21
+ - @gradio/video@0.20.1
22
+ - @gradio/audio@0.22.1
23
+
24
+ ## 0.11.3
25
+
26
+ ### Fixes
27
+
28
+ - [#12692](https://github.com/gradio-app/gradio/pull/12692) [`3b3eaf0`](https://github.com/gradio-app/gradio/commit/3b3eaf0dcb0690cc815eb2b3c40ca335c9a47a11) - Restore RTL prop to gradio components. Thanks @hannahblair!
29
+
30
+ ### Dependency updates
31
+
32
+ - @gradio/utils@0.11.1
33
+ - @gradio/client@2.0.2
34
+ - @gradio/video@0.20.0
35
+
3
36
  ## 0.11.2
4
37
 
5
38
  ### Dependency updates
package/Index.svelte CHANGED
@@ -121,6 +121,7 @@
121
121
  allow_overflow={false}
122
122
  padding={false}
123
123
  border_mode={dragging ? "focus" : "base"}
124
+ rtl={gradio.props.rtl}
124
125
  >
125
126
  {#if gradio.shared.loading_status}
126
127
  <StatusTracker
@@ -1,60 +1,63 @@
1
- <script>
2
- import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import MultimodalTextbox from "./Index.svelte";
4
- </script>
4
+ import { wrapProps } from "../storybook/wrapProps";
5
5
 
6
- <Meta
7
- title="Components/MultimodalTextbox"
8
- component={MultimodalTextbox}
9
- argTypes={{
10
- label: {
11
- control: "text",
12
- description: "The textbox label",
13
- name: "label"
14
- },
15
- show_label: {
16
- options: [true, false],
17
- description: "Whether to show the label",
18
- control: { type: "boolean" },
19
- defaultValue: true
20
- },
21
- text_align: {
22
- options: ["left", "right"],
23
- description: "Whether to align the text left or right",
24
- control: { type: "select" },
25
- defaultValue: "left"
26
- },
27
- lines: {
28
- options: [1, 5, 10, 20],
29
- description: "The number of lines to display in the textbox",
30
- control: { type: "select" },
31
- defaultValue: 1
32
- },
33
- max_lines: {
34
- options: [1, 5, 10, 20],
35
- description:
36
- "The maximum number of lines to allow users to type in the textbox",
37
- control: { type: "select" },
38
- defaultValue: 1
39
- },
40
- rtl: {
41
- options: [true, false],
42
- description: "Whether to render right-to-left",
43
- control: { type: "boolean" },
44
- defaultValue: false
45
- },
46
- sources: {
47
- options: ["upload", "microphone"],
48
- description: "The sources to enable",
49
- control: { type: "select" },
50
- defaultValue: ["upload", "microphone"]
6
+ const cheetah = "/cheetah.jpg";
7
+
8
+ const { Story } = defineMeta({
9
+ title: "Components/MultimodalTextbox",
10
+ component: MultimodalTextbox,
11
+ argTypes: {
12
+ label: {
13
+ control: "text",
14
+ description: "The textbox label",
15
+ name: "label"
16
+ },
17
+ show_label: {
18
+ options: [true, false],
19
+ description: "Whether to show the label",
20
+ control: { type: "boolean" },
21
+ defaultValue: true
22
+ },
23
+ text_align: {
24
+ options: ["left", "right"],
25
+ description: "Whether to align the text left or right",
26
+ control: { type: "select" },
27
+ defaultValue: "left"
28
+ },
29
+ lines: {
30
+ options: [1, 5, 10, 20],
31
+ description: "The number of lines to display in the textbox",
32
+ control: { type: "select" },
33
+ defaultValue: 1
34
+ },
35
+ max_lines: {
36
+ options: [1, 5, 10, 20],
37
+ description:
38
+ "The maximum number of lines to allow users to type in the textbox",
39
+ control: { type: "select" },
40
+ defaultValue: 1
41
+ },
42
+ rtl: {
43
+ options: [true, false],
44
+ description: "Whether to render right-to-left",
45
+ control: { type: "boolean" },
46
+ defaultValue: false
47
+ },
48
+ sources: {
49
+ options: ["upload", "microphone"],
50
+ description: "The sources to enable",
51
+ control: { type: "select" },
52
+ defaultValue: ["upload", "microphone"]
53
+ }
51
54
  }
52
- }}
53
- />
55
+ });
56
+ </script>
54
57
 
55
- <Template let:args>
56
- <MultimodalTextbox {...args} />
57
- </Template>
58
+ {#snippet template(args)}
59
+ <MultimodalTextbox {...wrapProps(args)} />
60
+ {/snippet}
58
61
 
59
62
  <Story
60
63
  name="MultimodalTextbox with file and label"
@@ -63,43 +66,77 @@
63
66
  text: "sample text",
64
67
  files: [
65
68
  {
66
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
67
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
69
+ path: cheetah,
70
+ url: cheetah,
68
71
  orig_name: "cheetah.jpg"
69
72
  }
70
73
  ]
71
74
  },
72
75
  label: "My simple label",
73
- show_label: true
76
+ show_label: true,
77
+ sources: ["upload", "microphone"],
78
+ file_types: [],
79
+ lines: 1,
80
+ max_lines: 5
74
81
  }}
82
+ {template}
75
83
  />
76
84
  <Story
77
85
  name="MultimodalTextbox with 5 lines and max 5 lines"
78
- args={{ lines: 5, max_lines: 5 }}
86
+ args={{
87
+ lines: 5,
88
+ max_lines: 5,
89
+ sources: ["upload", "microphone"],
90
+ file_types: [],
91
+ value: { text: "", files: [] }
92
+ }}
93
+ {template}
94
+ />
95
+ <Story
96
+ name="Right aligned textbox"
97
+ args={{
98
+ text_align: "right",
99
+ sources: ["upload", "microphone"],
100
+ file_types: [],
101
+ lines: 1,
102
+ max_lines: 5,
103
+ value: { text: "", files: [] }
104
+ }}
105
+ {template}
79
106
  />
80
- <Story name="Right aligned textbox" args={{ text_align: "right" }} />
81
107
  <Story
82
108
  name="Single file upload"
83
109
  args={{
84
110
  file_count: "single",
85
111
  submit_btn: true,
112
+ sources: ["upload"],
113
+ file_types: [],
114
+ lines: 1,
115
+ max_lines: 5,
86
116
  value: {
87
117
  text: "sample text",
88
118
  files: [
89
119
  {
90
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg"
120
+ path: cheetah,
121
+ url: cheetah,
122
+ orig_name: "cheetah.jpg"
91
123
  }
92
124
  ]
93
125
  }
94
126
  }}
127
+ {template}
95
128
  />
96
129
  <Story
97
130
  name="MultimodalTextbox with microphone input and right to left text"
98
131
  args={{
99
132
  label: "مرحبًا",
100
133
  sources: ["microphone"],
134
+ file_types: [],
101
135
  rtl: true,
102
136
  submit_btn: true,
137
+ lines: 1,
138
+ max_lines: 5,
103
139
  value: { text: "مرحبًا", files: [] }
104
140
  }}
141
+ {template}
105
142
  />
@@ -1,27 +1,31 @@
1
- <script>
2
- import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import MultimodalTextbox from "./Example.svelte";
4
- </script>
5
-
6
- <Meta
7
- title="Components/MultimodalTextbox/Example"
8
- component={MultimodalTextbox}
9
- />
10
4
 
11
- <Template let:args>
12
- <MultimodalTextbox {...args} />
13
- </Template>
5
+ const { Story } = defineMeta({
6
+ title: "Components/MultimodalTextbox/Example",
7
+ component: MultimodalTextbox
8
+ });
9
+ </script>
14
10
 
15
11
  <Story
16
12
  name="Text value"
17
13
  args={{
18
14
  value: { text: "the quick brown fox", files: [] }
19
15
  }}
20
- />
16
+ >
17
+ {#snippet template(args)}
18
+ <MultimodalTextbox {...args} />
19
+ {/snippet}
20
+ </Story>
21
21
 
22
22
  <Story
23
23
  name="Empty Value"
24
24
  args={{
25
25
  value: { text: "", files: [] }
26
26
  }}
27
- />
27
+ >
28
+ {#snippet template(args)}
29
+ <MultimodalTextbox {...args} />
30
+ {/snippet}
31
+ </Story>
package/dist/Index.svelte CHANGED
@@ -121,6 +121,7 @@
121
121
  allow_overflow={false}
122
122
  padding={false}
123
123
  border_mode={dragging ? "focus" : "base"}
124
+ rtl={gradio.props.rtl}
124
125
  >
125
126
  {#if gradio.shared.loading_status}
126
127
  <StatusTracker
@@ -7,7 +7,7 @@
7
7
  tick
8
8
  } from "svelte";
9
9
  import { text_area_resize, resize } from "../shared/utils";
10
- import { BlockTitle } from "@gradio/atoms";
10
+ import { BlockTitle, ScrollFade } from "@gradio/atoms";
11
11
  import { Upload } from "@gradio/upload";
12
12
  import { Image } from "@gradio/image/shared";
13
13
  import type { I18nFormatter } from "js/core/src/gradio_helper";
@@ -24,7 +24,7 @@
24
24
  Microphone,
25
25
  Check
26
26
  } from "@gradio/icons";
27
- import type { SelectData } from "@gradio/utils";
27
+ import { should_show_scroll_fade, type SelectData } from "@gradio/utils";
28
28
  import { BaseInteractiveAudio as InteractiveAudio } from "@gradio/audio";
29
29
  import {
30
30
  MinimalAudioPlayer,
@@ -77,6 +77,7 @@
77
77
  let can_scroll: boolean;
78
78
  let previous_scroll_top = 0;
79
79
  let user_has_scrolled_up = false;
80
+ let show_fade = false;
80
81
  export let dragging = false;
81
82
  let uploading = false;
82
83
  // value can be null in multimodalchatinterface when loading a deep link
@@ -110,6 +111,14 @@
110
111
  $: if (value === null) value = { text: "", files: [] };
111
112
  $: (value, el && lines !== max_lines && resize(el, lines, max_lines));
112
113
 
114
+ function update_fade(): void {
115
+ show_fade = should_show_scroll_fade(el);
116
+ }
117
+
118
+ $: if (el && value.text !== undefined) {
119
+ tick().then(update_fade);
120
+ }
121
+
113
122
  const dispatch = createEventDispatcher<{
114
123
  change: typeof value;
115
124
  submit: undefined;
@@ -206,11 +215,10 @@
206
215
  if (user_has_scrolled_to_bottom) {
207
216
  user_has_scrolled_up = false;
208
217
  }
218
+ update_fade();
209
219
  }
210
220
 
211
- async function handle_upload({
212
- detail
213
- }: CustomEvent<FileData>): Promise<void> {
221
+ async function handle_upload(detail: FileData | FileData[]): Promise<void> {
214
222
  handle_change();
215
223
  if (Array.isArray(detail)) {
216
224
  for (let file of detail) {
@@ -360,19 +368,14 @@
360
368
  {root}
361
369
  {max_file_size}
362
370
  bind:upload_promise
363
- on:change={({ detail }) => {
364
- mic_audio = detail;
371
+ onchange={(value) => {
372
+ mic_audio = value;
365
373
  }}
366
- on:stop_recording={() => {
374
+ onstoprecording={() => {
367
375
  recording = false;
368
376
  dispatch("stop_recording");
369
377
  }}
370
- on:clear={() => {
371
- active_source = null;
372
- recording = false;
373
- mic_audio = null;
374
- }}
375
- on:error={({ detail }) => {
378
+ onclear={() => {
376
379
  active_source = null;
377
380
  recording = false;
378
381
  mic_audio = null;
@@ -418,7 +421,7 @@
418
421
  <Upload
419
422
  bind:upload_promise
420
423
  bind:this={upload_component}
421
- on:load={handle_upload}
424
+ onload={handle_upload}
422
425
  {file_count}
423
426
  filetype={file_types}
424
427
  {root}
@@ -427,7 +430,7 @@
427
430
  bind:uploading
428
431
  show_progress={false}
429
432
  disable_click={true}
430
- on:error
433
+ onerror={(e) => dispatch("error", e)}
431
434
  hidden={true}
432
435
  {upload}
433
436
  {stream_handler}
@@ -438,7 +441,7 @@
438
441
  class="input-wrapper"
439
442
  class:has-files={value.files.length > 0 || uploading}
440
443
  >
441
- {#if value.files.length > 0 || uploading || show_upload}
444
+ {#if value.files.length > 0 || uploading}
442
445
  <div
443
446
  class="thumbnails"
444
447
  aria-label="Uploaded files"
@@ -496,35 +499,49 @@
496
499
  {/if}
497
500
 
498
501
  <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
- />
502
+ {#if show_upload && value.files.length === 0 && !uploading}
503
+ <button
504
+ data-testid="upload-button"
505
+ class="upload-button icon-button"
506
+ {disabled}
507
+ on:click={handle_upload_click}
508
+ aria-label="Upload a file"
509
+ >
510
+ <Paperclip />
511
+ </button>
512
+ {/if}
513
+ <div class="textarea-wrapper">
514
+ <textarea
515
+ data-testid="textbox"
516
+ use:text_area_resize={{
517
+ text: value.text,
518
+ lines: lines,
519
+ max_lines: max_lines
520
+ }}
521
+ class:no-label={!show_label}
522
+ dir={rtl ? "rtl" : "ltr"}
523
+ bind:value={value.text}
524
+ bind:this={el}
525
+ {placeholder}
526
+ rows={lines}
527
+ {disabled}
528
+ on:keypress={handle_keypress}
529
+ on:blur
530
+ on:select={handle_select}
531
+ on:focus
532
+ on:scroll={handle_scroll}
533
+ on:paste={handle_paste}
534
+ style={text_align ? "text-align: " + text_align : ""}
535
+ autocapitalize={html_attributes?.autocapitalize}
536
+ autocorrect={html_attributes?.autocorrect}
537
+ spellcheck={html_attributes?.spellcheck}
538
+ autocomplete={html_attributes?.autocomplete}
539
+ tabindex={html_attributes?.tabindex}
540
+ enterkeyhint={html_attributes?.enterkeyhint}
541
+ lang={html_attributes?.lang}
542
+ />
543
+ <ScrollFade visible={show_fade} position="absolute" />
544
+ </div>
528
545
 
529
546
  {#if sources && sources.includes("microphone")}
530
547
  <button
@@ -617,7 +634,7 @@
617
634
  gap: 0;
618
635
  background: var(--block-background-fill);
619
636
  border-radius: var(--radius-xl);
620
- padding: var(--spacing-sm);
637
+ padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) 0;
621
638
  align-items: flex-start;
622
639
  min-height: auto;
623
640
  }
@@ -628,7 +645,7 @@
628
645
 
629
646
  .input-row {
630
647
  display: flex;
631
- align-items: flex-end;
648
+ align-items: flex-start;
632
649
  gap: var(--spacing-sm);
633
650
  width: 100%;
634
651
  }
@@ -724,7 +741,7 @@
724
741
  flex-grow: 1;
725
742
  outline: none !important;
726
743
  background: transparent;
727
- padding: 0;
744
+ padding: var(--spacing-sm) 0;
728
745
  color: var(--body-text-color);
729
746
  font-weight: var(--input-text-weight);
730
747
  font-size: var(--input-text-size);
@@ -734,7 +751,6 @@
734
751
  position: relative;
735
752
  z-index: var(--layer-1);
736
753
  text-align: left;
737
- min-height: var(--size-9);
738
754
  }
739
755
 
740
756
  textarea:disabled {
@@ -750,15 +766,12 @@
750
766
  text-align: right;
751
767
  }
752
768
 
753
- textarea[dir="rtl"] ~ .submit-button {
754
- order: -1;
755
- }
756
-
757
769
  textarea[dir="rtl"] ~ .submit-button :global(svg) {
758
770
  transform: scaleX(-1);
759
771
  }
760
772
 
761
- .microphone-button {
773
+ .microphone-button,
774
+ .icon-button {
762
775
  color: var(--body-text-color);
763
776
  cursor: pointer;
764
777
  padding: var(--spacing-sm);
@@ -796,21 +809,25 @@
796
809
  height: var(--size-5);
797
810
  }
798
811
 
799
- .microphone-button {
812
+ .microphone-button,
813
+ .icon-button {
800
814
  width: var(--size-9);
801
815
  height: var(--size-9);
802
816
  }
803
817
 
804
- .microphone-button:hover:not(:disabled) {
818
+ .microphone-button:hover:not(:disabled),
819
+ .icon-button:hover:not(:disabled) {
805
820
  background: var(--button-secondary-background-fill);
806
821
  }
807
822
 
808
- .microphone-button:disabled {
823
+ .microphone-button:disabled,
824
+ .icon-button:disabled {
809
825
  opacity: 0.5;
810
826
  cursor: not-allowed;
811
827
  }
812
828
 
813
- .microphone-button :global(svg) {
829
+ .microphone-button :global(svg),
830
+ .icon-button :global(svg) {
814
831
  width: var(--size-5);
815
832
  height: var(--size-5);
816
833
  }
@@ -1002,4 +1019,13 @@
1002
1019
  opacity: 1;
1003
1020
  }
1004
1021
  }
1022
+
1023
+ .textarea-wrapper {
1024
+ position: relative;
1025
+ flex-grow: 1;
1026
+ }
1027
+
1028
+ .textarea-wrapper textarea {
1029
+ width: 100%;
1030
+ }
1005
1031
  </style>
@@ -1,7 +1,7 @@
1
1
  import type { I18nFormatter } from "js/core/src/gradio_helper";
2
2
  import type { FileData, Client } from "@gradio/client";
3
3
  import type { WaveformOptions } from "@gradio/audio";
4
- import type { SelectData } from "@gradio/utils";
4
+ import { type SelectData } from "@gradio/utils";
5
5
  import type { InputHTMLAttributes } from "./types";
6
6
  interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
7
7
  new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
@@ -52,7 +52,6 @@ declare const MultimodalTextbox: $$__sveltets_2_IsomorphicComponent<{
52
52
  dragging?: boolean;
53
53
  }, {
54
54
  dragover: DragEvent;
55
- error: CustomEvent<any>;
56
55
  blur: CustomEvent<any>;
57
56
  focus: CustomEvent<any>;
58
57
  change: CustomEvent<{
@@ -67,6 +66,7 @@ declare const MultimodalTextbox: $$__sveltets_2_IsomorphicComponent<{
67
66
  upload: CustomEvent<FileData | FileData[]>;
68
67
  clear: CustomEvent<undefined>;
69
68
  load: CustomEvent<FileData | FileData[]>;
69
+ error: CustomEvent<string>;
70
70
  start_recording: CustomEvent<undefined>;
71
71
  pause_recording: CustomEvent<undefined>;
72
72
  stop_recording: CustomEvent<undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/multimodaltextbox",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -22,21 +22,21 @@
22
22
  "./package.json": "./package.json"
23
23
  },
24
24
  "dependencies": {
25
- "@gradio/atoms": "^0.20.0",
26
- "@gradio/icons": "^0.15.0",
27
- "@gradio/statustracker": "^0.12.1",
28
- "@gradio/utils": "^0.11.0",
29
- "@gradio/video": "^0.19.0",
30
- "@gradio/image": "^0.25.0",
31
- "@gradio/client": "^2.0.1",
32
- "@gradio/upload": "^0.17.3",
33
- "@gradio/audio": "^0.22.0"
25
+ "@gradio/icons": "^0.15.1",
26
+ "@gradio/statustracker": "^0.12.2",
27
+ "@gradio/utils": "^0.11.2",
28
+ "@gradio/atoms": "^0.20.1",
29
+ "@gradio/upload": "^0.17.4",
30
+ "@gradio/image": "^0.25.1",
31
+ "@gradio/video": "^0.20.1",
32
+ "@gradio/client": "^2.0.3",
33
+ "@gradio/audio": "^0.22.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@gradio/preview": "^0.15.1"
36
+ "@gradio/preview": "^0.15.2"
37
37
  },
38
38
  "peerDependencies": {
39
- "svelte": "^5.43.4"
39
+ "svelte": "^5.48.0"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",
@@ -7,7 +7,7 @@
7
7
  tick
8
8
  } from "svelte";
9
9
  import { text_area_resize, resize } from "../shared/utils";
10
- import { BlockTitle } from "@gradio/atoms";
10
+ import { BlockTitle, ScrollFade } from "@gradio/atoms";
11
11
  import { Upload } from "@gradio/upload";
12
12
  import { Image } from "@gradio/image/shared";
13
13
  import type { I18nFormatter } from "js/core/src/gradio_helper";
@@ -24,7 +24,7 @@
24
24
  Microphone,
25
25
  Check
26
26
  } from "@gradio/icons";
27
- import type { SelectData } from "@gradio/utils";
27
+ import { should_show_scroll_fade, type SelectData } from "@gradio/utils";
28
28
  import { BaseInteractiveAudio as InteractiveAudio } from "@gradio/audio";
29
29
  import {
30
30
  MinimalAudioPlayer,
@@ -77,6 +77,7 @@
77
77
  let can_scroll: boolean;
78
78
  let previous_scroll_top = 0;
79
79
  let user_has_scrolled_up = false;
80
+ let show_fade = false;
80
81
  export let dragging = false;
81
82
  let uploading = false;
82
83
  // value can be null in multimodalchatinterface when loading a deep link
@@ -110,6 +111,14 @@
110
111
  $: if (value === null) value = { text: "", files: [] };
111
112
  $: (value, el && lines !== max_lines && resize(el, lines, max_lines));
112
113
 
114
+ function update_fade(): void {
115
+ show_fade = should_show_scroll_fade(el);
116
+ }
117
+
118
+ $: if (el && value.text !== undefined) {
119
+ tick().then(update_fade);
120
+ }
121
+
113
122
  const dispatch = createEventDispatcher<{
114
123
  change: typeof value;
115
124
  submit: undefined;
@@ -206,11 +215,10 @@
206
215
  if (user_has_scrolled_to_bottom) {
207
216
  user_has_scrolled_up = false;
208
217
  }
218
+ update_fade();
209
219
  }
210
220
 
211
- async function handle_upload({
212
- detail
213
- }: CustomEvent<FileData>): Promise<void> {
221
+ async function handle_upload(detail: FileData | FileData[]): Promise<void> {
214
222
  handle_change();
215
223
  if (Array.isArray(detail)) {
216
224
  for (let file of detail) {
@@ -360,19 +368,14 @@
360
368
  {root}
361
369
  {max_file_size}
362
370
  bind:upload_promise
363
- on:change={({ detail }) => {
364
- mic_audio = detail;
371
+ onchange={(value) => {
372
+ mic_audio = value;
365
373
  }}
366
- on:stop_recording={() => {
374
+ onstoprecording={() => {
367
375
  recording = false;
368
376
  dispatch("stop_recording");
369
377
  }}
370
- on:clear={() => {
371
- active_source = null;
372
- recording = false;
373
- mic_audio = null;
374
- }}
375
- on:error={({ detail }) => {
378
+ onclear={() => {
376
379
  active_source = null;
377
380
  recording = false;
378
381
  mic_audio = null;
@@ -418,7 +421,7 @@
418
421
  <Upload
419
422
  bind:upload_promise
420
423
  bind:this={upload_component}
421
- on:load={handle_upload}
424
+ onload={handle_upload}
422
425
  {file_count}
423
426
  filetype={file_types}
424
427
  {root}
@@ -427,7 +430,7 @@
427
430
  bind:uploading
428
431
  show_progress={false}
429
432
  disable_click={true}
430
- on:error
433
+ onerror={(e) => dispatch("error", e)}
431
434
  hidden={true}
432
435
  {upload}
433
436
  {stream_handler}
@@ -438,7 +441,7 @@
438
441
  class="input-wrapper"
439
442
  class:has-files={value.files.length > 0 || uploading}
440
443
  >
441
- {#if value.files.length > 0 || uploading || show_upload}
444
+ {#if value.files.length > 0 || uploading}
442
445
  <div
443
446
  class="thumbnails"
444
447
  aria-label="Uploaded files"
@@ -496,35 +499,49 @@
496
499
  {/if}
497
500
 
498
501
  <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
- />
502
+ {#if show_upload && value.files.length === 0 && !uploading}
503
+ <button
504
+ data-testid="upload-button"
505
+ class="upload-button icon-button"
506
+ {disabled}
507
+ on:click={handle_upload_click}
508
+ aria-label="Upload a file"
509
+ >
510
+ <Paperclip />
511
+ </button>
512
+ {/if}
513
+ <div class="textarea-wrapper">
514
+ <textarea
515
+ data-testid="textbox"
516
+ use:text_area_resize={{
517
+ text: value.text,
518
+ lines: lines,
519
+ max_lines: max_lines
520
+ }}
521
+ class:no-label={!show_label}
522
+ dir={rtl ? "rtl" : "ltr"}
523
+ bind:value={value.text}
524
+ bind:this={el}
525
+ {placeholder}
526
+ rows={lines}
527
+ {disabled}
528
+ on:keypress={handle_keypress}
529
+ on:blur
530
+ on:select={handle_select}
531
+ on:focus
532
+ on:scroll={handle_scroll}
533
+ on:paste={handle_paste}
534
+ style={text_align ? "text-align: " + text_align : ""}
535
+ autocapitalize={html_attributes?.autocapitalize}
536
+ autocorrect={html_attributes?.autocorrect}
537
+ spellcheck={html_attributes?.spellcheck}
538
+ autocomplete={html_attributes?.autocomplete}
539
+ tabindex={html_attributes?.tabindex}
540
+ enterkeyhint={html_attributes?.enterkeyhint}
541
+ lang={html_attributes?.lang}
542
+ />
543
+ <ScrollFade visible={show_fade} position="absolute" />
544
+ </div>
528
545
 
529
546
  {#if sources && sources.includes("microphone")}
530
547
  <button
@@ -617,7 +634,7 @@
617
634
  gap: 0;
618
635
  background: var(--block-background-fill);
619
636
  border-radius: var(--radius-xl);
620
- padding: var(--spacing-sm);
637
+ padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) 0;
621
638
  align-items: flex-start;
622
639
  min-height: auto;
623
640
  }
@@ -628,7 +645,7 @@
628
645
 
629
646
  .input-row {
630
647
  display: flex;
631
- align-items: flex-end;
648
+ align-items: flex-start;
632
649
  gap: var(--spacing-sm);
633
650
  width: 100%;
634
651
  }
@@ -724,7 +741,7 @@
724
741
  flex-grow: 1;
725
742
  outline: none !important;
726
743
  background: transparent;
727
- padding: 0;
744
+ padding: var(--spacing-sm) 0;
728
745
  color: var(--body-text-color);
729
746
  font-weight: var(--input-text-weight);
730
747
  font-size: var(--input-text-size);
@@ -734,7 +751,6 @@
734
751
  position: relative;
735
752
  z-index: var(--layer-1);
736
753
  text-align: left;
737
- min-height: var(--size-9);
738
754
  }
739
755
 
740
756
  textarea:disabled {
@@ -750,15 +766,12 @@
750
766
  text-align: right;
751
767
  }
752
768
 
753
- textarea[dir="rtl"] ~ .submit-button {
754
- order: -1;
755
- }
756
-
757
769
  textarea[dir="rtl"] ~ .submit-button :global(svg) {
758
770
  transform: scaleX(-1);
759
771
  }
760
772
 
761
- .microphone-button {
773
+ .microphone-button,
774
+ .icon-button {
762
775
  color: var(--body-text-color);
763
776
  cursor: pointer;
764
777
  padding: var(--spacing-sm);
@@ -796,21 +809,25 @@
796
809
  height: var(--size-5);
797
810
  }
798
811
 
799
- .microphone-button {
812
+ .microphone-button,
813
+ .icon-button {
800
814
  width: var(--size-9);
801
815
  height: var(--size-9);
802
816
  }
803
817
 
804
- .microphone-button:hover:not(:disabled) {
818
+ .microphone-button:hover:not(:disabled),
819
+ .icon-button:hover:not(:disabled) {
805
820
  background: var(--button-secondary-background-fill);
806
821
  }
807
822
 
808
- .microphone-button:disabled {
823
+ .microphone-button:disabled,
824
+ .icon-button:disabled {
809
825
  opacity: 0.5;
810
826
  cursor: not-allowed;
811
827
  }
812
828
 
813
- .microphone-button :global(svg) {
829
+ .microphone-button :global(svg),
830
+ .icon-button :global(svg) {
814
831
  width: var(--size-5);
815
832
  height: var(--size-5);
816
833
  }
@@ -1002,4 +1019,13 @@
1002
1019
  opacity: 1;
1003
1020
  }
1004
1021
  }
1022
+
1023
+ .textarea-wrapper {
1024
+ position: relative;
1025
+ flex-grow: 1;
1026
+ }
1027
+
1028
+ .textarea-wrapper textarea {
1029
+ width: 100%;
1030
+ }
1005
1031
  </style>