@gradio/multimodaltextbox 0.7.8 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @gradio/multimodaltextbox
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Features
6
+
7
+ - [#10186](https://github.com/gradio-app/gradio/pull/10186) [`9b17032`](https://github.com/gradio-app/gradio/commit/9b17032a6564c8bda4ab1749989a9797c670919c) - Add Microphone Input to MultimodalTextbox. Thanks @dawoodkhan82!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/atoms@0.13.0
12
+ - @gradio/utils@0.10.0
13
+ - @gradio/upload@0.14.4
14
+ - @gradio/client@1.9.0
15
+ - @gradio/icons@0.9.0
16
+ - @gradio/statustracker@0.10.0
17
+ - @gradio/image@0.20.0
18
+ - @gradio/video@0.13.0
19
+
20
+ ## 0.8.0
21
+
22
+ ### Features
23
+
24
+ - [#10135](https://github.com/gradio-app/gradio/pull/10135) [`3e93740`](https://github.com/gradio-app/gradio/commit/3e93740f057712278a7c2194a1702f48915683e1) - Improve pasted text behaviour in `Multimodaltextbox`. Thanks @hannahblair!
25
+
26
+ ### Fixes
27
+
28
+ - [#10168](https://github.com/gradio-app/gradio/pull/10168) [`7d70596`](https://github.com/gradio-app/gradio/commit/7d70596d0b3343dc4b357758f85420a0afdb65a2) - Multimodal autofocus fix. Thanks @dawoodkhan82!
29
+
30
+ ### Dependency updates
31
+
32
+ - @gradio/video@0.12.1
33
+ - @gradio/statustracker@0.9.7
34
+ - @gradio/upload@0.14.3
35
+ - @gradio/atoms@0.12.0
36
+ - @gradio/image@0.19.0
37
+
3
38
  ## 0.7.8
4
39
 
5
40
  ### Dependency updates
package/Index.svelte CHANGED
@@ -12,6 +12,8 @@
12
12
  import { StatusTracker } from "@gradio/statustracker";
13
13
  import type { LoadingStatus } from "@gradio/statustracker";
14
14
  import type { FileData } from "@gradio/client";
15
+ import { onMount } from "svelte";
16
+ import type { WaveformOptions } from "../audio/shared/types";
15
17
 
16
18
  export let gradio: Gradio<{
17
19
  change: typeof value;
@@ -23,6 +25,11 @@
23
25
  focus: never;
24
26
  error: string;
25
27
  clear_status: LoadingStatus;
28
+ start_recording: never;
29
+ pause_recording: never;
30
+ stop_recording: never;
31
+ upload: FileData[] | FileData;
32
+ clear: undefined;
26
33
  }>;
27
34
  export let elem_id = "";
28
35
  export let elem_classes: string[] = [];
@@ -38,7 +45,6 @@
38
45
  export let info: string | undefined = undefined;
39
46
  export let show_label: boolean;
40
47
  export let max_lines: number;
41
- export let container = true;
42
48
  export let scale: number | null = null;
43
49
  export let min_width: number | undefined = undefined;
44
50
  export let submit_btn: string | boolean | null = null;
@@ -52,8 +58,53 @@
52
58
  export let interactive: boolean;
53
59
  export let root: string;
54
60
  export let file_count: "single" | "multiple" | "directory";
61
+ export let max_plain_text_length: number;
62
+ export let sources: ["microphone" | "upload"] = ["upload"];
63
+ export let waveform_options: WaveformOptions = {};
55
64
 
56
65
  let dragging: boolean;
66
+ let active_source: "microphone" | null = null;
67
+ let waveform_settings: Record<string, any>;
68
+ let color_accent = "darkorange";
69
+
70
+ onMount(() => {
71
+ color_accent = getComputedStyle(document?.documentElement).getPropertyValue(
72
+ "--color-accent"
73
+ );
74
+ set_trim_region_colour();
75
+ waveform_settings.waveColor = waveform_options.waveform_color || "#9ca3af";
76
+ waveform_settings.progressColor =
77
+ waveform_options.waveform_progress_color || color_accent;
78
+ waveform_settings.mediaControls = waveform_options.show_controls;
79
+ waveform_settings.sampleRate = waveform_options.sample_rate || 44100;
80
+ });
81
+
82
+ $: waveform_settings = {
83
+ height: 50,
84
+
85
+ barWidth: 2,
86
+ barGap: 3,
87
+ cursorWidth: 2,
88
+ cursorColor: "#ddd5e9",
89
+ autoplay: false,
90
+ barRadius: 10,
91
+ dragToSeek: true,
92
+ normalize: true,
93
+ minPxPerSec: 20
94
+ };
95
+
96
+ const trim_region_settings = {
97
+ color: waveform_options.trim_region_color,
98
+ drag: true,
99
+ resize: true
100
+ };
101
+
102
+ function set_trim_region_colour(): void {
103
+ document.documentElement.style.setProperty(
104
+ "--trim-region-color",
105
+ trim_region_settings.color || color_accent
106
+ );
107
+ }
57
108
  </script>
58
109
 
59
110
  <Block
@@ -79,6 +130,7 @@
79
130
  bind:value
80
131
  bind:value_is_output
81
132
  bind:dragging
133
+ bind:active_source
82
134
  {file_types}
83
135
  {root}
84
136
  {label}
@@ -87,14 +139,16 @@
87
139
  {lines}
88
140
  {rtl}
89
141
  {text_align}
142
+ {waveform_settings}
143
+ i18n={gradio.i18n}
90
144
  max_lines={!max_lines ? lines + 1 : max_lines}
91
145
  {placeholder}
92
146
  {submit_btn}
93
147
  {stop_btn}
94
148
  {autofocus}
95
- {container}
96
149
  {autoscroll}
97
150
  {file_count}
151
+ {sources}
98
152
  max_file_size={gradio.max_file_size}
99
153
  on:change={() => gradio.dispatch("change", value)}
100
154
  on:input={() => gradio.dispatch("input")}
@@ -106,8 +160,14 @@
106
160
  on:error={({ detail }) => {
107
161
  gradio.dispatch("error", detail);
108
162
  }}
163
+ on:start_recording={() => gradio.dispatch("start_recording")}
164
+ on:pause_recording={() => gradio.dispatch("pause_recording")}
165
+ on:stop_recording={() => gradio.dispatch("stop_recording")}
166
+ on:upload={(e) => gradio.dispatch("upload", e.detail)}
167
+ on:clear={() => gradio.dispatch("clear")}
109
168
  disabled={!interactive}
110
169
  upload={(...args) => gradio.client.upload(...args)}
111
170
  stream_handler={(...args) => gradio.client.stream(...args)}
171
+ {max_plain_text_length}
112
172
  />
113
173
  </Block>
@@ -42,6 +42,12 @@
42
42
  description: "Whether to render right-to-left",
43
43
  control: { type: "boolean" },
44
44
  defaultValue: false
45
+ },
46
+ sources: {
47
+ options: ["upload", "microphone"],
48
+ description: "The sources to enable",
49
+ control: { type: "select" },
50
+ defaultValue: ["upload", "microphone"]
45
51
  }
46
52
  }}
