diff --git a/.changeset/fair-bees-report.md b/.changeset/fair-bees-report.md new file mode 100644 index 0000000000..4aba80ddbc --- /dev/null +++ b/.changeset/fair-bees-report.md @@ -0,0 +1,6 @@ +--- +"@gradio/multimodaltextbox": minor +"gradio": minor +--- + +feat:Improve pasted text behaviour in `Multimodaltextbox` diff --git a/gradio/components/multimodal_textbox.py b/gradio/components/multimodal_textbox.py index cea12d2b5a..3eac76cd17 100644 --- a/gradio/components/multimodal_textbox.py +++ b/gradio/components/multimodal_textbox.py @@ -86,6 +86,7 @@ class MultimodalTextbox(FormComponent): rtl: bool = False, submit_btn: str | bool | None = True, stop_btn: str | bool | None = False, + max_plain_text_length: int = 1000, ): """ Parameters: @@ -115,6 +116,7 @@ class MultimodalTextbox(FormComponent): autoscroll: If True, will automatically scroll to the bottom of the textbox when the value changes, unless the user scrolls up. If False, will not scroll to the bottom of the textbox when the value changes. submit_btn: If False, will not show a submit button. If a string, will use that string as the submit button text. stop_btn: If True, will show a stop button (useful for streaming demos). If a string, will use that string as the stop button text. + max_plain_text_length: Maximum length of plain text in the textbox. If the text exceeds this length, the text will be pasted as a file. Default is 1000. """ self.file_types = file_types self.file_count = file_count @@ -129,6 +131,7 @@ class MultimodalTextbox(FormComponent): self.stop_btn = stop_btn self.autofocus = autofocus self.autoscroll = autoscroll + self.max_plain_text_length = max_plain_text_length super().__init__( label=label, diff --git a/js/multimodaltextbox/Index.svelte b/js/multimodaltextbox/Index.svelte index 9afeed8143..b7aadede1d 100644 --- a/js/multimodaltextbox/Index.svelte +++ b/js/multimodaltextbox/Index.svelte @@ -52,6 +52,7 @@ export let interactive: boolean; export let root: string; export let file_count: "single" | "multiple" | "directory"; + export let max_plain_text_length: number; let dragging: boolean; @@ -109,5 +110,6 @@ disabled={!interactive} upload={(...args) => gradio.client.upload(...args)} stream_handler={(...args) => gradio.client.stream(...args)} + {max_plain_text_length} /> diff --git a/js/multimodaltextbox/shared/MultimodalTextbox.svelte b/js/multimodaltextbox/shared/MultimodalTextbox.svelte index 6a10fc5edb..13a05d29f7 100644 --- a/js/multimodaltextbox/shared/MultimodalTextbox.svelte +++ b/js/multimodaltextbox/shared/MultimodalTextbox.svelte @@ -47,6 +47,7 @@ export let upload: Client["upload"]; export let stream_handler: Client["stream"]; export let file_count: "single" | "multiple" | "directory" = "multiple"; + export let max_plain_text_length = 1000; let upload_component: Upload; let hidden_upload: HTMLInputElement; @@ -194,9 +195,23 @@ dispatch("submit"); } - function handle_paste(event: ClipboardEvent): void { + async function handle_paste(event: ClipboardEvent): Promise { if (!event.clipboardData) return; const items = event.clipboardData.items; + const text = event.clipboardData.getData("text"); + + if (text && text.length > max_plain_text_length) { + event.preventDefault(); + const file = new window.File([text], "pasted_text.txt", { + type: "text/plain", + lastModified: Date.now() + }); + if (upload_component) { + upload_component.load_files([file]); + } + return; + } + for (let index in items) { const item = items[index]; if (item.kind === "file" && item.type.includes("image")) { diff --git a/js/spa/test/chatbot_multimodal.spec.ts b/js/spa/test/chatbot_multimodal.spec.ts index 4d440664f7..bbb8ff4736 100644 --- a/js/spa/test/chatbot_multimodal.spec.ts +++ b/js/spa/test/chatbot_multimodal.spec.ts @@ -266,4 +266,33 @@ for (const msg_format of ["tuples", "messages"]) { await expect(page.locator(".thumbnail-image")).toHaveCount(2); }); + + test(`message format ${msg_format} - pasting large text should create a file upload`, async ({ + page + }) => { + if (msg_format === "tuples") { + await go_to_testcase(page, "tuples"); + } + const textbox = await page.getByTestId("textbox"); + const largeText = "x".repeat(2000); + + await textbox.focus(); + await page.evaluate((text) => { + const dataTransfer = new DataTransfer(); + const clipboardData = new ClipboardEvent("paste", { + clipboardData: dataTransfer, + bubbles: true, + cancelable: true + }); + dataTransfer.setData("text/plain", text); + document.activeElement?.dispatchEvent(clipboardData); + }, largeText); + + await expect(page.locator(".thumbnail-item")).toBeVisible(); + const fileIcon = await page.locator(".thumbnail-item").first(); + await expect(fileIcon).toBeVisible(); + + const textboxValue = await textbox.inputValue(); + await expect(textboxValue).toBe(""); + }); }