@gradio/multimodaltextbox 0.14.1 → 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,51 @@
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
+
21
+ ## 0.14.2
22
+
23
+ ### Fixes
24
+
25
+ - [#13802](https://github.com/gradio-app/gradio/pull/13802) [`d857094`](https://github.com/gradio-app/gradio/commit/d857094868c85167dd332b6093a3914dee960840) - Handle a paste in `gr.MultimodalTextbox` as text or as an image, not both. Thanks @hysts!
26
+
27
+ ### Dependency updates
28
+
29
+ - @gradio/client@2.6.0
30
+ - @gradio/atoms@0.26.2
31
+ - @gradio/utils@0.14.1
32
+ - @gradio/upload@0.18.3
33
+ - @gradio/image@0.28.3
34
+ - @gradio/audio@0.24.3
35
+ - @gradio/video@0.23.1
36
+
37
+ ## 0.14.1
38
+
39
+ ### Dependency updates
40
+
41
+ - @gradio/client@2.5.1
42
+
43
+ ## 0.14.1
44
+
45
+ ### Dependency updates
46
+
47
+ - @gradio/client@2.5.0
48
+
3
49
  ## 0.14.1
4
50
 
5
51
  ### Dependency updates
package/Index.svelte CHANGED
@@ -1,6 +1,4 @@
1
- <svelte:options accessors={true} />
2
-
3
- <script context="module" lang="ts">
1
+ <script module lang="ts">
4
2
  export { default as BaseMultimodalTextbox } from "./shared/MultimodalTextbox.svelte";
5
3
  export { default as BaseExample } from "./Example.svelte";
6
4
  </script>
@@ -1,86 +1,443 @@
1
- import { test, describe, assert, afterEach } from "vitest";
2
- import { cleanup, render } 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";
15
+ import { tick } from "svelte";
4
16
 
5
17
  import MultimodalTextbox from "./Index.svelte";
6
- import type { ILoadingStatus as LoadingStatus } from "@gradio/statustracker";
7
-
8
- const loading_status: LoadingStatus = {
9
- eta: 0,
10
- queue_position: 1,
11
- queue_size: 1,
12
- status: "complete" as LoadingStatus["status"],
13
- scroll_to_output: false,
14
- visible: true,
15
- fn_index: 0,
16
- 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()
17
37
  };
18
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
+
19
72
  describe("MultimodalTextbox", () => {
20
73
  afterEach(() => cleanup());
21
74
 
22
- test("renders provided value", async () => {
23
- const { getByDisplayValue } = await render(MultimodalTextbox, {
24
- show_label: true,
25
- max_lines: 1,
26
- loading_status,
27
- lines: 1,
28
- value: { text: "hello world", files: [] },
29
- label: "Textbox",
30
- interactive: false,
31
- root: "",
32
- 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] }
79
+ });
80
+
81
+ expect(getByRole("textbox")).toHaveValue("hello world");
82
+ expect(getAllByLabelText("File thumbnail")).toHaveLength(2);
83
+ });
84
+
85
+ test("placeholder is shown in the textbox", async () => {
86
+ const { getByPlaceholderText } = await render(MultimodalTextbox, {
87
+ ...default_props,
88
+ placeholder: "Type a message"
89
+ });
90
+
91
+ expect(getByPlaceholderText("Type a message")).toBeVisible();
92
+ });
93
+
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
+ });
100
+
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();
105
+ });
106
+
107
+ test("typing updates get_data", async () => {
108
+ const { getByRole, get_data } = await render(MultimodalTextbox, {
109
+ ...default_props,
110
+ value: { text: "hi ", files: [] }
111
+ });
112
+
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
140
+ );
141
+ }
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] }
149
+ });
150
+
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"
165
+ });
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
33
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
+ );
351
+ const upload = listen("upload");
352
+ const textbox = getByRole("textbox");
34
353
 
