mirror of
https://github.com/gradio-app/gradio.git
synced 2025-01-24 10:54:04 +08:00
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:
parent
2afcad80ab
commit
e7629f7eac
9
.changeset/new-wombats-rhyme.md
Normal file
9
.changeset/new-wombats-rhyme.md
Normal 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`
|
1
demo/copy_events/run.ipynb
Normal file
1
demo/copy_events/run.ipynb
Normal 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
22
demo/copy_events/run.py
Normal 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()
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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__(
|
||||
|
@ -25,7 +25,10 @@ class Markdown(Component):
|
||||
Guides: key-features
|
||||
"""
|
||||
|
||||
EVENTS = [Events.change]
|
||||
EVENTS = [
|
||||
Events.change,
|
||||
Events.copy,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -31,6 +31,7 @@ class Textbox(FormComponent):
|
||||
Events.focus,
|
||||
Events.blur,
|
||||
Events.stop,
|
||||
Events.copy,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,10 @@ export interface ShareData {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface CopyData {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class ShareError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
@ -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"}
|
||||
|
Loading…
Reference in New Issue
Block a user