Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow editing chatbot messages #10203

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/angry-times-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gradio/chatbot": minor
"gradio": minor
---

feat:Allow editing chatbot messages
1 change: 1 addition & 0 deletions demo/chatbot_editable/run.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: chatbot_editable"]}, {"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", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " chatbot = gr.Chatbot(value=[], type=\"messages\", editable=True)\n", " chatbot2 = gr.Chatbot(value=[], type=\"tuples\", editable=True)\n", " add_message_btn = gr.Button(\"Add Message\")\n", "\n", " with gr.Row():\n", " concatenated_text1 = gr.Textbox(label=\"Concatenated Chat 1\")\n", " concatenated_text2 = gr.Textbox(label=\"Concatenated Chat 2\")\n", " edited_messages = gr.Textbox(label=\"Edited Message\")\n", "\n", " def add_message(history: list, history2: list[list[str]]):\n", " usr_msg = \"I'm a user\"\n", " bot_msg = \"I'm a bot\"\n", " history.append({\"role\": \"user\", \"content\": usr_msg})\n", " history.append({\"role\": \"assistant\", \"content\": bot_msg})\n", " history2.append([usr_msg, bot_msg])\n", " return history, history2\n", " \n", " add_message_btn.click(add_message, [chatbot, chatbot2], [chatbot, chatbot2])\n", " chatbot.change(lambda m: \"|\".join(m[\"content\"] for m in m), chatbot, concatenated_text1)\n", " chatbot2.change(lambda m: \"|\".join(\"|\".join(m) for m in m), chatbot2, concatenated_text2)\n", "\n", " def edit_message(edited_message: gr.SelectData):\n", " return f\"{edited_message.value} at {edited_message.index}\"\n", " \n", " chatbot.edit(edit_message, None, edited_messages)\n", " chatbot2.edit(edit_message, None, edited_messages)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
33 changes: 33 additions & 0 deletions demo/chatbot_editable/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import gradio as gr

with gr.Blocks() as demo:
with gr.Row():
chatbot = gr.Chatbot(value=[], type="messages", editable=True)
chatbot2 = gr.Chatbot(value=[], type="tuples", editable=True)
add_message_btn = gr.Button("Add Message")

with gr.Row():
concatenated_text1 = gr.Textbox(label="Concatenated Chat 1")
concatenated_text2 = gr.Textbox(label="Concatenated Chat 2")
edited_messages = gr.Textbox(label="Edited Message")

def add_message(history: list, history2: list[list[str]]):
usr_msg = "I'm a user"
bot_msg = "I'm a bot"
history.append({"role": "user", "content": usr_msg})
history.append({"role": "assistant", "content": bot_msg})
history2.append([usr_msg, bot_msg])
return history, history2

add_message_btn.click(add_message, [chatbot, chatbot2], [chatbot, chatbot2])
chatbot.change(lambda m: "|".join(m["content"] for m in m), chatbot, concatenated_text1)
chatbot2.change(lambda m: "|".join("|".join(m) for m in m), chatbot2, concatenated_text2)

def edit_message(edited_message: gr.SelectData):
return f"{edited_message.value} at {edited_message.index}"

chatbot.edit(edit_message, None, edited_messages)
chatbot2.edit(edit_message, None, edited_messages)

if __name__ == "__main__":
demo.launch()
4 changes: 4 additions & 0 deletions gradio/components/chatbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ class Chatbot(Component):
Events.option_select,
Events.clear,
Events.copy,
Events.edit,
]