47
53
  />
@@ -87,3 +93,7 @@
87
93
  }
88
94
  }}
89
95
  />
96
+ <Story
97
+ name="MultimodalTextbox with microphone input"
98
+ args={{ sources: ["microphone"] }}
99
+ />
package/dist/Index.svelte CHANGED
@@ -7,6 +7,7 @@ export { default as BaseExample } from "./Example.svelte";
7
7
  <script>import MultimodalTextbox from "./shared/MultimodalTextbox.svelte";
8
8
  import { Block } from "@gradio/atoms";
9
9
  import { StatusTracker } from "@gradio/statustracker";
10
+ import { onMount } from "svelte";
10
11
  export let gradio;
11
12
  export let elem_id = "";
12
13
  export let elem_classes = [];
@@ -22,7 +23,6 @@ export let label = "MultimodalTextbox";
22
23
  export let info = void 0;
23
24
  export let show_label;
24
25
  export let max_lines;
25
- export let container = true;
26
26
  export let scale = null;
27
27
  export let min_width = void 0;
28
28
  export let submit_btn = null;
@@ -36,7 +36,47 @@ export let autoscroll = true;
36
36
  export let interactive;
37
37
  export let root;
38
38
  export let file_count;
39
+ export let max_plain_text_length;
40
+ export let sources = ["upload"];
41
+ export let waveform_options = {};
39
42
  let dragging;
43
+ let active_source = null;
44
+ let waveform_settings;
45
+ let color_accent = "darkorange";
46
+ onMount(() => {
47
+ color_accent = getComputedStyle(document?.documentElement).getPropertyValue(
48
+ "--color-accent"
49
+ );
50
+ set_trim_region_colour();
51
+ waveform_settings.waveColor = waveform_options.waveform_color || "#9ca3af";
52
+ waveform_settings.progressColor = waveform_options.waveform_progress_color || color_accent;
53
+ waveform_settings.mediaControls = waveform_options.show_controls;
54
+ waveform_settings.sampleRate = waveform_options.sample_rate || 44100;
55
+ });
56
+ $:
57
+ waveform_settings = {
58
+ height: 50,
59
+ barWidth: 2,
60
+ barGap: 3,
61
+ cursorWidth: 2,
62
+ cursorColor: "#ddd5e9",
63
+ autoplay: false,
64
+ barRadius: 10,
65
+ dragToSeek: true,
66
+ normalize: true,
67
+ minPxPerSec: 20
68
+ };
69
+ const trim_region_settings = {
70
+ color: waveform_options.trim_region_color,
71
+ drag: true,
72
+ resize: true
73
+ };
74
+ function set_trim_region_colour() {
75
+ document.documentElement.style.setProperty(
76
+ "--trim-region-color",
77
+ trim_region_settings.color || color_accent
78
+ );
79
+ }
40
80
  </script>
41
81
 
42
82
  <Block
@@ -62,6 +102,7 @@ let dragging;
62
102
  bind:value
63
103
  bind:value_is_output
64
104
  bind:dragging
105
+ bind:active_source
65
106
  {file_types}
66
107
  {root}
67
108
  {label}
@@ -70,14 +111,16 @@ let dragging;
70
111
  {lines}
71
112
  {rtl}
72
113
  {text_align}
114
+ {waveform_settings}
115
+ i18n={gradio.i18n}
73
116
  max_lines={!max_lines ? lines + 1 : max_lines}
74
117
  {placeholder}
75
118
  {submit_btn}
76
119
  {stop_btn}
77
120
  {autofocus}
78
- {container}
79
121
  {autoscroll}
80
122
  {file_count}
123
+ {sources}
81
124
  max_file_size={gradio.max_file_size}
82
125
  on:change={() => gradio.dispatch("change", value)}
83
126
  on:input={() => gradio.dispatch("input")}
@@ -89,8 +132,14 @@ let dragging;
89
132
  on:error={({ detail }) => {
90
133
  gradio.dispatch("error", detail);
91
134
  }}
135
+ on:start_recording={() => gradio.dispatch("start_recording")}
136
+ on:pause_recording={() => gradio.dispatch("pause_recording")}
137
+ on:stop_recording={() => gradio.dispatch("stop_recording")}
138
+ on:upload={(e) => gradio.dispatch("upload", e.detail)}
139
+ on:clear={() => gradio.dispatch("clear")}
92
140
  disabled={!interactive}
93
141
  upload={(...args) => gradio.client.upload(...args)}
94
142
  stream_handler={(...args) => gradio.client.stream(...args)}
143
+ {max_plain_text_length}
95
144
  />
96
145
  </Block>
@@ -19,6 +19,11 @@ declare const __propDef: {
19
19
  focus: never;
20
20
  error: string;
21
21
  clear_status: LoadingStatus;
22
+ start_recording: never;
23
+ pause_recording: never;
24
+ stop_recording: never;
25
+ upload: FileData[] | FileData;
26
+ clear: undefined;
22
27
  }>;
