@gradio/multimodaltextbox 0.14.2 → 0.14.3

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,23 @@
1
1
  # @gradio/multimodaltextbox
2
2
 
3
+ ## 0.14.3
4
+
5
+ ### Dependency updates
6
+
7
+ - @gradio/upload@0.19.0
8
+ - @gradio/statustracker@0.15.4
9
+ - @gradio/client@2.7.1
10
+ - @gradio/image@0.28.4
11
+ - @gradio/video@0.23.2
12
+ - @gradio/audio@0.24.4
13
+
14
+ ## 0.14.2
15
+
16
+ ### Dependency updates
17
+
18
+ - @gradio/utils@0.14.2
19
+ - @gradio/client@2.7.0
20
+
3
21
  ## 0.14.2
4
22
 
5
23
  ### Fixes
@@ -1,182 +1,399 @@
1
- import { test, describe, assert, afterEach, expect, vi } from "vitest";
2
- import { cleanup, render, mock_client } from "@self/tootils/render";
1
+ import { test, describe, afterEach, expect } from "vitest";
2
+ import {
3
+ cleanup,
4
+ render,
5
+ fireEvent,
6
+ waitFor,
7
+ mock_client,
8
+ upload_file,
9
+ drop_file,
10
+ TEST_JPG,
11
+ TEST_TXT
12
+ } from "@self/tootils/render";
13
+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
3
14
  import event from "@testing-library/user-event";
4
15
  import { tick } from "svelte";
5
16
 
6
17
  import MultimodalTextbox from "./Index.svelte";