35
- const item: HTMLInputElement = getByDisplayValue(
36
- "hello world"
37
- ) as HTMLInputElement;
38
- assert.equal(item.value, "hello world");
39
- });
40
-
41
- test.skip("changing the text should update the value", async () => {
42
- const { getByDisplayValue, listen } = await render(MultimodalTextbox, {
43
- show_label: true,
44
- max_lines: 10,
45
- loading_status,
46
- lines: 1,
47
- value: { text: "hi ", files: [] },
48
- label: "MultimodalTextbox",
49
- interactive: true,
50
- root: "",
51
- sources: []
354
+ const excel_paste = paste_event("1\t2\n3\t4", "cells.png");
355
+ textbox.dispatchEvent(excel_paste);
356
+ // an image alone is still attached; waiting for that upload also bounds
357
+ // the assertion that the paste above uploaded nothing
358
+ const image_paste = paste_event(null, "screenshot.png");
359
+ textbox.dispatchEvent(image_paste);
360
+
361
+ await waitFor(() =>
362
+ expect(uploaded_names(upload)).toContain("screenshot.png")
363
+ );
364
+ await tick();
365
+
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"]));
385
+ });
386
+
387
+ test("with nowhere to upload, only a text paste falls back to the browser", async () => {
388
+ const { getByRole, listen } = await render(MultimodalTextbox, {
389
+ ...default_props,
390
+ // file_count "single" with a file already attached unmounts Upload
391
+ file_count: "single",
392
+ value: { text: "", files: [jpg_file] },
393
+ max_plain_text_length: 1000
52
394
  });
395
+ const upload = listen("upload");
396
+ const textbox = getByRole("textbox");
397
+
398
+ const image_paste = paste_event(null, "screenshot.png");
399
+ textbox.dispatchEvent(image_paste);
400
+ const long_paste = paste_event("x".repeat(2000));
401
+ textbox.dispatchEvent(long_paste);
402
+ const path_paste = paste_event("file:///cats.jpg", "cats.jpg", false);
403
+ textbox.dispatchEvent(path_paste);
53
404
 
54
- const item: HTMLInputElement = getByDisplayValue("hi") as HTMLInputElement;
405
+ await tick();
406
+ await tick();
407
+
408
+ expect(upload).not.toHaveBeenCalled();
409
+ // the browser would paste the image's file name, which is worth nothing
410
+ expect(image_paste.defaultPrevented).toBe(true);
411
+ // text is its own fallback, so it must not be swallowed either way
412
+ expect(long_paste.defaultPrevented).toBe(false);
413
+ expect(path_paste.defaultPrevented).toBe(false);
414
+ });
415
+ });
416
+
417
+ describe("get_data / set_data", () => {
418
+ afterEach(() => cleanup());
55
419
 
56
- const mock = listen("change");
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] };
57
425
 
58
- item.focus();
59
- await event.keyboard("some text");
426
+ await set_data({ value });
60
427
 
61
- assert.equal(item.value, "hi some text");
62
- assert.equal(mock.callCount, 9);
63
- assert.equal(mock.calls[8][0].detail.data.text, "hi some text");
64
- assert.equal(mock.calls[8][0].detail.data.files.length, 0);
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);
65
432
  });
66
433
 
67
- test.skip("submitting should clear mic_audio", async () => {
68
- const { getByTestId, listen } = await render(MultimodalTextbox, {
69
- show_label: true,
70
- max_lines: 10,
71
- loading_status,
72
- lines: 1,
73
- value: { text: "", files: [] },
74
- label: "MultimodalTextbox",
75
- interactive: true,
76
- root: "",
77
- sources: ["microphone"],
78
- submit_btn: true
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
79
438
  });
80
439
 
81
- const mock = listen("submit");
82
- const submitButton = getByTestId("submit-button");
83
- await event.click(submitButton);
84
- assert.equal(mock.callCount, 1);
440
+ expect(getByRole("textbox")).toHaveValue("");
441
+ expect((await get_data()).value).toEqual({ text: "", files: [] });
85
442
  });
86
443
  });
package/dist/Index.svelte CHANGED
@@ -1,6 +1,4 @@
1
- <svelte:options accessors={true} />
2
-
3
- <script context="module" lang="ts">
1
+ <script module lang="ts">
4
2
  export { default as BaseMultimodalTextbox } from "./shared/MultimodalTextbox.svelte";
5
3
  export { default as BaseExample } from "./Example.svelte";
6
4
  </script>
@@ -312,23 +312,31 @@
312
312
  const items = event.clipboardData.items;
313
313
  const text = event.clipboardData.getData("text");
314
314
 
