Adds copy event to gr.Markdown, gr.Chatbot, and gr.Textbox (#9979)

* add copy event

* add changeset

* test

* demo

* changes

* add changeset

* add list format

* typing

* notebook

* copy events'

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Abubakar Abid 2024-11-19 11:58:19 -08:00 committed by GitHub
parent 2afcad80ab
commit e7629f7eac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 118 additions and 13 deletions

View File

@ -0,0 +1,9 @@
---
"@gradio/chatbot": minor
"@gradio/markdown": minor
"@gradio/textbox": minor
"@gradio/utils": minor
"gradio": minor
---
feat:Adds copy event to `gr.Markdown`, `gr.Chatbot`, and `gr.Textbox`

View File

@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: copy_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "md = \"This is **bold** text.\"\n", "\n", "def copy_callback(copy_data: gr.CopyData):\n", " return copy_data.value\n", "\n", "with gr.Blocks() as demo:\n", " textbox = gr.Textbox(label=\"Copied text\")\n", " with gr.Row():\n", " markdown = gr.Markdown(value=md, header_links=True, height=400, show_copy_button=True)\n", " chatbot = gr.Chatbot([(\"Hello\", \"World\"), (\"Goodbye\", \"World\")], show_copy_button=True)\n", " textbox2 = gr.Textbox(\"Write something here\", interactive=True, show_copy_button=True)\n", "\n", " gr.on(\n", " [markdown.copy, chatbot.copy, textbox2.copy],\n", " copy_callback,\n", " outputs=textbox\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

22
demo/copy_events/run.py Normal file
View File

@ -0,0 +1,22 @@
import gradio as gr
md = "This is **bold** text."
def copy_callback(copy_data: gr.CopyData):
return copy_data.value
with gr.Blocks() as demo:
textbox = gr.Textbox(label="Copied text")
with gr.Row():
markdown = gr.Markdown(value=md, header_links=True, height=400, show_copy_button=True)
chatbot = gr.Chatbot([("Hello", "World"), ("Goodbye", "World")], show_copy_button=True)
textbox2 = gr.Textbox("Write something here", interactive=True, show_copy_button=True)
gr.on(
[markdown.copy, chatbot.copy, textbox2.copy],
copy_callback,
outputs=textbox
)
if __name__ == "__main__":
demo.launch()

View File

@ -65,6 +65,7 @@ from gradio.components.audio import WaveformOptions
from gradio.components.image_editor import Brush, Eraser
from gradio.data_classes import FileData
from gradio.events import (
CopyData,
DeletedFileData,
DownloadData,
EventData,

View File

@ -749,7 +749,7 @@ class ChatInterface(Blocks):
response = await anyio.to_thread.run_sync(
self.fn, *inputs, limiter=self.limiter
)
return self._process_example(message, response)
return self._process_example(message, response) # type: ignore
async def _examples_stream_fn(
self,

View File

@ -108,7 +108,10 @@ class ChatbotDataMessages(GradioRootModel):
root: list[Message]
TupleFormat = list[list[Union[str, tuple[str], tuple[str, str], None]]]
TupleFormat = list[
tuple[Union[str, tuple[str], None], Union[str, tuple[str], None]]
| list[Union[str, tuple[str], None]]
]
if TYPE_CHECKING:
from gradio.components import Timer
@ -148,6 +151,7 @@ class Chatbot(Component):
Events.undo,
Events.example_select,
Events.clear,
Events.copy,
]
def __init__(

View File

@ -25,7 +25,10 @@ class Markdown(Component):
Guides: key-features
"""
EVENTS = [Events.change]
EVENTS = [
Events.change,
Events.copy,
]
def __init__(
self,

View File

@ -31,6 +31,7 @@ class Textbox(FormComponent):
Events.focus,
Events.blur,
Events.stop,
Events.copy,
]
def __init__(

View File

@ -396,6 +396,31 @@ class DownloadData(EventData):
"""
@document()
class CopyData(EventData):
"""
The gr.CopyData class is a subclass of gr.EventData that specifically carries information about the `.copy()` event. When gr.CopyData
is added as a type hint to an argument of an event listener method, a gr.CopyData object will automatically be passed as the value of that argument.
The attributes of this object contains information about the event that triggered the listener.
Example:
import gradio as gr
def on_copy(copy_data: gr.CopyData):
return f"Copied text: {copy_data.value}"
with gr.Blocks() as demo:
textbox = gr.Textbox("Hello World!")
copied = gr.Textbox()
textbox.copy(on_copy, None, copied)
demo.launch()
"""
def __init__(self, target: Block | None, data: Any):
super().__init__(target, data)
self.value: Any = data["value"]
"""
The value that was copied.
"""
@dataclasses.dataclass
class EventListenerMethod:
block: Block | None
@ -987,3 +1012,7 @@ class Events:
"download",
doc="This listener is triggered when the user downloads a file from the {{ component }}. Uses event data gradio.DownloadData to carry information about the downloaded file as a FileData object. See EventData documentation on how to use this event data",
)
copy = EventListener(
"copy",
doc="This listener is triggered when the user copies content from the {{ component }}. Uses event data gradio.CopyData to carry information about the copied content. See EventData documentation on how to use this event data",
)

View File

@ -3,7 +3,7 @@
</script>
<script lang="ts">
import type { Gradio, SelectData, LikeData } from "@gradio/utils";
import type { Gradio, SelectData, LikeData, CopyData } from "@gradio/utils";
import ChatBot from "./shared/ChatBot.svelte";
import type { UndoRetryData } from "./shared/utils";
@ -61,6 +61,7 @@
retry: UndoRetryData;
undo: UndoRetryData;
clear: null;
copy: CopyData;
}>;
let _value: NormalisedMessage[] | null = [];
@ -143,6 +144,7 @@
value = [];
gradio.dispatch("clear");
}}
on:copy={(e) => gradio.dispatch("copy", e.detail)}
{avatar_images}
{sanitize_html}
{bubble_full_width}