7
- import type { ILoadingStatus as LoadingStatus } from "@gradio/statustracker";
8
-
9
- const loading_status: LoadingStatus = {
10
- eta: 0,
11
- queue_position: 1,
12
- queue_size: 1,
13
- status: "complete" as LoadingStatus["status"],
14
- scroll_to_output: false,
15
- visible: true,
16
- fn_index: 0,
17
- show_progress: "full"
18
+
19
+ const jpg_file = {
20
+ path: "cats.jpg",
21
+ orig_name: "cats.jpg",
22
+ mime_type: "image/jpeg",
23
+ meta: { _type: "gradio.FileData" }
24
+ };
25
+
26
+ const default_props = {
27
+ label: "MultimodalTextbox",
28
+ show_label: true,
29
+ lines: 1,
30
+ max_lines: 1,
31
+ value: { text: "", files: [] },
32
+ interactive: true,
33
+ root: "http://localhost:7860",
34
+ sources: ["upload"],
35
+ submit_btn: true,
36
+ client: mock_client()
18
37
  };
19
38
 
39
+ function paste_event(
40
+ text: string | null,
41
+ image_name?: string,
42
+ with_html = true
43
+ ): ClipboardEvent {
44
+ const data = new DataTransfer();
45
+ if (text) {
46
+ data.setData("text/plain", text);
47
+ if (with_html) {
48
+ data.setData("text/html", `<table><tr><td>${text}</td></tr></table>`);
49
+ }
50
+ }
51
+ if (image_name) {
52
+ data.items.add(
53
+ new File([new Uint8Array([1, 2, 3])], image_name, { type: "image/png" })
54
+ );
55
+ }
56
+ return new ClipboardEvent("paste", {
57
+ clipboardData: data,
58
+ bubbles: true,
59
+ cancelable: true
60
+ });
61
+ }
62
+
63
+ const uploaded_names = (upload: { mock: { calls: any[][] } }): string[] =>
64
+ upload.mock.calls.flat(2).map((file) => file.orig_name);
65
+
66
+ run_shared_prop_tests({
67
+ component: MultimodalTextbox,
68
+ name: "MultimodalTextbox",
69
+ base_props: default_props
70
+ });
71
+
20
72
  describe("MultimodalTextbox", () => {
21
73
  afterEach(() => cleanup());
22
74
 
23
- test("renders provided value", async () => {
24
- const { getByDisplayValue } = await render(MultimodalTextbox, {
25
- show_label: true,
26
- max_lines: 1,
27
- loading_status,
28
- lines: 1,
29
- value: { text: "hello world", files: [] },
30
- label: "Textbox",
31
- interactive: false,
32
- root: "",
33
- sources: []
75
+ test("renders the provided text and file thumbnails", async () => {
76
+ const { getByRole, getAllByLabelText } = await render(MultimodalTextbox, {
77
+ ...default_props,
78
+ value: { text: "hello world", files: [jpg_file, jpg_file] }
34
79
  });
35
80
 
36
- const item: HTMLInputElement = getByDisplayValue(
37
- "hello world"
38
- ) as HTMLInputElement;
39
- assert.equal(item.value, "hello world");
40
- });
41
-
42
- test.skip("changing the text should update the value", async () => {
43
- const { getByDisplayValue, listen } = await render(MultimodalTextbox, {
44
- show_label: true,
45
- max_lines: 10,
46
- loading_status,
47
- lines: 1,
48
- value: { text: "hi ", files: [] },
49
- label: "MultimodalTextbox",
50
- interactive: true,
51
- root: "",
52
- sources: []
53
- });
81
+ expect(getByRole("textbox")).toHaveValue("hello world");
82
+ expect(getAllByLabelText("File thumbnail")).toHaveLength(2);
83
+ });
54
84
 
55
- const item: HTMLInputElement = getByDisplayValue("hi") as HTMLInputElement;
85
+ test("placeholder is shown in the textbox", async () => {
86
+ const { getByPlaceholderText } = await render(MultimodalTextbox, {
87
+ ...default_props,
88
+ placeholder: "Type a message"
89
+ });
56
90
 
57
- const mock = listen("change");
91
+ expect(getByPlaceholderText("Type a message")).toBeVisible();
92
+ });
58
93
 
59
- item.focus();
60
- await event.keyboard("some text");
94
+ test("interactive: false disables the textbox and its buttons", async () => {
95
+ const { getByRole } = await render(MultimodalTextbox, {
96
+ ...default_props,
97
+ sources: ["upload", "microphone"],
98
+ interactive: false
99
+ });
61
100
 
62
- assert.equal(item.value, "hi some text");
63
- assert.equal(mock.callCount, 9);
64
- assert.equal(mock.calls[8][0].detail.data.text, "hi some text");
65
- assert.equal(mock.calls[8][0].detail.data.files.length, 0);
101
+ expect(getByRole("textbox")).toBeDisabled();
102
+ expect(getByRole("button", { name: "Upload a file" })).toBeDisabled();
103
+ expect(getByRole("button", { name: "Record audio" })).toBeDisabled();
104
+ expect(getByRole("button", { name: "Submit" })).toBeDisabled();
66
105
  });
67
106
 
68
- test.skip("submitting should clear mic_audio", async () => {
69
- const { getByTestId, listen } = await render(MultimodalTextbox, {
70
- show_label: true,
71
- max_lines: 10,
72
- loading_status,
73
- lines: 1,
74
- value: { text: "", files: [] },
75
- label: "MultimodalTextbox",
76
- interactive: true,
77
- root: "",
78
- sources: ["microphone"],
79
- submit_btn: true
107
+ test("typing updates get_data", async () => {
108
+ const { getByRole, get_data } = await render(MultimodalTextbox, {
109
+ ...default_props,
110
+ value: { text: "hi ", files: [] }
80
111
  });
81
112
 
82
- const mock = listen("submit");
83
- const submitButton = getByTestId("submit-button");
84
- await event.click(submitButton);
85
- assert.equal(mock.callCount, 1);
86
- });
87
-
88
- function paste_event(
89
- text: string | null,
90
- image_name?: string,
91
- with_html = true
92
- ): ClipboardEvent {
93
- const data = new DataTransfer();
94
- if (text) {
95
- data.setData("text/plain", text);
96
- if (with_html) {
97
- data.setData("text/html", `<table><tr><td>${text}</td></tr></table>`);
98
- }
99
- }
100
- if (image_name) {
101
- data.items.add(
102
- new File([new Uint8Array([1, 2, 3])], image_name, {
103
- type: "image/png"
104
- })
113
+ getByRole("textbox").focus();
114
+ await event.keyboard("there");
115
+
116
+ expect((await get_data()).value).toEqual({ text: "hi there", files: [] });
117
+ });
118
+ });
119
+
120
+ describe("Props: sources", () => {
121
+ afterEach(() => cleanup());
122
+
123
+ test.each([
124
+ [["upload"], true, false],
125
+ [["microphone"], false, true],
126
+ [["upload", "microphone"], true, true]
127
+ ])(
128
+ "sources=%j shows upload=%s, microphone=%s",
129
+ async (sources, has_upload, has_mic) => {
130
+ const { queryByRole } = await render(MultimodalTextbox, {
131
+ ...default_props,
132
+ sources
133
+ });
134
+
135
+ expect(queryByRole("button", { name: "Upload a file" }) !== null).toBe(
136
+ has_upload
137
+ );
138
+ expect(queryByRole("button", { name: "Record audio" }) !== null).toBe(
139
+ has_mic
105
140
  );
106
141
  }
107
- return new ClipboardEvent("paste", {
108
- clipboardData: data,
109
- bubbles: true,
110
- cancelable: true
142
+ );
143
+
144
+ test("file_count='single' hides the upload button once a file is attached", async () => {
145
+ const { queryByRole } = await render(MultimodalTextbox, {
146
+ ...default_props,
147
+ file_count: "single",
148
+ value: { text: "", files: [jpg_file] }
111
149
  });
112
- }
113
150
 
114
- test("a paste is handled as text or as an image, never both", async () => {
115
- const { getByTestId, listen } = await render(MultimodalTextbox, {
116
- show_label: true,
117
- max_lines: 1,
118
- loading_status,
119
- lines: 1,
120
- value: { text: "", files: [] },
121
- label: "MultimodalTextbox",
122
- interactive: true,
123
- root: "http://localhost:7860",
124
- sources: ["upload"],
125
- client: mock_client()
151
+ expect(
152
+ queryByRole("button", { name: "Upload a file" })
153
+ ).not.toBeInTheDocument();
154
+ });
155
+ });
156
+
157
+ describe("Props: submit_btn / stop_btn", () => {
158
+ afterEach(() => cleanup());
159
+
160
+ test("string values render as button text", async () => {
161
+ const { getByRole } = await render(MultimodalTextbox, {
162
+ ...default_props,
163
+ submit_btn: "Send",
164
+ stop_btn: "Halt"
126
165
  });
127
166
 
167
+ expect(getByRole("button", { name: "Submit" })).toHaveTextContent("Send");
168
+ expect(getByRole("button", { name: "Stop" })).toHaveTextContent("Halt");
169
+ });
170
+
171
+ test("falsy values render no buttons", async () => {
172
+ const { queryByRole } = await render(MultimodalTextbox, {
173
+ ...default_props,
174
+ submit_btn: false,
175
+ stop_btn: null
176
+ });
177
+
178
+ expect(queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
179
+ expect(queryByRole("button", { name: "Stop" })).not.toBeInTheDocument();
180
+ });
181
+ });
182
+
183
+ describe("Events", () => {
184
+ afterEach(() => cleanup());
185
+
186
+ test("typing emits change, but mounting does not", async () => {
187
+ const { getByRole, listen } = await render(MultimodalTextbox, {
188
+ ...default_props,
189
+ value: { text: "hi", files: [] }
190
+ });
191
+ const change = listen("change", { retrospective: true });
192
+
193
+ expect(change).not.toHaveBeenCalled();
194
+
195
+ getByRole("textbox").focus();
196
+ await event.keyboard("!");
197
+
198
+ expect(change).toHaveBeenLastCalledWith({ text: "hi!", files: [] });
199
+ });
200
+
201
+ test("Enter submits a single-line textbox, Shift+Enter does not", async () => {
202
+ const { getByRole, listen } = await render(
203
+ MultimodalTextbox,
204
+ default_props
205
+ );
206
+ const submit = listen("submit");
207
+
208
+ getByRole("textbox").focus();
209
+ await event.keyboard("{Shift>}{Enter}{/Shift}");
210
+ expect(submit).not.toHaveBeenCalled();
211
+
212
+ await event.keyboard("{Enter}");
213
+ await waitFor(() => expect(submit).toHaveBeenCalledTimes(1));
214
+ });
215
+
216
+ test("Shift+Enter submits a multi-line textbox", async () => {
217
+ const { getByRole, listen } = await render(MultimodalTextbox, {
218
+ ...default_props,
219
+ lines: 3,
220
+ max_lines: 5
221
+ });
222
+ const submit = listen("submit");
223
+
224
+ getByRole("textbox").focus();
225
+ await event.keyboard("{Shift>}{Enter}{/Shift}");
226
+
227
+ await waitFor(() => expect(submit).toHaveBeenCalledTimes(1));
228
+ });
229
+
230
+ test("submit and stop buttons emit submit and stop", async () => {
231
+ const { getByRole, listen } = await render(MultimodalTextbox, {
232
+ ...default_props,
233
+ stop_btn: true
234
+ });
235
+ const submit = listen("submit");
236
+ const stop = listen("stop");
237
+
238
+ await fireEvent.click(getByRole("button", { name: "Submit" }));
239
+ await fireEvent.click(getByRole("button", { name: "Stop" }));
240
+
241
+ expect(submit).toHaveBeenCalledTimes(1);
242
+ expect(stop).toHaveBeenCalledTimes(1);
243
+ });
244
+
245
+ test("focus and blur are emitted", async () => {
246
+ const { getByRole, listen } = await render(
247
+ MultimodalTextbox,
248
+ default_props
249
+ );
250
+ const focus = listen("focus");
251
+ const blur = listen("blur");
252
+
253
+ await fireEvent.focus(getByRole("textbox"));
254
+ await fireEvent.blur(getByRole("textbox"));
255
+
256
+ expect(focus).toHaveBeenCalledTimes(1);
257
+ expect(blur).toHaveBeenCalledTimes(1);
258
+ });
259
+
260
+ test("select emits the selected text and its range", async () => {
261
+ const { getByRole, listen } = await render(MultimodalTextbox, {
262
+ ...default_props,
263
+ value: { text: "hello world", files: [] }
264
+ });
265
+ const select = listen("select");
266
+ const textbox = getByRole("textbox") as HTMLTextAreaElement;
267
+
268
+ textbox.setSelectionRange(6, 11);
269
+ await fireEvent.select(textbox);
270
+
271
+ expect(select).toHaveBeenCalledWith({ value: "world", index: [6, 11] });
272
+ });
273
+ });
274
+
275
+ describe("Files", () => {
276
+ afterEach(() => cleanup());
277
+
278
+ test("uploading a file emits upload and adds it to the value", async () => {
279
+ const { listen, get_data, getAllByLabelText } = await render(
280
+ MultimodalTextbox,
281
+ default_props
282
+ );
283
+ const upload = listen("upload");
284
+
285
+ await upload_file(TEST_JPG);
286
+
287
+ await waitFor(() => expect(upload).toHaveBeenCalledTimes(1));
288
+ expect(getAllByLabelText("File thumbnail")).toHaveLength(1);
289
+ const { value } = await get_data();
290
+ expect(value.files.map((f: any) => f.orig_name)).toEqual([
291
+ TEST_JPG.orig_name
292
+ ]);
293
+ });
294
+
295
+ test("removing a thumbnail removes the file from the value", async () => {
296
+ const { getAllByRole, get_data, queryByLabelText } = await render(
297
+ MultimodalTextbox,
298
+ { ...default_props, value: { text: "", files: [jpg_file, jpg_file] } }
299
+ );
300
+
301
+ await fireEvent.click(getAllByRole("button", { name: "Remove file" })[0]);
302
+
303
+ expect((await get_data()).value.files).toHaveLength(1);
304
+ await fireEvent.click(getAllByRole("button", { name: "Remove file" })[0]);
305
+ expect(queryByLabelText("File thumbnail")).not.toBeInTheDocument();
306
+ });
307
+
308
+ test("dropping a file outside file_types emits an error and uploads nothing", async () => {
309
+ const { listen } = await render(MultimodalTextbox, {
310
+ ...default_props,
311
+ file_types: [".jpg"]
312
+ });
313
+ const error = listen("error");
314
+ const upload = listen("upload");
315
+
316
+ await drop_file(TEST_TXT, "[aria-label='Multimedia input field']");
317
+
318
+ await waitFor(() =>
319
+ expect(error).toHaveBeenCalledWith(
320
+ "1 file(s) were rejected. Accepted formats: .jpg"
321
+ )
322
+ );
323
+ expect(upload).not.toHaveBeenCalled();
324
+ });
325
+ });
326
+
327
+ describe("Paste", () => {
328
+ afterEach(() => cleanup());
329
+
330
+ test("text longer than max_plain_text_length is uploaded as a file", async () => {
331
+ const { getByRole, listen } = await render(MultimodalTextbox, {
332
+ ...default_props,
333
+ max_plain_text_length: 10
334
+ });
335
+ const upload = listen("upload");
336
+ const paste = paste_event("x".repeat(20), undefined, false);
337
+
338
+ getByRole("textbox").dispatchEvent(paste);
339
+
340
+ await waitFor(() =>
341
+ expect(uploaded_names(upload)).toEqual(["pasted_text.txt"])
342
+ );
343
+ expect(paste.defaultPrevented).toBe(true);
344
+ });
345
+
346
+ test("a paste is handled as text or as an image, never both", async () => {
347
+ const { getByRole, listen } = await render(
348
+ MultimodalTextbox,
349
+ default_props
350
+ );
128
351
  const upload = listen("upload");
129
- const textbox = getByTestId("textbox");
352
+ const textbox = getByRole("textbox");
130
353
 
131
354
  const excel_paste = paste_event("1\t2\n3\t4", "cells.png");
132
355
  textbox.dispatchEvent(excel_paste);
133
- // an image alone is still attached. Waiting for that upload also bounds
134
- // the assertion that the paste above uploaded nothing.
356
+ // an image alone is still attached; waiting for that upload also bounds
357
+ // the assertion that the paste above uploaded nothing
135
358
  const image_paste = paste_event(null, "screenshot.png");
136
359
  textbox.dispatchEvent(image_paste);
137
360
 
138
- const uploaded = (): string[] =>
139
- upload.mock.calls.flat(2).map((file) => file.orig_name);
140
-
141
- // wait for the screenshot itself, so a regression that uploads
142
- // cells.png first cannot satisfy the wait and slip past
143
- await vi.waitFor(() => expect(uploaded()).toContain("screenshot.png"));
361
+ await waitFor(() =>
362
+ expect(uploaded_names(upload)).toContain("screenshot.png")
363
+ );
144
364
  await tick();
145
365
 
146
- assert.deepEqual(uploaded(), ["screenshot.png"]);
147
- assert.isFalse(excel_paste.defaultPrevented);
148
- assert.isTrue(image_paste.defaultPrevented);
366
+ expect(uploaded_names(upload)).toEqual(["screenshot.png"]);
367
+ expect(excel_paste.defaultPrevented).toBe(false);
368
+ expect(image_paste.defaultPrevented).toBe(true);
369
+ });
370
+
371
+ test("text with no HTML flavor leaves the image alone", async () => {
372
+ const { getByRole, listen } = await render(
373
+ MultimodalTextbox,
374
+ default_props
375
+ );
376
+ const upload = listen("upload");
377
+
378
+ // a file manager copying an image can put its path in text/plain with
379
+ // no text/html, and there the image is the point of the paste
380
+ getByRole("textbox").dispatchEvent(
381
+ paste_event("file:///cats.jpg", "cats.jpg", false)
382
+ );
383
+
384
+ await waitFor(() => expect(uploaded_names(upload)).toEqual(["cats.jpg"]));
149
385
  });
150
386
 
151
387
  test("with nowhere to upload, only a text paste falls back to the browser", async () => {
152
- const { getByTestId, listen } = await render(MultimodalTextbox, {
153
- show_label: true,
154
- max_lines: 1,
155
- loading_status,
156
- lines: 1,
388
+ const { getByRole, listen } = await render(MultimodalTextbox, {
389
+ ...default_props,
157
390
  // file_count "single" with a file already attached unmounts Upload
158
391
  file_count: "single",
159
- value: {
160
- text: "",
161
- files: [
162
- {
163
- path: "cats.jpg",
164
- orig_name: "cats.jpg",
165
- mime_type: "image/jpeg",
166
- meta: { _type: "gradio.FileData" }
167
- }
168
- ]
169
- },
170
- label: "MultimodalTextbox",
171
- interactive: true,
172
- root: "http://localhost:7860",
173
- max_plain_text_length: 1000,
174
- sources: ["upload"],
175
- client: mock_client()
392
+ value: { text: "", files: [jpg_file] },
393
+ max_plain_text_length: 1000
176
394
  });
177
-
178
395
  const upload = listen("upload");
179
- const textbox = getByTestId("textbox");
396
+ const textbox = getByRole("textbox");
180
397
 
181
398
  const image_paste = paste_event(null, "screenshot.png");
182
399
  textbox.dispatchEvent(image_paste);
@@ -190,38 +407,37 @@ describe("MultimodalTextbox", () => {
190
407
 
191
408
  expect(upload).not.toHaveBeenCalled();
192
409
  // the browser would paste the image's file name, which is worth nothing
193
- assert.isTrue(image_paste.defaultPrevented);
410
+ expect(image_paste.defaultPrevented).toBe(true);
194
411
  // text is its own fallback, so it must not be swallowed either way
195
- assert.isFalse(long_paste.defaultPrevented);
196
- assert.isFalse(path_paste.defaultPrevented);
412
+ expect(long_paste.defaultPrevented).toBe(false);
413
+ expect(path_paste.defaultPrevented).toBe(false);
197
414
  });
415
+ });
198
416
 
199
- test("text with no HTML flavor leaves the image alone", async () => {
200
- const { getByTestId, listen } = await render(MultimodalTextbox, {
201
- show_label: true,
202
- max_lines: 1,
203
- loading_status,
204
- lines: 1,
205
- value: { text: "", files: [] },
206
- label: "MultimodalTextbox",
207
- interactive: true,
208
- root: "http://localhost:7860",
209
- sources: ["upload"],
210
- client: mock_client()
211
- });
417
+ describe("get_data / set_data", () => {
418
+ afterEach(() => cleanup());
212
419
 
213
- const upload = listen("upload");
214
- // a file manager copying an image can put its path in text/plain with
215
- // no text/html, and there the image is the point of the paste
216
- getByTestId("textbox").dispatchEvent(
217
- paste_event("file:///cats.jpg", "cats.jpg", false)
218
- );
420
+ test("set_data updates the DOM, emits change, and round-trips", async () => {
421
+ const { getByRole, getAllByLabelText, set_data, get_data, listen } =
422
+ await render(MultimodalTextbox, default_props);
423
+ const change = listen("change");
424
+ const value = { text: "from backend", files: [jpg_file] };
219
425
 
220
- await vi.waitFor(() => expect(upload).toHaveBeenCalled());
426
+ await set_data({ value });
221
427
 
222
- assert.deepEqual(
223
- upload.mock.calls.flat(2).map((file) => file.orig_name),
224
- ["cats.jpg"]
225
- );
428
+ expect(getByRole("textbox")).toHaveValue("from backend");
429
+ expect(getAllByLabelText("File thumbnail")).toHaveLength(1);
430
+ expect(change).toHaveBeenCalledTimes(1);
431
+ expect((await get_data()).value).toEqual(value);
432
+ });
433
+
434
+ test("a null value falls back to empty text and no files", async () => {
435
+ const { getByRole, get_data } = await render(MultimodalTextbox, {
436
+ ...default_props,
437
+ value: null
438
+ });
439
+
440
+ expect(getByRole("textbox")).toHaveValue("");
441
+ expect((await get_data()).value).toEqual({ text: "", files: [] });
226
442
  });
227
443
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/multimodaltextbox",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -22,18 +22,18 @@
22
22
  "./package.json": "./package.json"
23
23
  },
24
24
  "dependencies": {
25
- "@gradio/icons": "^0.16.0",
26
25
  "@gradio/atoms": "^0.26.2",
27
- "@gradio/statustracker": "^0.15.3",
28
- "@gradio/utils": "^0.14.1",
29
- "@gradio/image": "^0.28.3",
30
- "@gradio/upload": "^0.18.3",
31
- "@gradio/video": "^0.23.1",
32
- "@gradio/client": "^2.6.0",
33
- "@gradio/audio": "^0.24.3"
26
+ "@gradio/statustracker": "^0.15.4",
27
+ "@gradio/icons": "^0.16.0",
28
+ "@gradio/utils": "^0.14.2",
29
+ "@gradio/image": "^0.28.4",
30
+ "@gradio/upload": "^0.19.0",
31
+ "@gradio/video": "^0.23.2",
32
+ "@gradio/client": "^2.7.1",
33
+ "@gradio/audio": "^0.24.4"
34
34
  },
35
35
  "devDependencies": {
36
- "@gradio/preview": "^0.17.0"
36
+ "@gradio/preview": "^0.17.1"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "svelte": "^5.48.0"