Skip to content

Commit c3a9e64

Browse files
aliabid94Ali Abidgradio-pr-botabidlabs
authored
Allow editing chatbot messages (#10203)
* changes * add changeset * changes * changes * changes * changes * changes * changes * changes * Update gradio/events.py Co-authored-by: Abubakar Abid <[email protected]> * Update gradio/components/chatbot.py Co-authored-by: Abubakar Abid <[email protected]> * changes --------- Co-authored-by: Ali Abid <[email protected]> Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Abubakar Abid <[email protected]>
1 parent 9c6d83d commit c3a9e64

File tree

16 files changed

+347
-125
lines changed

16 files changed

+347
-125
lines changed

.changeset/angry-times-float.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gradio/chatbot": minor
3+
"gradio": minor
4+
---
5+
6+
feat:Allow editing chatbot messages

demo/chatbot_editable/run.ipynb

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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=\"user\")\n", " chatbot2 = gr.Chatbot(value=[], type=\"tuples\", editable=\"user\")\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.EditData):\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}

demo/chatbot_editable/run.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import gradio as gr
2+
3+
with gr.Blocks() as demo:
4+
with gr.Row():
5+
chatbot = gr.Chatbot(value=[], type="messages", editable="user")
6+
chatbot2 = gr.Chatbot(value=[], type="tuples", editable="user")
7+
add_message_btn = gr.Button("Add Message")
8+
9+
with gr.Row():
10+
concatenated_text1 = gr.Textbox(label="Concatenated Chat 1")
11+
concatenated_text2 = gr.Textbox(label="Concatenated Chat 2")
12+
edited_messages = gr.Textbox(label="Edited Message")
13+
14+
def add_message(history: list, history2: list[list[str]]):
15+
usr_msg = "I'm a user"
16+
bot_msg = "I'm a bot"
17+
history.append({"role": "user", "content": usr_msg})
18+
history.append({"role": "assistant", "content": bot_msg})
19+
history2.append([usr_msg, bot_msg])
20+
return history, history2
21+
22+
add_message_btn.click(add_message, [chatbot, chatbot2], [chatbot, chatbot2])
23+
chatbot.change(lambda m: "|".join(m["content"] for m in m), chatbot, concatenated_text1)
24+
chatbot2.change(lambda m: "|".join("|".join(m) for m in m), chatbot2, concatenated_text2)
25+
26+
def edit_message(edited_message: gr.EditData):
27+
return f"{edited_message.value} at {edited_message.index}"
28+
29+
chatbot.edit(edit_message, None, edited_messages)
30+
chatbot2.edit(edit_message, None, edited_messages)
31+
32+
if __name__ == "__main__":
33+
demo.launch()

gradio/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
CopyData,
6969
DeletedFileData,
7070
DownloadData,
71+
EditData,
7172
EventData,
7273
KeyUpData,
7374
LikeData,

gradio/components/chatbot.py

+4
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ class Chatbot(Component):
161161
Events.option_select,
162162
Events.clear,
163163
Events.copy,
164+
Events.edit,
164165
]
165166