View File

@ -8,7 +8,6 @@
import { is_component_message } from "./utils";
import { Retry, Undo } from "@gradio/icons";
import { IconButtonWrapper, IconButton } from "@gradio/atoms";
export let likeable: boolean;
export let show_retry: boolean;
export let show_undo: boolean;
@ -20,6 +19,7 @@
export let handle_action: (selected: string | null) => void;
export let layout: "bubble" | "panel";
export let dispatch: any;
function is_all_text(
message: NormalisedMessage[] | NormalisedMessage
@ -54,7 +54,10 @@
>
<IconButtonWrapper top_panel={false}>
{#if show_copy}
<Copy value={message_text} />
<Copy
value={message_text}
on:copy={(e) => dispatch("copy", e.detail)}
/>
{/if}
{#if show_retry}
<IconButton

View File

@ -9,6 +9,7 @@
} from "./utils";
import type { NormalisedMessage } from "../types";
import { copy } from "@gradio/utils";
import type { CopyData } from "@gradio/utils";
import Message from "./Message.svelte";
import { DownloadLink } from "@gradio/wasm/svelte";
@ -114,6 +115,7 @@
share: any;
error: string;
example_select: SelectData;
copy: CopyData;
}>();
function is_at_bottom(): boolean {
@ -343,6 +345,7 @@
handle_action={(selected) => handle_like(i, messages[0], selected)}
scroll={is_browser ? scroll : () => {}}
{allow_file_downloads}
on:copy={(e) => dispatch("copy", e.detail)}
/>
{/each}
{#if pending_message}

View File

@ -1,7 +1,13 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { onDestroy } from "svelte";
import { Copy, Check } from "@gradio/icons";
import { IconButton } from "@gradio/atoms";
import type { CopyData } from "@gradio/utils";
const dispatch = createEventDispatcher<{
change: undefined;
copy: CopyData;
}>();
let copied = false;
export let value: string;
@ -17,6 +23,7 @@
async function handle_copy(): Promise<void> {
if ("clipboard" in navigator) {
dispatch("copy", { value: value });
await navigator.clipboard.writeText(value);
copy_feedback();
} else {

View File

@ -82,6 +82,7 @@
position: "left" | "right";
layout: "bubble" | "panel";
avatar: FileData | null;
dispatch: any;
};
let button_panel_props: ButtonPanelProps;
@ -95,7 +96,8 @@
message: msg_format === "tuples" ? messages[0] : messages,
position: role === "user" ? "right" : "left",
avatar: avatar_img,
layout
layout,
dispatch
};
</script>
@ -209,7 +211,10 @@
</div>
{#if layout === "panel"}
<ButtonPanel {...button_panel_props} />
<ButtonPanel
{...button_panel_props}
on:copy={(e) => dispatch("copy", e.detail)}
/>
{/if}
{/each}
</div>

View File

@ -4,7 +4,7 @@
</script>
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import type { Gradio, CopyData } from "@gradio/utils";
import Markdown from "./shared/Markdown.svelte";
import { StatusTracker } from "@gradio/statustracker";
@ -22,6 +22,7 @@
export let line_breaks = false;
export let gradio: Gradio<{
change: never;
copy: CopyData;
clear_status: LoadingStatus;
}>;
export let latex_delimiters: {
@ -64,6 +65,7 @@
{visible}
{rtl}
on:change={() => gradio.dispatch("change")}
on:copy={(e) => gradio.dispatch("copy", e.detail)}
{latex_delimiters}
{sanitize_html}
{line_breaks}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { copy, css_units } from "@gradio/utils";
import type { CopyData } from "@gradio/utils";
import { Copy, Check } from "@gradio/icons";
import type { LoadingStatus } from "@gradio/statustracker";
import { IconButton, IconButtonWrapper } from "@gradio/atoms";
@ -28,13 +29,17 @@
let copied = false;
let timer: NodeJS.Timeout;
const dispatch = createEventDispatcher<{ change: undefined }>();
const dispatch = createEventDispatcher<{
change: undefined;
copy: CopyData;
}>();
$: value, dispatch("change");
async function handle_copy(): Promise<void> {
if ("clipboard" in navigator) {
await navigator.clipboard.writeText(value);
dispatch("copy", { value: value });
copy_feedback();
}
}

View File

@ -6,7 +6,7 @@
</script>
<script lang="ts">
import type { Gradio, SelectData } from "@gradio/utils";
import type { Gradio, SelectData, CopyData } from "@gradio/utils";
import TextBox from "./shared/Textbox.svelte";
import { Block } from "@gradio/atoms";
import { StatusTracker } from "@gradio/statustracker";
@ -21,6 +21,7 @@
focus: never;
stop: never;
clear_status: LoadingStatus;
copy: CopyData;
}>;
export let label = "Textbox";
export let info: string | undefined = undefined;
@ -95,6 +96,7 @@
on:select={(e) => gradio.dispatch("select", e.detail)}
on:focus={() => gradio.dispatch("focus")}
on:stop={() => gradio.dispatch("stop")}
on:copy={(e) => gradio.dispatch("copy", e.detail)}
disabled={!interactive}
/>
</Block>

View File

@ -8,7 +8,7 @@
import { BlockTitle } from "@gradio/atoms";
import { Copy, Check, Send, Square } from "@gradio/icons";
import { fade } from "svelte/transition";
import type { SelectData } from "@gradio/utils";
import type { SelectData, CopyData } from "@gradio/utils";
export let value = "";
export let value_is_output = false;
@ -52,6 +52,7 @@
select: SelectData;
input: undefined;
focus: undefined;
copy: CopyData;
}>();
beforeUpdate(() => {
@ -84,6 +85,7 @@
async function handle_copy(): Promise<void> {
if ("clipboard" in navigator) {
await navigator.clipboard.writeText(value);
dispatch("copy", { value: value });
copy_feedback();
}
}

View File

@ -30,6 +30,10 @@ export interface ShareData {
title?: string;
}
export interface CopyData {
value: string;
}
export class ShareError extends Error {
constructor(message: string) {
super(message);

View File

@ -22,7 +22,7 @@ class TestChatbot:
[(Path("test/test_files/audio_sample.wav"),), "cool audio"],
[(Path("test/test_files/bus.png"), "A bus"), "cool pic"],
]
postprocessed_multimodal_msg = chatbot.postprocess(multimodal_msg).model_dump()
postprocessed_multimodal_msg = chatbot.postprocess(multimodal_msg).model_dump() # type: ignore
for msg in postprocessed_multimodal_msg:
assert "file" in msg[0]
assert msg[1] in {"cool video", "cool audio", "cool pic"}