def __init__(
Expand All @@ -185,6 +186,7 @@ def __init__(
resizeable: bool = False,
max_height: int | str | None = None,
min_height: int | str | None = None,
editable: bool = False,
latex_delimiters: list[dict[str, str | bool]] | None = None,
rtl: bool = False,
show_share_button: bool | None = None,
Expand Down Expand Up @@ -222,6 +224,7 @@ def __init__(
resizeable: If True, the component will be resizeable by the user.
max_height: The maximum height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. If messages exceed the height, the component will scroll. If messages are shorter than the height, the component will shrink to fit the content. Will not have any effect if `height` is set and is smaller than `max_height`.
min_height: The minimum height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. If messages exceed the height, the component will expand to fit the content. Will not have any effect if `height` is set and is larger than `min_height`.
editable: If True, the user can edit the text in the chatbot. Can also be set to "all", which allows the user to edit assistant messages as well.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since "all" is allowed, how about the type iseditable: Literal["user", "all"] | None = None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a new editable attribute, why not use interactive, which is meant for exactly this use case (i.e. that a user can edit the component's value via the UI)?

If the concern is that we want to restrict the scope of interactivity to either user messages or all messages, then I think its time we allow interactive to take non-boolean values as well, where it makes sense. For example, we've had requests to restrict the scope of interactivity in the DataFrame component, and this is analogous.

In that case, we could use the interactive parameter like this:

interactive: bool | Literal["user"] = False

if interactive is True, then all messages can be edited. If interactive is "user", then only user messages can be edited. We may choose to add Literal["assistant"] later on.

Copy link
Collaborator Author

@aliabid94 aliabid94 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a new editable attribute, why not use interactive

I didn't go this route because a lot of gr.Blocks() demos use chatbot as input to store history, and therefore by default, interactive would be true, so these chatbots would become editable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, ok that makes sense, I agree with @freddyaboulton's suggestion above 👍

latex_delimiters: A list of dicts of the form {"left": open delimiter (str), "right": close delimiter (str), "display": whether to display in newline (bool)} that will be used to render LaTeX expressions. If not provided, `latex_delimiters` is set to `[{ "left": "$$", "right": "$$", "display": True }]`, so only expressions enclosed in $$ delimiters will be rendered as LaTeX, and in a new line. Pass in an empty list to disable LaTeX rendering. For more information, see the [KaTeX documentation](https://katex.org/docs/autorender.html).
rtl: If True, sets the direction of the rendered text to right-to-left. Default is False, which renders text left-to-right.
show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
Expand Down Expand Up @@ -260,6 +263,7 @@ def __init__(
self.resizeable = resizeable
self.max_height = max_height
self.min_height = min_height
self.editable = editable
self.rtl = rtl
self.group_consecutive_messages = group_consecutive_messages
if latex_delimiters is None:
Expand Down
15 changes: 15 additions & 0 deletions js/chatbot/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
clear_status: LoadingStatus;
example_select: SelectData;
option_select: SelectData;
edit: SelectData;
retry: UndoRetryData;
undo: UndoRetryData;
clear: null;
Expand All @@ -79,6 +80,7 @@
export let resizeable: boolean;
export let min_height: number | string | undefined;
export let max_height: number | string | undefined;
export let editable: boolean | "all" = false;
export let placeholder: string | null = null;
export let examples: ExampleMessage[] | null = null;
export let theme_mode: "system" | "light" | "dark";
Expand Down Expand Up @@ -131,6 +133,7 @@
display_consecutive_in_same_bubble={group_consecutive_messages}
{render_markdown}
{theme_mode}
{editable}
pending_message={loading_status?.status === "pending"}
generating={loading_status?.status === "generating"}
{rtl}
Expand All @@ -150,6 +153,18 @@
gradio.dispatch("clear");
}}
on:copy={(e) => gradio.dispatch("copy", e.detail)}
on:edit={(e) => {
if (value === null || value.length === 0) return;
if (type === "messages") {
//@ts-ignore
value[e.detail.index].content = e.detail.value;
} else {
//@ts-ignore
value[e.detail.index[0]][e.detail.index[1]] = e.detail.value;
}
value = value;
gradio.dispatch("edit", e.detail);
}}
{avatar_images}
{sanitize_html}
{line_breaks}
Expand Down
72 changes: 45 additions & 27 deletions js/chatbot/shared/ButtonPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import LikeDislike from "./LikeDislike.svelte";
import Copy from "./Copy.svelte";
import type { FileData } from "@gradio/client";
import DownloadIcon from "./Download.svelte";
import { DownloadLink } from "@gradio/wasm/svelte";
import type { NormalisedMessage, TextMessage } from "../types";
import { is_component_message } from "./utils";
import { Retry, Undo } from "@gradio/icons";
import { Retry, Undo, Edit, Check, Clear } from "@gradio/icons";
import { IconButtonWrapper, IconButton } from "@gradio/atoms";
export let likeable: boolean;
export let show_retry: boolean;
export let show_undo: boolean;
export let show_edit: boolean;
export let in_edit_mode: boolean;
export let show_copy_button: boolean;
export let message: NormalisedMessage | NormalisedMessage[];
export let position: "right" | "left";
Expand Down Expand Up @@ -41,42 +40,61 @@
$: message_text = is_all_text(message) ? all_text(message) : "";

$: show_copy = show_copy_button && message && is_all_text(message);
$: show_download =
!Array.isArray(message) &&
is_component_message(message) &&
message.content.value?.url;
</script>

{#if show_copy || show_retry || show_undo || likeable}
{#if show_copy || show_retry || show_undo || show_edit || likeable}
<div
class="message-buttons-{position} {layout} message-buttons {avatar !==
null && 'with-avatar'}"
>
<IconButtonWrapper top_panel={false}>
{#if show_copy}
<Copy
value={message_text}
on:copy={(e) => dispatch("copy", e.detail)}
/>
{/if}
{#if show_retry}
{#if in_edit_mode}
<IconButton
Icon={Retry}
label="Retry"
on:click={() => handle_action("retry")}
label="Submit"
Icon={Check}
on:click={() => handle_action("edit_submit")}
disabled={generating}
/>
{/if}
{#if show_undo}
<IconButton
label="Undo"
Icon={Undo}
on:click={() => handle_action("undo")}
label="Cancel"
Icon={Clear}
on:click={() => handle_action("edit_cancel")}
disabled={generating}
/>
{/if}
{#if likeable}
<LikeDislike {handle_action} />
{:else}
{#if show_copy}
<Copy
value={message_text}
on:copy={(e) => dispatch("copy", e.detail)}
/>
{/if}
{#if show_retry}
<IconButton
Icon={Retry}
label="Retry"
on:click={() => handle_action("retry")}
disabled={generating}
/>
{/if}
{#if show_undo}
<IconButton
label="Undo"
Icon={Undo}
on:click={() => handle_action("undo")}
disabled={generating}
/>
{/if}
{#if show_edit}
<IconButton
label="Edit"
Icon={Edit}
on:click={() => handle_action("edit")}
disabled={generating}
/>
{/if}
{#if likeable}
<LikeDislike {handle_action} />
{/if}
{/if}
</IconButtonWrapper>
</div>
Expand Down
83 changes: 45 additions & 38 deletions js/chatbot/shared/ChatBot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
format_chat_for_sharing,
type UndoRetryData,
type EditData,
is_last_bot_message,
group_messages,
load_components,
Expand All @@ -11,7 +12,6 @@
import { copy } from "@gradio/utils";
import type { CopyData } from "@gradio/utils";
import Message from "./Message.svelte";
import { DownloadLink } from "@gradio/wasm/svelte";

import { dequal } from "dequal/lite";
import {
Expand All @@ -21,13 +21,11 @@
tick,
onMount
} from "svelte";
import { Image } from "@gradio/image/shared";

import { Trash, Community, ScrollDownArrow } from "@gradio/icons";
import { IconButtonWrapper, IconButton } from "@gradio/atoms";
import type { SelectData, LikeData } from "@gradio/utils";
import type { ExampleMessage } from "../types";
import { MarkdownCode as Markdown } from "@gradio/markdown-code";
import type { FileData, Client } from "@gradio/client";
import type { I18nFormatter } from "js/core/src/gradio_helper";
import Pending from "./Pending.svelte";
Expand Down Expand Up @@ -69,6 +67,7 @@
export let generating = false;
export let selectable = false;
export let likeable = false;
export let editable: boolean | "all" = false;
export let show_share_button = false;
export let show_copy_all_button = false;
export let rtl = false;
Expand All @@ -91,6 +90,8 @@
export let root: string;

let target: HTMLElement | null = null;
let edit_index: number | null = null;
let edit_message: string = "";

Check failure on line 94 in js/chatbot/shared/ChatBot.svelte

View workflow job for this annotation

GitHub Actions / js-test

Type string trivially inferred from a string literal, remove type annotation

onMount(() => {
target = document.querySelector("div.gradio-container");
Expand All @@ -104,6 +105,7 @@
change: undefined;
select: SelectData;
like: LikeData;
edit: EditData;
undo: UndoRetryData;
retry: UndoRetryData;
clear: undefined;
Expand All @@ -125,12 +127,6 @@
}

let scroll_after_component_load = false;
function on_child_component_load(): void {
if (scroll_after_component_load) {
scroll_to_bottom();
scroll_after_component_load = false;
}
}

async function scroll_on_value_update(): Promise<void> {
if (!autoscroll) return;
Expand Down Expand Up @@ -170,21 +166,15 @@

$: {
if (!dequal(value, old_value)) {
console.log("CHANGE!", JSON.stringify(value));

Check failure on line 169 in js/chatbot/shared/ChatBot.svelte

View workflow job for this annotation

GitHub Actions / js-test

Unexpected console statement
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: remove

old_value = value;
dispatch("change");
}
}
$: groupedMessages = value && group_messages(value, msg_format);
$: options = value && get_last_bot_options();

function handle_example_select(i: number, example: ExampleMessage): void {
dispatch("example_select", {
index: i,
value: { text: example.text, files: example.files }
});
}

function handle_like(
function handle_action(
i: number,
message: NormalisedMessage,
selected: string | null
Expand All @@ -201,29 +191,39 @@
index: val_[last_index].index,
value: val_[last_index].content
});
return;
}

if (msg_format === "tuples") {
dispatch("like", {
} else if (selected == "edit") {
edit_index = i;
edit_message = message.content as string;
} else if (selected == "edit_cancel") {
edit_index = null;
} else if (selected == "edit_submit") {
edit_index = null;
dispatch("edit", {
index: message.index,
value: message.content,
liked: selected === "like"
value: edit_message
});
} else {
if (!groupedMessages) return;

const message_group = groupedMessages[i];
const [first, last] = [
message_group[0],
message_group[message_group.length - 1]
];

dispatch("like", {
index: [first.index, last.index] as [number, number],
value: message_group.map((m) => m.content),
liked: selected === "like"
});
if (msg_format === "tuples") {
dispatch("like", {
index: message.index,
value: message.content,
liked: selected === "like"
});
} else {
if (!groupedMessages) return;

const message_group = groupedMessages[i];
const [first, last] = [
message_group[0],
message_group[message_group.length - 1]
];

dispatch("like", {
index: [first.index, last.index] as [number, number],
value: message_group.map((m) => m.content),
liked: selected === "like"
});
}
}
}

Expand Down Expand Up @@ -305,8 +305,15 @@
show_like={role === "user" ? likeable && like_user_message : likeable}
show_retry={_retryable && is_last_bot_message(messages, value)}
show_undo={_undoable && is_last_bot_message(messages, value)}
show_edit={editable === "all" ||
(editable &&
role === "user" &&
messages.length > 0 &&
messages[messages.length - 1].type == "text")}
in_edit_mode={edit_index === i}
bind:edit_message
{show_copy_button}
handle_action={(selected) => handle_like(i, messages[0], selected)}
handle_action={(selected) => handle_action(i, messages[0], selected)}
scroll={is_browser ? scroll : () => {}}
{allow_file_downloads}
on:copy={(e) => dispatch("copy", e.detail)}
Expand Down
Loading
Loading