315
- if (text && text.length > max_plain_text_length) {
315
+ if (text && text.length > max_plain_text_length && upload_component) {
316
316
  event.preventDefault();
317
317
  const file = new window.File([text], "pasted_text.txt", {
318
318
  type: "text/plain",
319
319
  lastModified: Date.now()
320
320
  });
321
- if (upload_component) {
322
- upload_component.load_files([file]);
323
- }
321
+ upload_component.load_files([file]);
324
322
  return;
325
323
  }
326
324
 
325
+ // text plus HTML means the image is a rendering of that content. Text
326
+ // alone may be an image's own file path on platforms we have not
327
+ // measured, so leave those pastes to the image branch (#10910)
328
+ if (text && event.clipboardData.types.includes("text/html")) return;
329
+
327
330
  for (let index in items) {
328
331
  const item = items[index];
329
332
  if (item.kind === "file" && item.type.includes("image")) {
330
333
  const blob = item.getAsFile();
331
- if (blob) upload_component.load_files([blob]);
334
+ if (blob) {
335
+ // claim the paste so the browser cannot insert the file name,
336
+ // unless that would leave text with nothing to replace it
337
+ if (upload_component || !text) event.preventDefault();
338
+ upload_component?.load_files([blob]);
339
+ }
332
340
  }
333
341
  }
334
342
  }
@@ -376,10 +384,10 @@
376
384
  }
377
385
 
378
386
  if (valid_files.length > 0) {
379
- upload_component.load_files(valid_files);
387
+ upload_component?.load_files(valid_files);
380
388
  }
381
389
  } else {
382
- upload_component.load_files(files);
390
+ upload_component?.load_files(files);
383
391
  }
384
392
  }
385
393
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/multimodaltextbox",
3
- "version": "0.14.1",
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/statustracker": "^0.15.2",
26
- "@gradio/atoms": "^0.26.1",
27
- "@gradio/upload": "^0.18.2",
25
+ "@gradio/atoms": "^0.26.2",
26
+ "@gradio/statustracker": "^0.15.4",
28
27
  "@gradio/icons": "^0.16.0",
29
- "@gradio/utils": "^0.14.0",
30
- "@gradio/video": "^0.23.0",
31
- "@gradio/image": "^0.28.1",
32
- "@gradio/audio": "^0.24.2",
33
- "@gradio/client": "^2.4.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"
@@ -312,23 +312,31 @@
312
312
  const items = event.clipboardData.items;
313
313
  const text = event.clipboardData.getData("text");
314
314
 
315
- if (text && text.length > max_plain_text_length) {
315
+ if (text && text.length > max_plain_text_length && upload_component) {
316
316
  event.preventDefault();
317
317
  const file = new window.File([text], "pasted_text.txt", {
318
318
  type: "text/plain",
319
319
  lastModified: Date.now()
320
320
  });
321
- if (upload_component) {
322
- upload_component.load_files([file]);
323
- }
321
+ upload_component.load_files([file]);
324
322
  return;
325
323
  }
326
324
 
325
+ // text plus HTML means the image is a rendering of that content. Text
326
+ // alone may be an image's own file path on platforms we have not
327
+ // measured, so leave those pastes to the image branch (#10910)
328
+ if (text && event.clipboardData.types.includes("text/html")) return;
329
+
327
330
  for (let index in items) {
328
331
  const item = items[index];
329
332
  if (item.kind === "file" && item.type.includes("image")) {
330
333
  const blob = item.getAsFile();
331
- if (blob) upload_component.load_files([blob]);
334
+ if (blob) {
335
+ // claim the paste so the browser cannot insert the file name,
336
+ // unless that would leave text with nothing to replace it
337
+ if (upload_component || !text) event.preventDefault();
338
+ upload_component?.load_files([blob]);
339
+ }
332
340
  }
333
341
  }
334
342
  }
@@ -376,10 +384,10 @@
376
384
  }
377
385
 
378
386
  if (valid_files.length > 0) {
379
- upload_component.load_files(valid_files);
387
+ upload_component?.load_files(valid_files);
380
388
  }
381
389
  } else {
382
- upload_component.load_files(files);
390
+ upload_component?.load_files(files);
383
391
  }
384
392
  }
385
393
  }