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:
Hannah 2024-08-12 15:08:12 +01:00 committed by GitHub
parent 62ed369efa
commit 5350f1feb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 137 additions and 10 deletions

View File

@ -0,0 +1,7 @@
---
"@gradio/chatbot": minor
"@gradio/code": minor
"gradio": minor
---
feat:Add copy all messages button to chatbot

View File

@ -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,

View File

@ -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")
);
});
});

View File

@ -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}

View File

@ -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}

View 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>

View File

@ -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 />

View File

@ -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):