23
28
  elem_id?: string | undefined;
24
29
  elem_classes?: string[] | undefined;
@@ -34,7 +39,6 @@ declare const __propDef: {
34
39
  info?: string | undefined;
35
40
  show_label: boolean;
36
41
  max_lines: number;
37
- container?: boolean | undefined;
38
42
  scale?: (number | null) | undefined;
39
43
  min_width?: number | undefined;
40
44
  submit_btn?: (string | boolean | null) | undefined;
@@ -48,6 +52,9 @@ declare const __propDef: {
48
52
  interactive: boolean;
49
53
  root: string;
50
54
  file_count: "single" | "multiple" | "directory";
55
+ max_plain_text_length: number;
56
+ sources?: ["microphone" | "upload"] | undefined;
57
+ waveform_options?: any;
51
58
  };
52
59
  events: {
53
60
  [evt: string]: CustomEvent<any>;
@@ -71,6 +78,11 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
71
78
  focus: never;
72
79
  error: string;
73
80
  clear_status: LoadingStatus;
81
+ start_recording: never;
82
+ pause_recording: never;
83
+ stop_recording: never;
84
+ upload: FileData[] | FileData;
85
+ clear: undefined;
74
86
  }>;
75
87
  /**accessor*/
76
88
  set gradio(_: Gradio<{
@@ -86,6 +98,11 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
86
98
  focus: never;
87
99
  error: string;
88
100
  clear_status: LoadingStatus;
101
+ start_recording: never;
102
+ pause_recording: never;
103
+ stop_recording: never;
104
+ upload: FileData[] | FileData;
105
+ clear: undefined;
89
106
  }>);
90
107
  get elem_id(): string | undefined;
91
108
  /**accessor*/
@@ -126,9 +143,6 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
126
143
  get max_lines(): number;
127
144
  /**accessor*/
128
145
  set max_lines(_: number);
129
- get container(): boolean | undefined;
130
- /**accessor*/
131
- set container(_: boolean | undefined);
132
146
  get scale(): number | null | undefined;
133
147
  /**accessor*/
134
148
  set scale(_: number | null | undefined);
@@ -168,4 +182,13 @@ export default class Index extends SvelteComponent<IndexProps, IndexEvents, Inde
168
182
  get file_count(): "single" | "multiple" | "directory";
169
183
  /**accessor*/
170
184
  set file_count(_: "single" | "multiple" | "directory");
185
+ get max_plain_text_length(): number;
186
+ /**accessor*/
187
+ set max_plain_text_length(_: number);
188
+ get sources(): ["upload" | "microphone"] | undefined;
189
+ /**accessor*/
190
+ set sources(_: ["upload" | "microphone"] | undefined);
191
+ get waveform_options(): any;
192
+ /**accessor*/
193
+ set waveform_options(_: any);
171
194
  }
