@gradio/multimodaltextbox 0.14.1 → 0.14.2

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,33 @@
1
1
  # @gradio/multimodaltextbox
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Fixes
6
+
7
+ - [#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!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/client@2.6.0
12
+ - @gradio/atoms@0.26.2
13
+ - @gradio/utils@0.14.1
14
+ - @gradio/upload@0.18.3
15
+ - @gradio/image@0.28.3
16
+ - @gradio/audio@0.24.3
17
+ - @gradio/video@0.23.1
18
+
19
+ ## 0.14.1
20
+
21
+ ### Dependency updates
22
+
23
+ - @gradio/client@2.5.1
24
+
25
+ ## 0.14.1
26
+
27
+ ### Dependency updates
28
+
29
+ - @gradio/client@2.5.0
30
+
3
31
  ## 0.14.1
4
32
 
5
33
  ### 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,6 +1,7 @@
1
- import { test, describe, assert, afterEach } from "vitest";
2
- import { cleanup, render } from "@self/tootils/render";
1
+ import { test, describe, assert, afterEach, expect, vi } from "vitest";
2
+ import { cleanup, render, mock_client } from "@self/tootils/render";
3
3
  import event from "@testing-library/user-event";
4
+ import { tick } from "svelte";
4
5
 
5
6
  import MultimodalTextbox from "./Index.svelte";
6
7
  import type { ILoadingStatus as LoadingStatus } from "@gradio/statustracker";
@@ -83,4 +84,144 @@ describe("MultimodalTextbox", () => {
83
84
  await event.click(submitButton);
84
85
  assert.equal(mock.callCount, 1);
85
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
+ })
105
+ );
106
+ }
107
+ return new ClipboardEvent("paste", {
108
+ clipboardData: data,
109
+ bubbles: true,
110
+ cancelable: true
111
+ });
112
+ }
113
+
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()
126
+ });
127
+
128
+ const upload = listen("upload");
129
+ const textbox = getByTestId("textbox");
130
+
131
+ const excel_paste = paste_event("1\t2\n3\t4", "cells.png");
132
+ 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.
135
+ const image_paste = paste_event(null, "screenshot.png");
136
+ textbox.dispatchEvent(image_paste);
137
+
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"));
144
+ await tick();
145
+
146
+ assert.deepEqual(uploaded(), ["screenshot.png"]);
147
+ assert.isFalse(excel_paste.defaultPrevented);
148
+ assert.isTrue(image_paste.defaultPrevented);
149
+ });
150
+
151
+ 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,
157
+ // file_count "single" with a file already attached unmounts Upload
158
+ 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()
176
+ });
177
+
178
+ const upload = listen("upload");
179
+ const textbox = getByTestId("textbox");
180
+
181
+ const image_paste = paste_event(null, "screenshot.png");
182
+ textbox.dispatchEvent(image_paste);
183
+ const long_paste = paste_event("x".repeat(2000));
184
+ textbox.dispatchEvent(long_paste);
185
+ const path_paste = paste_event("file:///cats.jpg", "cats.jpg", false);
186
+ textbox.dispatchEvent(path_paste);
187
+
188
+ await tick();
189
+ await tick();
190
+
191
+ expect(upload).not.toHaveBeenCalled();
192
+ // the browser would paste the image's file name, which is worth nothing
193
+ assert.isTrue(image_paste.defaultPrevented);
194
+ // 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);
197
+ });
198
+
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
+ });
212
+
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
+ );
219
+
220
+ await vi.waitFor(() => expect(upload).toHaveBeenCalled());
221
+
222
+ assert.deepEqual(
223
+ upload.mock.calls.flat(2).map((file) => file.orig_name),
224
+ ["cats.jpg"]
225
+ );
226
+ });
86
227
  });
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.2",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -22,15 +22,15 @@
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",
28
25
  "@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"
26
+ "@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"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@gradio/preview": "^0.17.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
  }