mirror of
https://github.com/gradio-app/gradio.git
synced 2025-02-23 11:39:17 +08:00
Add copy all messages button to chatbot (#9013)
* add copy all button to chatbot * tweaks * lint * use value.url for media * remove import * add changeset --------- Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
parent
62ed369efa
commit
5350f1feb2
7
.changeset/new-masks-retire.md
Normal file
7
.changeset/new-masks-retire.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
"@gradio/chatbot": minor
|
||||
"@gradio/code": minor
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:Add copy all messages button to chatbot
|
@ -172,6 +172,7 @@ class Chatbot(Component):
|
||||
likeable: bool = False,
|
||||
layout: Literal["panel", "bubble"] | None = None,
|
||||
placeholder: str | None = None,
|
||||
show_copy_all_button=False,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
@ -202,6 +203,7 @@ class Chatbot(Component):
|
||||
likeable: Whether the chat messages display a like or dislike button. Set automatically by the .like method but has to be present in the signature for it to show up in the config.
|
||||
layout: If "panel", will display the chatbot in a llm style layout. If "bubble", will display the chatbot with message bubbles, with the user and bot messages on alterating sides. Will default to "bubble".
|
||||
placeholder: a placeholder message to display in the chatbot when it is empty. Centered vertically and horizontally in the Chatbot. Supports Markdown and HTML. If None, no placeholder is displayed.
|
||||
show_copy_all_button: If True, will show a copy all button that copies all chatbot messages to the clipboard.
|
||||
"""
|
||||
self.likeable = likeable
|
||||
if type not in ["messages", "tuples"]:
|
||||
@ -227,6 +229,7 @@ class Chatbot(Component):
|
||||
self.bubble_full_width = bubble_full_width
|
||||
self.line_breaks = line_breaks
|
||||
self.layout = layout
|
||||
self.show_copy_all_button = show_copy_all_button
|
||||
super().__init__(
|
||||
label=label,
|
||||
every=every,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { test, describe, assert, afterEach } from "vitest";
|
||||
import { cleanup, render } from "@gradio/tootils";
|
||||
import { test, describe, assert, afterEach, vi } from "vitest";
|
||||
import { cleanup, render, fireEvent } from "@gradio/tootils";
|
||||
import Chatbot from "./Index.svelte";
|
||||
import type { LoadingStatus } from "@gradio/statustracker";
|
||||
import type { FileData } from "@gradio/client";
|
||||
@ -204,4 +204,33 @@ describe("Chatbot", () => {
|
||||
assert.isTrue(file_link[0].href.includes("titanic.csv"));
|
||||
assert.isTrue(file_link[0].href.includes("titanic.csv"));
|
||||
});
|
||||
|
||||
test("renders copy all messages button and copies all messages to clipboard", async () => {
|
||||
// mock the clipboard API
|
||||
const clipboard_write_text_mock = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: clipboard_write_text_mock },
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
|
||||
const { getByLabelText } = await render(Chatbot, {
|
||||
loading_status,
|
||||
label: "chatbot",
|
||||
value: [["user message one", "bot message one"]],
|
||||
show_copy_all_button: true
|
||||
});
|
||||
|
||||
const copy_button = getByLabelText("Copy conversation");
|
||||
|
||||
fireEvent.click(copy_button);
|
||||
|
||||
expect(clipboard_write_text_mock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("user: user message one")
|
||||
);
|
||||
expect(clipboard_write_text_mock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("assistant: bot message one")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -11,12 +11,7 @@
|
||||
import { Chat } from "@gradio/icons";
|
||||
import type { FileData } from "@gradio/client";
|
||||
import { StatusTracker } from "@gradio/statustracker";
|
||||
import type {
|
||||
Message,
|
||||
TupleFormat,
|
||||
MessageRole,
|
||||
NormalisedMessage
|
||||
} from "./types";
|
||||
import type { Message, TupleFormat, NormalisedMessage } from "./types";
|
||||
|
||||
import { normalise_tuples, normalise_messages } from "./shared/utils";
|
||||
|
||||
@ -34,6 +29,7 @@
|
||||
export let show_share_button = false;
|
||||
export let rtl = false;
|
||||
export let show_copy_button = true;
|
||||
export let show_copy_all_button = false;
|
||||
export let sanitize_html = true;
|
||||
export let bubble_full_width = true;
|
||||
export let layout: "bubble" | "panel" = "bubble";
|
||||
@ -103,6 +99,7 @@
|
||||
selectable={_selectable}
|
||||
{likeable}
|
||||
{show_share_button}
|
||||
{show_copy_all_button}
|
||||
value={_value}
|
||||
{latex_delimiters}
|
||||
{render_markdown}
|
||||
|
@ -30,6 +30,7 @@
|
||||
import Component from "./Component.svelte";
|
||||
import LikeButtons from "./ButtonPanel.svelte";
|
||||
import type { LoadedComponent } from "../../app/src/types";
|
||||
import CopyAll from "./CopyAll.svelte";
|
||||
|
||||
export let _fetch: typeof fetch;
|
||||
export let load_component: Gradio["load_component"];
|
||||
@ -80,6 +81,7 @@
|
||||
export let selectable = false;
|
||||
export let likeable = false;
|
||||
export let show_share_button = false;
|
||||
export let show_copy_all_button = false;
|
||||
export let rtl = false;
|
||||
export let show_copy_button = false;
|
||||
export let avatar_images: [FileData | null, FileData | null] = [null, null];
|
||||
@ -275,6 +277,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if show_copy_all_button}
|
||||
<CopyAll {value} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class={layout === "bubble" ? "bubble-wrap" : "panel-wrap"}
|
||||
class:placeholder-container={value === null || value.length === 0}
|
||||
|
84
js/chatbot/shared/CopyAll.svelte
Normal file
84
js/chatbot/shared/CopyAll.svelte
Normal file
@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import { Copy, Check } from "@gradio/icons";
|
||||
import type { NormalisedMessage } from "../types";
|
||||
|
||||
let copied = false;
|
||||
export let value: NormalisedMessage[] | null;
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
function copy_feedback(): void {
|
||||
copied = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
copied = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const copy_conversation = (): void => {
|
||||
if (value) {
|
||||
const conversation_value = value
|
||||
.map((message) => {
|
||||
if (message.type === "text") {
|
||||
return `${message.role}: ${message.content}`;
|
||||
}
|
||||
return `${message.role}: ${message.content.value.url}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
navigator.clipboard.writeText(conversation_value).catch((err) => {
|
||||
console.error("Failed to copy conversation: ", err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function handle_copy(): Promise<void> {
|
||||
if ("clipboard" in navigator) {
|
||||
copy_conversation();
|
||||
copy_feedback();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={handle_copy}
|
||||
title="Copy conversation"
|
||||
class={copied ? "copied" : "copy-text"}
|
||||
aria-label={copied ? "Copied conversation" : "Copy conversation"}
|
||||
>
|
||||
{#if copied}
|
||||
<Check />
|
||||
{:else}
|
||||
<Copy />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: var(--block-label-margin);
|
||||
right: var(--block-label-margin);
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-drop);
|
||||
border: 1px solid var(--border-color-primary);
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-radius: var(--block-label-right-radius);
|
||||
background: var(--block-label-background-fill);
|
||||
padding: var(--spacing-sm);
|
||||
width: var(--size-6);
|
||||
height: var(--size-6);
|
||||
overflow: hidden;
|
||||
color: var(--block-label-text-color);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
</style>
|
@ -29,9 +29,9 @@
|
||||
|
||||
<button
|
||||
on:click={handle_copy}
|
||||
title="copy"
|
||||
title="Copy message"
|
||||
class:copied
|
||||
aria-label={copied ? "Value copied" : "Copy value"}
|
||||
aria-label={copied ? "Message copied" : "Copy Message"}
|
||||
>
|
||||
{#if !copied}
|
||||
<Copy />
|
||||
|
@ -58,6 +58,7 @@ class TestChatbot:
|
||||
"bubble_full_width": True,
|
||||
"line_breaks": True,
|
||||
"layout": None,
|
||||
"show_copy_all_button": False,
|
||||
}
|
||||
|
||||
def test_avatar_images_are_moved_to_cache(self):
|
||||
|
Loading…
Reference in New Issue
Block a user