@@ -1,4 +1,5 @@
1
1
  <script>import {
2
+ onMount,
2
3
  beforeUpdate,
3
4
  afterUpdate,
4
5
  createEventDispatcher,
@@ -15,20 +16,22 @@ import {
15
16
  Paperclip,
16
17
  Video,
17
18
  Send,
18
- Square
19
+ Square,
20
+ Microphone
19
21
  } from "@gradio/icons";
22
+ import InteractiveAudio from "../../audio/interactive/InteractiveAudio.svelte";
20
23
  export let value = {
21
24
  text: "",
22
25
  files: []
23
26
  };
24
27
  export let value_is_output = false;
25
28
  export let lines = 1;
29
+ export let i18n;
26
30
  export let placeholder = "Type here...";
27
31
  export let disabled = false;
28
32
  export let label;
29
33
  export let info = void 0;
30
34
  export let show_label = true;
31
- export let container = true;
32
35
  export let max_lines;
33
36
  export let submit_btn = null;
34
37
  export let stop_btn = null;
@@ -42,6 +45,11 @@ export let max_file_size = null;
42
45
  export let upload;
43
46
  export let stream_handler;
44
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
+ export let sources = ["upload"];
52
+ export let active_source = null;
45
53
  let upload_component;
46
54
  let hidden_upload;
47
55
  let el;
@@ -51,8 +59,10 @@ let user_has_scrolled_up = false;
51
59
  export let dragging = false;
52
60
  let uploading = false;
53
61
  let oldValue = value.text;
62
+ let recording = false;
54
63
  $:
55
64
  dispatch("drag", dragging);
65
+ let mic_audio = null;
56
66
  let full_container;
57
67
  $:
58
68
  if (oldValue !== value.text) {
@@ -79,10 +89,12 @@ async function handle_change() {
79
89
  dispatch("input");
80
90
  }
81
91
  }
82
- afterUpdate(() => {
92
+ onMount(() => {
83
93
  if (autofocus && el !== null) {
84
94
  el.focus();
85
95
  }
96
+ });
97
+ afterUpdate(() => {
86
98
  if (can_scroll && autoscroll) {
87
99
  scroll();
88
100
  }
@@ -105,6 +117,11 @@ async function handle_keypress(e) {
105
117
  } else if (e.key === "Enter" && !e.shiftKey && lines === 1 && max_lines >= 1) {
106
118
  e.preventDefault();
107
119
  dispatch("submit");
120
+ active_source = null;
121
+ if (mic_audio) {
122
+ value.files.push(mic_audio);
123
+ value = value;
124
+ }
108
125
  }
109
126
  }
110
127
  function handle_scroll(event) {
@@ -154,11 +171,28 @@ function handle_stop() {
154
171
  }
155
172
  function handle_submit() {
156
173
  dispatch("submit");
174
+ active_source = null;
175
+ if (mic_audio) {
176
+ value.files.push(mic_audio);
177
+ value = value;
178
+ }
157
179
  }
158
- function handle_paste(event) {
180
+ async function handle_paste(event) {
159
181
  if (!event.clipboardData)
160
182
  return;
161
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
+ }
162
196
  for (let index in items) {
163
197
  const item = items[index];
164
198
  if (item.kind === "file" && item.type.includes("image")) {
@@ -222,127 +256,167 @@ function handle_drop(event) {
222
256
  role="group"
223
257
  aria-label="Multimedia input field"
224
258
  >
225
- <!-- svelte-ignore a11y-autofocus -->
226
- <label class:container>
227
- <BlockTitle {root} {show_label} {info}>{label}</BlockTitle>
228
- {#if value.files.length > 0 || uploading}
229
- <div
230
- class="thumbnails scroll-hide"
231
- aria-label="Uploaded files"
232
- data-testid="container_el"
233
- style="display: {value.files.length > 0 || uploading
234
- ? 'flex'
235
- : 'none'};"
259
+ <BlockTitle {root} {show_label} {info}>{label}</BlockTitle>
260
+ {#if value.files.length > 0 || uploading}
261
+ <div
262
+ class="thumbnails scroll-hide"
263
+ aria-label="Uploaded files"
264
+ data-testid="container_el"
265
+ style="display: {value.files.length > 0 || uploading ? 'flex' : 'none'};"
266
+ >
267
+ {#each value.files as file, index}
268
+ <span role="listitem" aria-label="File thumbnail">
269
+ <button class="thumbnail-item thumbnail-small">
270
+ <button
271
+ class:disabled
272
+ class="delete-button"
273
+ on:click={(event) => remove_thumbnail(event, index)}
274
+ ><Clear /></button
275
+ >
276
+ {#if file.mime_type && file.mime_type.includes("image")}
277
+ <Image
278
+ src={file.url}
279
+ title={null}
280
+ alt=""
281
+ loading="lazy"
282
+ class={"thumbnail-image"}
283
+ />
284
+ {:else if file.mime_type && file.mime_type.includes("audio")}
285
+ <Music />
286
+ {:else if file.mime_type && file.mime_type.includes("video")}
287
+ <Video />
288
+ {:else}
289
+ <File />
290
+ {/if}
291
+ </button>
292
+ </span>
293
+ {/each}
294
+ {#if uploading}
295
+ <div class="loader" role="status" aria-label="Uploading"></div>
296
+ {/if}
297
+ </div>
298
+ {/if}
299
+ {#if sources && sources.includes("microphone") && active_source === "microphone"}
300
+ <InteractiveAudio
301
+ on:change={({ detail }) => {
302
+ if (detail !== null) {
303
+ mic_audio = detail;
304
+ }
305
+ }}
306
+ on:clear={() => {
307
+ active_source = null;
308
+ }}
309
+ on:start_recording={() => dispatch("start_recording")}
310
+ on:pause_recording={() => dispatch("pause_recording")}
311
+ on:stop_recording={() => dispatch("stop_recording")}
312
+ sources={["microphone"]}
313
+ class_name="compact-audio"
314
+ {recording}
315
+ {waveform_settings}
316
+ {waveform_options}
317
+ {i18n}
318
+ {active_source}
319
+ {upload}
320
+ {stream_handler}
321
+ stream_every={1}
322
+ editable={true}
323
+ {label}
324
+ {root}
325
+ loop={false}
326
+ show_label={false}
327
+ show_download_button={false}
328
+ dragging={false}
329
+ />
330
+ {/if}
331
+ <div class="input-container">
332
+ {#if sources && sources.includes("upload") && !disabled && !(file_count === "single" && value.files.length > 0)}
333
+ <Upload
334
+ bind:this={upload_component}
335
+ on:load={handle_upload}
336
+ {file_count}
337
+ filetype={file_types}
338
+ {root}
339
+ {max_file_size}
340
+ bind:dragging
341
+ bind:uploading
342
+ show_progress={false}
343
+ disable_click={true}
344
+ bind:hidden_upload
345
+ on:error
346
+ hidden={true}
347
+ {upload}
348
+ {stream_handler}
349
+ />
350
+ <button
351
+ data-testid="upload-button"
352
+ class="upload-button"
353
+ on:click={handle_upload_click}><Paperclip /></button
236
354
  >
237
- {#each value.files as file, index}
238
- <span role="listitem" aria-label="File thumbnail">
239
- <button class="thumbnail-item thumbnail-small">
240
- <button
241
- class:disabled
242
- class="delete-button"
243
- on:click={(event) => remove_thumbnail(event, index)}
244
- ><Clear /></button
245
- >
246
- {#if file.mime_type && file.mime_type.includes("image")}
247
- <Image
248
- src={file.url}
249
- title={null}
250
- alt=""
251
- loading="lazy"
252
- class={"thumbnail-image"}
253
- />
254
- {:else if file.mime_type && file.mime_type.includes("audio")}
255
- <Music />
256
- {:else if file.mime_type && file.mime_type.includes("video")}
257
- <Video />
258
- {:else}
259
- <File />
260
- {/if}
261
- </button>
262
- </span>
263
- {/each}
264
- {#if uploading}
265
- <div class="loader" role="status" aria-label="Uploading"></div>
266
- {/if}
267
- </div>
268
355
  {/if}
269
- <div class="input-container">
270
- {#if !disabled && !(file_count === "single" && value.files.length > 0)}
271
- <Upload
272
- bind:this={upload_component}
273
- on:load={handle_upload}
274
- {file_count}
275
- filetype={file_types}
276
- {root}
277
- {max_file_size}
278
- bind:dragging
279
- bind:uploading
280
- show_progress={false}
281
- disable_click={true}
282
- bind:hidden_upload
283
- on:error
284
- hidden={true}
285
- {upload}
286
- {stream_handler}
287
- ></Upload>
288
- <button
289
- data-testid="upload-button"
290
- class="upload-button"
291
- on:click={handle_upload_click}><Paperclip /></button
292
- >
293
- {/if}
294
- <textarea
295
- data-testid="textbox"
296
- use:text_area_resize={{
297
- text: value.text,
298
- lines: lines,
299
- max_lines: max_lines
356
+ {#if sources && sources.includes("microphone")}
357
+ <button
358
+ data-testid="microphone-button"
359
+ class="microphone-button"
360
+ class:recording
361
+ on:click={() => {
362
+ active_source = active_source !== "microphone" ? "microphone" : null;
300
363
  }}
301
- class="scroll-hide"
302
- class:no-label={!show_label}
303
- dir={rtl ? "rtl" : "ltr"}
304
- bind:value={value.text}
305
- bind:this={el}
306
- {placeholder}
307
- rows={lines}
308
- {disabled}
309
- {autofocus}
310
- on:keypress={handle_keypress}
311
- on:blur
312
- on:select={handle_select}
313
- on:focus
314
- on:scroll={handle_scroll}
315
- on:paste={handle_paste}
316
- style={text_align ? "text-align: " + text_align : ""}
317
- />
318
- {#if submit_btn}
319
- <button
320
- class="submit-button"
321
- class:padded-button={submit_btn !== true}
322
- on:click={handle_submit}
323
- >
324
- {#if submit_btn === true}
325
- <Send />
326
- {:else}
327
- {submit_btn}
328
- {/if}
329
- </button>
330
- {/if}
331
- {#if stop_btn}
332
- <button
333
- class="stop-button"
334
- class:padded-button={stop_btn !== true}
335
- on:click={handle_stop}
336
- >
337
- {#if stop_btn === true}
338
- <Square fill={"none"} stroke_width={2.5} />
339
- {:else}
340
- {stop_btn}
341
- {/if}
342
- </button>
343
- {/if}
344
- </div>
345
- </label>
364
+ >
365
+ <Microphone />
366
+ </button>
367
+ {/if}
368
+ <!-- svelte-ignore a11y-autofocus -->
369
+ <textarea
370
+ data-testid="textbox"
371
+ use:text_area_resize={{
372
+ text: value.text,
373
+ lines: lines,
374
+ max_lines: max_lines
375
+ }}
376
+ class="scroll-hide"
377
+ class:no-label={!show_label}
378
+ dir={rtl ? "rtl" : "ltr"}
379
+ bind:value={value.text}
380
+ bind:this={el}
381
+ {placeholder}
382
+ rows={lines}
383
+ {disabled}
384
+ {autofocus}
385
+ on:keypress={handle_keypress}
386
+ on:blur
387
+ on:select={handle_select}
388
+ on:focus
389
+ on:scroll={handle_scroll}
390
+ on:paste={handle_paste}
391
+ style={text_align ? "text-align: " + text_align : ""}
392
+ />
393
+ {#if submit_btn}
394
+ <button
395
+ class="submit-button"
396
+ class:padded-button={submit_btn !== true}
397
+ on:click={handle_submit}
398
+ >
399
+ {#if submit_btn === true}
400
+ <Send />
401
+ {:else}
402
+ {submit_btn}
403
+ {/if}
404
+ </button>
405
+ {/if}
406
+ {#if stop_btn}
407
+ <button
408
+ class="stop-button"
409
+ class:padded-button={stop_btn !== true}
410
+ on:click={handle_stop}
411
+ >
412
+ {#if stop_btn === true}
413
+ <Square fill={"none"} stroke_width={2.5} />
414
+ {:else}
415
+ {stop_btn}
416
+ {/if}
417
+ </button>
418
+ {/if}
419
+ </div>
346
420
  </div>
347
421
 
348
422
  <style>
@@ -404,6 +478,7 @@ function handle_drop(event) {
404
478
  color: var(--input-placeholder-color);
405
479
  }
406
480
 
481
+ .microphone-button,
407
482
  .upload-button,
408
483
  .submit-button,
409
484
  .stop-button {
@@ -420,29 +495,34 @@ function handle_drop(event) {
420
495
  justify-content: center;
421
496
  align-items: center;
422
497
  z-index: var(--layer-1);
498
+ margin-left: var(--spacing-sm);
423
499
  }
424
500
  .padded-button {
425
501
  padding: 0 10px;
426
502
  }
427
503
 
504
+ .microphone-button,
428
505
  .stop-button,
429
506
  .upload-button,
430
507
  .submit-button {
431
508
  background: var(--button-secondary-background-fill);
432
509
  }
433
510
 
511
+ .microphone-button:hover,
434
512
  .stop-button:hover,
435
513
  .upload-button:hover,
436
514
  .submit-button:hover {
437
515
  background: var(--button-secondary-background-fill-hover);
438
516
  }
439
517
 
518
+ .microphone-button:disabled,
440
519
  .stop-button:disabled,
441
520
  .upload-button:disabled,
442
521
  .submit-button:disabled {
443
522
  background: var(--button-secondary-background-fill);
444
523
  cursor: initial;
445
524
  }
525
+ .microphone-button:active,
446
526
  .stop-button:active,
447
527
  .upload-button:active,
448
528
  .submit-button:active {
@@ -453,6 +533,7 @@ function handle_drop(event) {
453
533
  height: 22px;
454
534
  width: 22px;
455
535
  }
536
+ .microphone-button :global(svg),
456
537
  .upload-button :global(svg) {
457
538
  height: 17px;
458
539
  width: 17px;
@@ -1,4 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
+ import type { I18nFormatter } from "js/core/src/gradio_helper";
2
3
  import type { FileData, Client } from "@gradio/client";
3
4
  import type { SelectData } from "@gradio/utils";
4
5
  declare const __propDef: {
@@ -9,12 +10,12 @@ declare const __propDef: {
9
10
  } | undefined;
10
11
  value_is_output?: boolean | undefined;
11
12
  lines?: number | undefined;
13
+ i18n: I18nFormatter;
12
14
  placeholder?: string | undefined;
13
15
  disabled?: boolean | undefined;
14
16
  label: string;
15
17
  info?: string | undefined;
16
18
  show_label?: boolean | undefined;
17
- container?: boolean | undefined;
18
19
  max_lines: number;
19
20
  submit_btn?: (string | boolean | null) | undefined;
20
21
  stop_btn?: (string | boolean | null) | undefined;
@@ -28,6 +29,11 @@ declare const __propDef: {
28
29
  upload: Client["upload"];
29
30
  stream_handler: Client["stream"];
30
31
  file_count?: ("single" | "multiple" | "directory") | undefined;
32
+ max_plain_text_length?: number | undefined;
33
+ waveform_settings: Record<string, any>;
34
+ waveform_options?: any;
35
+ sources?: ["microphone" | "upload"] | undefined;
36
+ active_source?: ("microphone" | null) | undefined;
31
37
  dragging?: boolean | undefined;
32
38
  };
33
39
  events: {
@@ -47,6 +53,9 @@ declare const __propDef: {
47
53
  upload: CustomEvent<any>;
48
54
  clear: CustomEvent<undefined>;
49
55
  load: CustomEvent<any>;
56
+ start_recording: CustomEvent<undefined>;
57
+ pause_recording: CustomEvent<undefined>;
58
+ stop_recording: CustomEvent<undefined>;
50
59
  } & {
51
60
  [evt: string]: CustomEvent<any>;
52
61
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/multimodaltextbox",
3
- "version": "0.7.8",
3
+ "version": "0.9.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -22,14 +22,14 @@
22
22
  "./package.json": "./package.json"
23
23
  },
24
24
  "dependencies": {
25
- "@gradio/atoms": "^0.11.2",
26
- "@gradio/icons": "^0.8.1",
27
- "@gradio/statustracker": "^0.9.6",
28
- "@gradio/utils": "^0.9.0",
29
- "@gradio/upload": "^0.14.2",
30
- "@gradio/image": "^0.18.0",
31
- "@gradio/video": "^0.12.0",
32
- "@gradio/client": "^1.8.0"
25
+ "@gradio/icons": "^0.9.0",
26
+ "@gradio/atoms": "^0.13.0",
27
+ "@gradio/statustracker": "^0.10.0",
28
+ "@gradio/upload": "^0.14.4",
29
+ "@gradio/utils": "^0.10.0",
30
+ "@gradio/image": "^0.20.0",
31
+ "@gradio/video": "^0.13.0",
32
+ "@gradio/client": "^1.9.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@gradio/preview": "^0.13.0"
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import {
3
+ onMount,
3
4
  beforeUpdate,
4
5
  afterUpdate,
5
6
  createEventDispatcher,
@@ -9,7 +10,9 @@
9
10
  import { BlockTitle } from "@gradio/atoms";
10
11
  import { Upload } from "@gradio/upload";
11
12
  import { Image } from "@gradio/image/shared";
13
+ import type { I18nFormatter } from "js/core/src/gradio_helper";
12
14
  import type { FileData, Client } from "@gradio/client";
15
+ import type { WaveformOptions } from "../../audio/shared/types";
13
16
  import {
14
17
  Clear,
15
18
  File,
@@ -17,9 +20,11 @@
17
20
  Paperclip,
18
21
  Video,
19
22
  Send,
20
- Square
23
+ Square,
24
+ Microphone
21
25
  } from "@gradio/icons";
22
26
  import type { SelectData } from "@gradio/utils";
27
+ import InteractiveAudio from "../../audio/interactive/InteractiveAudio.svelte";
23
28
 
24
29
  export let value: { text: string; files: FileData[] } = {
25
30
  text: "",
@@ -28,12 +33,12 @@
28
33
 
29
34
  export let value_is_output = false;
30
35
  export let lines = 1;
36
+ export let i18n: I18nFormatter;
31
37
  export let placeholder = "Type here...";
32
38
  export let disabled = false;
33
39
  export let label: string;
34
40
  export let info: string | undefined = undefined;
35
41
  export let show_label = true;
36
- export let container = true;
37
42
  export let max_lines: number;
38
43
  export let submit_btn: string | boolean | null = null;
39
44
  export let stop_btn: string | boolean | null = null;
@@ -47,7 +52,11 @@
47
52
  export let upload: Client["upload"];
48
53
  export let stream_handler: Client["stream"];
49
54
  export let file_count: "single" | "multiple" | "directory" = "multiple";
50
-
55
+ export let max_plain_text_length = 1000;
56
+ export let waveform_settings: Record<string, any>;
57
+ export let waveform_options: WaveformOptions = {};
58
+ export let sources: ["microphone" | "upload"] = ["upload"];
59
+ export let active_source: "microphone" | null = null;
51
60
  let upload_component: Upload;
52
61
  let hidden_upload: HTMLInputElement;
53
62
  let el: HTMLTextAreaElement | HTMLInputElement;
@@ -57,7 +66,9 @@
57
66
  export let dragging = false;
58
67
  let uploading = false;
59
68
  let oldValue = value.text;
69
+ let recording = false;
60
70
  $: dispatch("drag", dragging);
71
+ let mic_audio: FileData | null = null;
61
72
 
62
73
  let full_container: HTMLDivElement;
63
74
 
@@ -82,6 +93,9 @@
82
93
  clear: undefined;
83
94
  load: FileData[] | FileData;
84
95
  error: string;
96
+ start_recording: undefined;
97
+ pause_recording: undefined;
98
+ stop_recording: undefined;
85
99
  }>();
86
100
 
87
101
  beforeUpdate(() => {
@@ -101,10 +115,13 @@
101
115
  }
102
116
  }
103
117
 
104
- afterUpdate(() => {
118
+ onMount(() => {
105
119
  if (autofocus && el !== null) {
106
120
  el.focus();
107
121
  }
122
+ });
123
+
124
+ afterUpdate(() => {
108
125
  if (can_scroll && autoscroll) {
109
126
  scroll();
110
127
  }
@@ -136,6 +153,11 @@
136
153
  ) {
137
154
  e.preventDefault();
138
155
  dispatch("submit");
156
+ active_source = null;
157
+ if (mic_audio) {
158
+ value.files.push(mic_audio);
159
+ value = value;
160
+ }
139
161
  }
140
162
  }
141
163
 
@@ -156,7 +178,7 @@
156
178
 
157
179
  async function handle_upload({
158
180
  detail
159
- }: CustomEvent<FileData | FileData[]>): Promise<void> {
181
+ }: CustomEvent<FileData>): Promise<void> {
160
182
  handle_change();
161
183
  if (Array.isArray(detail)) {
162
184
  for (let file of detail) {
@@ -192,11 +214,30 @@
192
214
 
193
215
  function handle_submit(): void {
194
216
  dispatch("submit");
217
+ active_source = null;
218
+ if (mic_audio) {
219
+ value.files.push(mic_audio);
220
+ value = value;
221
+ }
195
222
  }
196
223
 
197
- function handle_paste(event: ClipboardEvent): void {
224
+ async function handle_paste(event: ClipboardEvent): Promise<void> {
198
225
  if (!event.clipboardData) return;
199
226
  const items = event.clipboardData.items;
227
+ const text = event.clipboardData.getData("text");
228
+
229
+ if (text && text.length > max_plain_text_length) {
230
+ event.preventDefault();
231
+ const file = new window.File([text], "pasted_text.txt", {
232
+ type: "text/plain",
233
+ lastModified: Date.now()
234
+ });
235
+ if (upload_component) {
236
+ upload_component.load_files([file]);
237
+ }
238
+ return;
239
+ }
240
+
200
241
  for (let index in items) {
201
242
  const item = items[index];
202
243
  if (item.kind === "file" && item.type.includes("image")) {
@@ -270,127 +311,167 @@
270
311
  role="group"
271
312
  aria-label="Multimedia input field"
272
313
  >
273
- <!-- svelte-ignore a11y-autofocus -->
274
- <label class:container>
275
- <BlockTitle {root} {show_label} {info}>{label}</BlockTitle>
276
- {#if value.files.length > 0 || uploading}
277
- <div
278
- class="thumbnails scroll-hide"
279
- aria-label="Uploaded files"
280
- data-testid="container_el"
281
- style="display: {value.files.length > 0 || uploading
282
- ? 'flex'
283
- : 'none'};"
314
+ <BlockTitle {root} {show_label} {info}>{label}</BlockTitle>
315
+ {#if value.files.length > 0 || uploading}
316
+ <div
317
+ class="thumbnails scroll-hide"
318
+ aria-label="Uploaded files"
319
+ data-testid="container_el"
320
+ style="display: {value.files.length > 0 || uploading ? 'flex' : 'none'};"
321
+ >
322
+ {#each value.files as file, index}
323
+ <span role="listitem" aria-label="File thumbnail">
324
+ <button class="thumbnail-item thumbnail-small">
325
+ <button
326
+ class:disabled
327
+ class="delete-button"
328
+ on:click={(event) => remove_thumbnail(event, index)}
329
+ ><Clear /></button
330
+ >
331
+ {#if file.mime_type && file.mime_type.includes("image")}
332
+ <Image
333
+ src={file.url}
334
+ title={null}
335
+ alt=""
336
+ loading="lazy"
337
+ class={"thumbnail-image"}
338
+ />
339
+ {:else if file.mime_type && file.mime_type.includes("audio")}
340
+ <Music />
341
+ {:else if file.mime_type && file.mime_type.includes("video")}
342
+ <Video />
343
+ {:else}
344
+ <File />
345
+ {/if}
346
+ </button>
347
+ </span>
348
+ {/each}
349
+ {#if uploading}
350
+ <div class="loader" role="status" aria-label="Uploading"></div>
351
+ {/if}
352
+ </div>
353
+ {/if}
354
+ {#if sources && sources.includes("microphone") && active_source === "microphone"}
355
+ <InteractiveAudio
356
+ on:change={({ detail }) => {
357
+ if (detail !== null) {
358
+ mic_audio = detail;
359
+ }
360
+ }}
361
+ on:clear={() => {
362
+ active_source = null;
363
+ }}
364
+ on:start_recording={() => dispatch("start_recording")}
365
+ on:pause_recording={() => dispatch("pause_recording")}
366
+ on:stop_recording={() => dispatch("stop_recording")}
367
+ sources={["microphone"]}
368
+ class_name="compact-audio"
369
+ {recording}
370
+ {waveform_settings}
371
+ {waveform_options}
372
+ {i18n}
373
+ {active_source}
374
+ {upload}
375
+ {stream_handler}
376
+ stream_every={1}
377
+ editable={true}
378
+ {label}
379
+ {root}
380
+ loop={false}
381
+ show_label={false}
382
+ show_download_button={false}
383
+ dragging={false}
384
+ />
385
+ {/if}
386
+ <div class="input-container">
387
+ {#if sources && sources.includes("upload") && !disabled && !(file_count === "single" && value.files.length > 0)}
388
+ <Upload
389
+ bind:this={upload_component}
390
+ on:load={handle_upload}
391
+ {file_count}
392
+ filetype={file_types}
393
+ {root}
394
+ {max_file_size}
395
+ bind:dragging
396
+ bind:uploading
397
+ show_progress={false}
398
+ disable_click={true}
399
+ bind:hidden_upload
400
+ on:error
401
+ hidden={true}
402
+ {upload}
403
+ {stream_handler}
404
+ />
405
+ <button
406
+ data-testid="upload-button"
407
+ class="upload-button"
408
+ on:click={handle_upload_click}><Paperclip /></button
284
409
  >
285
- {#each value.files as file, index}
286
- <span role="listitem" aria-label="File thumbnail">
287
- <button class="thumbnail-item thumbnail-small">
288
- <button
289
- class:disabled
290
- class="delete-button"
291
- on:click={(event) => remove_thumbnail(event, index)}
292
- ><Clear /></button
293
- >
294
- {#if file.mime_type && file.mime_type.includes("image")}
295
- <Image
296
- src={file.url}
297
- title={null}
298
- alt=""
299
- loading="lazy"
300
- class={"thumbnail-image"}
301
- />
302
- {:else if file.mime_type && file.mime_type.includes("audio")}
303
- <Music />
304
- {:else if file.mime_type && file.mime_type.includes("video")}
305
- <Video />
306
- {:else}
307
- <File />
308
- {/if}
309
- </button>
310
- </span>
311
- {/each}
312
- {#if uploading}
313
- <div class="loader" role="status" aria-label="Uploading"></div>
314
- {/if}
315
- </div>
316
410
  {/if}
317
- <div class="input-container">
318
- {#if !disabled && !(file_count === "single" && value.files.length > 0)}
319
- <Upload
320
- bind:this={upload_component}
321
- on:load={handle_upload}
322
- {file_count}
323
- filetype={file_types}
324
- {root}
325
- {max_file_size}
326
- bind:dragging
327
- bind:uploading
328
- show_progress={false}
329
- disable_click={true}
330
- bind:hidden_upload
331
- on:error
332
- hidden={true}
333
- {upload}
334
- {stream_handler}
335
- ></Upload>
336
- <button
337
- data-testid="upload-button"
338
- class="upload-button"
339
- on:click={handle_upload_click}><Paperclip /></button
340
- >
341
- {/if}
342
- <textarea
343
- data-testid="textbox"
344
- use:text_area_resize={{
345
- text: value.text,
346
- lines: lines,
347
- max_lines: max_lines
411
+ {#if sources && sources.includes("microphone")}
412
+ <button
413
+ data-testid="microphone-button"
414
+ class="microphone-button"
415
+ class:recording
416
+ on:click={() => {
417
+ active_source = active_source !== "microphone" ? "microphone" : null;
348
418
  }}
349
- class="scroll-hide"
350
- class:no-label={!show_label}
351
- dir={rtl ? "rtl" : "ltr"}
352
- bind:value={value.text}
353
- bind:this={el}
354
- {placeholder}
355
- rows={lines}
356
- {disabled}
357
- {autofocus}
358
- on:keypress={handle_keypress}
359
- on:blur
360
- on:select={handle_select}
361
- on:focus
362
- on:scroll={handle_scroll}
363
- on:paste={handle_paste}
364
- style={text_align ? "text-align: " + text_align : ""}
365
- />
366
- {#if submit_btn}
367
- <button
368
- class="submit-button"
369
- class:padded-button={submit_btn !== true}
370
- on:click={handle_submit}
371
- >
372
- {#if submit_btn === true}
373
- <Send />
374
- {:else}
375
- {submit_btn}
376
- {/if}
377
- </button>
378
- {/if}
379
- {#if stop_btn}
380
- <button
381
- class="stop-button"
382
- class:padded-button={stop_btn !== true}
383
- on:click={handle_stop}
384
- >
385
- {#if stop_btn === true}
386
- <Square fill={"none"} stroke_width={2.5} />
387
- {:else}
388
- {stop_btn}
389
- {/if}
390
- </button>
391
- {/if}
392
- </div>
393
- </label>
419
+ >
420
+ <Microphone />
421
+ </button>
422
+ {/if}
423
+ <!-- svelte-ignore a11y-autofocus -->
424
+ <textarea
425
+ data-testid="textbox"
426
+ use:text_area_resize={{
427
+ text: value.text,
428
+ lines: lines,
429
+ max_lines: max_lines
430
+ }}
431
+ class="scroll-hide"
432
+ class:no-label={!show_label}
433
+ dir={rtl ? "rtl" : "ltr"}
434
+ bind:value={value.text}
435
+ bind:this={el}
436
+ {placeholder}
437
+ rows={lines}
438
+ {disabled}
439
+ {autofocus}
440
+ on:keypress={handle_keypress}
441
+ on:blur
442
+ on:select={handle_select}
443
+ on:focus
444
+ on:scroll={handle_scroll}
445
+ on:paste={handle_paste}
446
+ style={text_align ? "text-align: " + text_align : ""}
447
+ />
448
+ {#if submit_btn}
449
+ <button
450
+ class="submit-button"
451
+ class:padded-button={submit_btn !== true}
452
+ on:click={handle_submit}
453
+ >
454
+ {#if submit_btn === true}
455
+ <Send />
456
+ {:else}
457
+ {submit_btn}
458
+ {/if}
459
+ </button>
460
+ {/if}
461
+ {#if stop_btn}
462
+ <button
463
+ class="stop-button"
464
+ class:padded-button={stop_btn !== true}
465
+ on:click={handle_stop}
466
+ >
467
+ {#if stop_btn === true}
468
+ <Square fill={"none"} stroke_width={2.5} />
469
+ {:else}
470
+ {stop_btn}
471
+ {/if}
472
+ </button>
473
+ {/if}
474
+ </div>
394
475
  </div>
395
476
 
396
477
  <style>
@@ -452,6 +533,7 @@
452
533
  color: var(--input-placeholder-color);
453
534
  }
454
535
 
536
+ .microphone-button,
455
537
  .upload-button,
456
538
  .submit-button,
457
539
  .stop-button {
@@ -468,29 +550,34 @@
468
550
  justify-content: center;
469
551
  align-items: center;
470
552
  z-index: var(--layer-1);
553
+ margin-left: var(--spacing-sm);
471
554
  }
472
555
  .padded-button {
473
556
  padding: 0 10px;
474
557
  }
475
558
 
559
+ .microphone-button,
476
560
  .stop-button,
477
561
  .upload-button,
478
562
  .submit-button {
479
563
  background: var(--button-secondary-background-fill);
480
564
  }
481
565
 
566
+ .microphone-button:hover,
482
567
  .stop-button:hover,
483
568
  .upload-button:hover,
484
569
  .submit-button:hover {
485
570
  background: var(--button-secondary-background-fill-hover);
486
571
  }
487
572
 
573
+ .microphone-button:disabled,
488
574
  .stop-button:disabled,
489
575
  .upload-button:disabled,
490
576
  .submit-button:disabled {
491
577
  background: var(--button-secondary-background-fill);
492
578
  cursor: initial;
493
579
  }
580
+ .microphone-button:active,
494
581
  .stop-button:active,
495
582
  .upload-button:active,
496
583
  .submit-button:active {
@@ -501,6 +588,7 @@
501
588
  height: 22px;
502
589
  width: 22px;
503
590
  }
591
+ .microphone-button :global(svg),
504
592
  .upload-button :global(svg) {
505
593
  height: 17px;
506
594
  width: 17px;