166167
def __init__(
@@ -185,6 +186,7 @@ def __init__(
185186
resizeable: bool = False,
186187
max_height: int | str | None = None,
187188
min_height: int | str | None = None,
189+
editable: Literal["user", "all"] | None = None,
188190
latex_delimiters: list[dict[str, str | bool]] | None = None,
189191
rtl: bool = False,
190192
show_share_button: bool | None = None,
@@ -222,6 +224,7 @@ def __init__(
222224
resizeable: If True, the component will be resizeable by the user.
223225
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`.
224226
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`.
227+
editable: Allows user to edit messages in the chatbot. If set to "user", allows editing of user messages. If set to "all", allows editing of assistant messages as well.
225228
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).
226229
rtl: If True, sets the direction of the rendered text to right-to-left. Default is False, which renders text left-to-right.
227230
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.
@@ -260,6 +263,7 @@ def __init__(
260263
self.resizeable = resizeable
261264
self.max_height = max_height
262265
self.min_height = min_height
266+
self.editable = editable
263267
self.rtl = rtl
264268
self.group_consecutive_messages = group_consecutive_messages
265269
if latex_delimiters is None:

gradio/events.py

+32
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,38 @@ def __init__(self, target: Block | None, data: Any):
371371
"""
372372

373373

374+
@document()
375+
class EditData(EventData):
376+
"""
377+
The gr.EditData class is a subclass of gr.Event data that specifically carries information about the `.edit()` event. When gr.EditData
378+
is added as a type hint to an argument of an event listener method, a gr.EditData object will automatically be passed as the value of that argument.
379+
The attributes of this object contains information about the event that triggered the listener.
380+
Example:
381+
import gradio as gr
382+
383+
def edit(edit_data: gr.EditData, history: list[gr.MessageDict]):
384+
history_up_to_edit = history[:edit_data.index]
385+
history_up_to_edit[-1] = edit_data.value
386+
return history_up_to_edit
387+
388+
with gr.Blocks() as demo:
389+
chatbot = gr.Chatbot()
390+
chatbot.undo(edit, chatbot, chatbot)
391+
demo.launch()
392+
"""
393+
394+
def __init__(self, target: Block | None, data: Any):
395+
super().__init__(target, data)
396+
self.index: int | tuple[int, int] = data["index"]
397+
"""
398+
The index of the message that was edited.
399+
"""
400+
self.value: Any = data["value"]
401+
"""
402+
The content of the message that was edited.
403+
"""
404+
405+
374406
@document()
375407
class DownloadData(EventData):
376408
"""

guides/05_chatbots/05_chatbot-specific-events.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def handle_retry(history, retry_data: gr.RetryData):
105105

106106
...
107107

108-
chatbot.retry(handle_retry, chatbot, [chatbot])
108+
chatbot.retry(handle_retry, chatbot, chatbot)
109109
```
110110

111111
You'll see that the bot messages have a "retry" icon now -
@@ -133,6 +133,21 @@ def handle_like(data: gr.LikeData):
133133
chatbot.like(vote, None, None)
134134
```
135135

136+
## The Edit Event
137+
138+
Same idea with the edit listener! with `gr.Chatbot(editable=True)`, you can capture user edits. The `gr.EditData` object tells us the index of the message edited and the new text of the mssage. Below, we use this object to edit the history, and delete any subsequent messages.
139+
140+
```python
141+
def handle_edit(history, edit_data: gr.EditData):
142+
new_history = history[:edit_data.index]
143+
new_history[-1]['content'] = edit_data.value
144+
return new_history
145+
146+
...
147+
148+
chatbot.edit(handle_edit, chatbot, chatbot)
149+
```
150+
136151

137152
## Conclusion
138153

js/chatbot/Chatbot.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ describe("Chatbot", () => {
4343

4444
const chatbot = getByRole("log");
4545

46-
const userButton = container.querySelector(".user button");
47-
const botButton = container.querySelector(".bot button");
46+
const userButton = container.querySelector(".user > div");
47+
const botButton = container.querySelector(".bot > div");
4848

4949
assert.notExists(userButton);
5050
assert.notExists(botButton);
@@ -60,8 +60,8 @@ describe("Chatbot", () => {
6060
latex_delimiters: [{ left: "$$", right: "$$", display: true }]
6161
});
6262

63-
const userButton = container.querySelector(".user button");
64-
const botButton = container.querySelector(".bot button");
63+
const userButton = container.querySelector(".user > div");
64+
const botButton = container.querySelector(".bot > div");
6565

6666
assert.exists(userButton);
6767
assert.exists(botButton);

js/chatbot/Index.svelte

+15
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
clear_status: LoadingStatus;
6060
example_select: SelectData;
6161
option_select: SelectData;
62+
edit: SelectData;
6263
retry: UndoRetryData;
6364
undo: UndoRetryData;
6465
clear: null;
@@ -79,6 +80,7 @@
7980
export let resizeable: boolean;
8081
export let min_height: number | string | undefined;
8182
export let max_height: number | string | undefined;
83+
export let editable: "user" | "all" | null = null;
8284
export let placeholder: string | null = null;
8385
export let examples: ExampleMessage[] | null = null;
8486
export let theme_mode: "system" | "light" | "dark";
@@ -131,6 +133,7 @@
131133
display_consecutive_in_same_bubble={group_consecutive_messages}
132134
{render_markdown}
133135
{theme_mode}
136+
{editable}
134137
pending_message={loading_status?.status === "pending"}
135138
generating={loading_status?.status === "generating"}
136139
{rtl}
@@ -150,6 +153,18 @@
150153
gradio.dispatch("clear");
151154
}}
152155
on:copy={(e) => gradio.dispatch("copy", e.detail)}
156+
on:edit={(e) => {
157+
if (value === null || value.length === 0) return;
158+
if (type === "messages") {
159+
//@ts-ignore
160+
value[e.detail.index].content = e.detail.value;
161+
} else {
162+
//@ts-ignore
163+
value[e.detail.index[0]][e.detail.index[1]] = e.detail.value;
164+
}
165+
value = value;
166+
gradio.dispatch("edit", e.detail);
167+
}}
153168
{avatar_images}
154169
{sanitize_html}
155170
{line_breaks}

js/chatbot/shared/ButtonPanel.svelte

+45-27
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
import LikeDislike from "./LikeDislike.svelte";
33
import Copy from "./Copy.svelte";
44
import type { FileData } from "@gradio/client";
5-
import DownloadIcon from "./Download.svelte";
6-
import { DownloadLink } from "@gradio/wasm/svelte";
75
import type { NormalisedMessage, TextMessage } from "../types";
8-
import { is_component_message } from "./utils";
9-
import { Retry, Undo } from "@gradio/icons";
6+
import { Retry, Undo, Edit, Check, Clear } from "@gradio/icons";
107
import { IconButtonWrapper, IconButton } from "@gradio/atoms";
118
export let likeable: boolean;
129
export let show_retry: boolean;
1310
export let show_undo: boolean;
11+
export let show_edit: boolean;
12+
export let in_edit_mode: boolean;
1413
export let show_copy_button: boolean;
1514
export let message: NormalisedMessage | NormalisedMessage[];
1615
export let position: "right" | "left";
@@ -41,42 +40,61 @@
4140
$: message_text = is_all_text(message) ? all_text(message) : "";
4241
4342
$: show_copy = show_copy_button && message && is_all_text(message);
44-
$: show_download =
45-
!Array.isArray(message) &&
46-
is_component_message(message) &&
47-
message.content.value?.url;
4843
</script>
4944

50-
{#if show_copy || show_retry || show_undo || likeable}
45+
{#if show_copy || show_retry || show_undo || show_edit || likeable}
5146
<div
5247
class="message-buttons-{position} {layout} message-buttons {avatar !==
5348
null && 'with-avatar'}"
5449
>
5550
<IconButtonWrapper top_panel={false}>
56-
{#if show_copy}
57-
<Copy
58-
value={message_text}
59-
on:copy={(e) => dispatch("copy", e.detail)}
60-
/>
61-
{/if}
62-
{#if show_retry}
51+
{#if in_edit_mode}
6352
<IconButton
64-
Icon={Retry}
65-
label="Retry"
66-
on:click={() => handle_action("retry")}
53+
label="Submit"
54+
Icon={Check}
55+
on:click={() => handle_action("edit_submit")}
6756
disabled={generating}
6857
/>
69-
{/if}
70-
{#if show_undo}
7158
<IconButton
72-
label="Undo"
73-
Icon={Undo}
74-
on:click={() => handle_action("undo")}
59+
label="Cancel"
60+
Icon={Clear}
61+
on:click={() => handle_action("edit_cancel")}
7562
disabled={generating}
7663
/>
77-
{/if}
78-
{#if likeable}
79-
<LikeDislike {handle_action} />
64+
{:else}
65+
{#if show_copy}
66+
<Copy
67+
value={message_text}
68+
on:copy={(e) => dispatch("copy", e.detail)}
69+
/>
70+
{/if}
71+
{#if show_retry}
72+
<IconButton
73+
Icon={Retry}
74+
label="Retry"
75+
on:click={() => handle_action("retry")}
76+
disabled={generating}
77+
/>
78+
{/if}
79+
{#if show_undo}
80+
<IconButton
81+
label="Undo"
82+
Icon={Undo}
83+
on:click={() => handle_action("undo")}
84+
disabled={generating}
85+
/>
86+
{/if}
87+
{#if show_edit}
88+
<IconButton
89+
label="Edit"
90+
Icon={Edit}
91+
on:click={() => handle_action("edit")}
92+
disabled={generating}
93+
/>
94+
{/if}
95+
{#if likeable}
96+
<LikeDislike {handle_action} />
97+
{/if}
8098
{/if}
8199
</IconButtonWrapper>
82100
</div>

0 commit comments

Comments
 (0)