Repository: gradio-app/gradio
Pull request: Allow editing chatbot messages
Adds the ability to edit chatbot messages. See below:
![Recording 2024-12-15 at 12 41 27](https://github.com/user-attachments/assets/102d0ab9-2263-491f-be2a-274f99a669d4)

In a followup PR, I still need to:
- Allow editing non-text messages (images, etc.)
- Add editing to ChatInterface by default
- When messages are grouped, allow editing other then the last message

Closes: https://github.com/gradio-app/gradio/issues/7919

Problem statement:
Allow editing of messages in Chatbot
- [x] I have searched to see if a similar issue already exists.


**Is your feature request related to a problem? Please describe.**  
It would be nice if we could add an option to allow people to edit both their and a chatbot's message.

**Describe the solution you'd like**  
Add a `allow_assistant_edit=True` and `allow_user_edit=True` method, as well as an event (`chatbot.user_edit()` and `chatbot.assistant_edit()`) when a user or assistant message is edited.

Hints from issue discussion:
Can you describe the use case for this a bit?
Sure! Would be nice for data collection, data editing, data review, etc as well as steerable writing (ie to help write blog posts/essays/etc)
I see, wouldn't a `gr.Textbox` output be more useful for these use cases? It seems a bit cumbersome to edit an essay within a Chatbot message for example
I mean for interactive editing, also it would be great for data collection (which is what I was thinking of doing), a textbox doesn’t support multi round conversations 
Data collection meaning the chatbot generates something, and then you allow users to edit the generation e.g. to provide a better generation that you could use to finetune the model?
Correct, to edit any mistakes the chatbot made
Looking into the recent regressions and in the past, the current chatbot should be kept un-touched, so folks can use it as a baseline and create custom components the way they want. Something new would come up tomorrow and people would say we want this or that to be included in the bot. This screws up existing applications and they can't upgrade to newer version of gradio. 

If these features are added, they should be published as 'experimental' custom components (not part of main package). Then, after months of testing and acceptance, they may be merged into main package. 
I was thinking more of something like creating a new component based on Chatbot (ie `EditableChatbot`)
If we do implement this, the appropriate way would be to allow this behavior when `interactive=True` in the `gr.Chatbot` -- a parameter that currently has no effect.

But that might be a while off. In the meantime, I think this would be a great case for a custom component!  https://www.gradio.app/guides/five-minute-guide, and we're happy to help.
Right now, if you'd like to implement something liek this, you can use the Chatbot.select listener to allow users to select a message, in which case another gr.Textbox could become visible and allow for a user to edit the message. Something like:

```python
import gradio as gr

with gr.Blocks() as demo:
    chatbot = gr.Chatbot(value=[["Hello", "Hi"], ["How are you?", "I'm good, how are you?"]])
    edit_box = gr.Textbox(label="Edit Message", visible=False)
    selection = gr.State(None)

    def select_message(selection: gr.SelectData):
        return selection.index, gr.Textbox(visible=True)

    chatbot.select(select_message, None, [selection, edit_box])

    def submit_edit(edit_message, selection, chat):
        chat[selection[0]][selection[1]] = edit_message
        return gr.Textbox("", visible=False), None, chat
    
    edit_box.submit(submit_edit, [edit_box, selection, chatbot], [edit_box, selection, chatbot])

if __name__ == "__main__":
    demo.launch()
```

![Recording 2024-12-03 at 12 56 11](https://github.com/user-attachments/assets/bf304640-f03a-4b95-b5c9-0e2a3d2a7ad2)

Commit message: changes

Patch to review:
diff --git a/.changeset/angry-times-float.md b/.changeset/angry-times-float.md
new file mode 100644
index 0000000000..e9bd59032b
--- /dev/null
+++ b/.changeset/angry-times-float.md
@@ -0,0 +1,6 @@
+---
+"@gradio/chatbot": minor
+"gradio": minor
+---
+
+feat:Allow editing chatbot messages
diff --git a/demo/chatbot_editable/run.ipynb b/demo/chatbot_editable/run.ipynb
new file mode 100644
index 0000000000..8d69157334
--- /dev/null
+++ b/demo/chatbot_editable/run.ipynb
@@ -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=\"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}
\ No newline at end of file
diff --git a/demo/chatbot_editable/run.py b/demo/chatbot_editable/run.py
new file mode 100644
index 0000000000..69e46e5efb
--- /dev/null
+++ b/demo/chatbot_editable/run.py
@@ -0,0 +1,33 @@
+import gradio as gr
+
+with gr.Blocks() as demo:
+    with gr.Row():
+        chatbot = gr.Chatbot(value=[], type="messages", editable="user")
+        chatbot2 = gr.Chatbot(value=[], type="tuples", editable="user")
+    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.EditData):
+        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()
diff --git a/gradio/__init__.py b/gradio/__init__.py
index 5135c9fef7..8962da473f 100644
--- a/gradio/__init__.py
+++ b/gradio/__init__.py
@@ -68,6 +68,7 @@
     CopyData,
     DeletedFileData,
     DownloadData,
+    EditData,
     EventData,
     KeyUpData,
     LikeData,
diff --git a/gradio/components/chatbot.py b/gradio/components/chatbot.py
index eb089a5ade..a3f9840857 100644
--- a/gradio/components/chatbot.py
+++ b/gradio/components/chatbot.py
@@ -161,6 +161,7 @@ class Chatbot(Component):
         Events.option_select,
         Events.clear,
         Events.copy,
+        Events.edit,
     ]
 
     def __init__(
@@ -185,6 +186,7 @@ def __init__(
         resizeable: bool = False,
         max_height: int | str | None = None,
         min_height: int | str | None = None,
+        editable: Literal["user", "all"] | None = None,
         latex_delimiters: list[dict[str, str | bool]] | None = None,
         rtl: bool = False,
         show_share_button: bool | None = None,
@@ -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: Allows user to edit a previous message's text in the chatbot. If set to "all", allows the user to edit assistant messages as well.
             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.
@@ -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:
diff --git a/gradio/events.py b/gradio/events.py
index cd9f0838a0..5c84187982 100644
--- a/gradio/events.py
+++ b/gradio/events.py
@@ -371,6 +371,38 @@ def __init__(self, target: Block | None, data: Any):
         """
 
 
+@document()
+class EditData(EventData):
+    """
+    The gr.UndoData class is a subclass of gr.Event data that specifically carries information about the `.edit()` event. When gr.EditData
+    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.
+    The attributes of this object contains information about the event that triggered the listener.
+    Example:
+        import gradio as gr
+
+        def edit(edit_data: gr.EditData, history: list[gr.MessageDict]):
+            history_up_to_edit = history[:edit_data.index]
+            history_up_to_edit[-1] = edit_data.value
+            return history_up_to_edit
+
+        with gr.Blocks() as demo:
+            chatbot = gr.Chatbot()
+            chatbot.undo(edit, chatbot, chatbot)
+        demo.launch()
+    """
+
+    def __init__(self, target: Block | None, data: Any):
+        super().__init__(target, data)
+        self.index: int | tuple[int, int] = data["index"]
+        """
+        The index of the message that was edited.
+        """
+        self.value: Any = data["value"]
+        """
+        The content of the message that was edited.
+        """
+
+
 @document()
 class DownloadData(EventData):
     """
diff --git a/guides/05_chatbots/05_chatbot-specific-events.md b/guides/05_chatbots/05_chatbot-specific-events.md
index b569076927..c22220da08 100644
--- a/guides/05_chatbots/05_chatbot-specific-events.md
+++ b/guides/05_chatbots/05_chatbot-specific-events.md
@@ -105,7 +105,7 @@ def handle_retry(history, retry_data: gr.RetryData):
 
 ...
 
-chatbot.retry(handle_retry, chatbot, [chatbot])
+chatbot.retry(handle_retry, chatbot, chatbot)
 ```
 
 You'll see that the bot messages have a "retry" icon now -
@@ -133,6 +133,22 @@ def handle_like(data: gr.LikeData):
 chatbot.like(vote, None, None)
 ```
 
+## The Edit Event
+
+Same idea with the edit listener! with `gr.Chatbot(editable=True)`, you can capture user edits as such:
+
+```python
+def handle_edit(history, edit_data: gr.EditData):
+    new_history = history[:edit_data.index]
+    new_history[-1]['content'] = edit_data.value
+    return new_history
+
+...
+
+chatbot.edit(handle_edit, chatbot, chatbot)
+```
+
+
 
 ## Conclusion
 
diff --git a/js/chatbot/Chatbot.test.ts b/js/chatbot/Chatbot.test.ts
index c001c716ff..8fe06abbbc 100644
--- a/js/chatbot/Chatbot.test.ts
+++ b/js/chatbot/Chatbot.test.ts
@@ -43,8 +43,8 @@ describe("Chatbot", () => {
 
 		const chatbot = getByRole("log");
 
-		const userButton = container.querySelector(".user button");
-		const botButton = container.querySelector(".bot button");
+		const userButton = container.querySelector(".user > div");
+		const botButton = container.querySelector(".bot > div");
 
 		assert.notExists(userButton);
 		assert.notExists(botButton);
@@ -60,8 +60,8 @@ describe("Chatbot", () => {
 			latex_delimiters: [{ left: "$$", right: "$$", display: true }]
 		});
 
-		const userButton = container.querySelector(".user button");
-		const botButton = container.querySelector(".bot button");
+		const userButton = container.querySelector(".user > div");
+		const botButton = container.querySelector(".bot > div");
 
 		assert.exists(userButton);
 		assert.exists(botButton);
diff --git a/js/chatbot/Index.svelte b/js/chatbot/Index.svelte
index 3a15ac7585..5d00de3171 100644
--- a/js/chatbot/Index.svelte
+++ b/js/chatbot/Index.svelte
@@ -59,6 +59,7 @@
 		clear_status: LoadingStatus;
 		example_select: SelectData;
 		option_select: SelectData;
+		edit: SelectData;
 		retry: UndoRetryData;
 		undo: UndoRetryData;
 		clear: null;
@@ -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: "user" | "all" | null = null;
 	export let placeholder: string | null = null;
 	export let examples: ExampleMessage[] | null = null;
 	export let theme_mode: "system" | "light" | "dark";
@@ -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}
@@ -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}
diff --git a/js/chatbot/shared/ButtonPanel.svelte b/js/chatbot/shared/ButtonPanel.svelte
index 1ddcb10116..10d332a61c 100644
--- a/js/chatbot/shared/ButtonPanel.svelte
+++ b/js/chatbot/shared/ButtonPanel.svelte
@@ -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";
@@ -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>
diff --git a/js/chatbot/shared/ChatBot.svelte b/js/chatbot/shared/ChatBot.svelte
index f28343416f..dcb87dcd1b 100644
--- a/js/chatbot/shared/ChatBot.svelte
+++ b/js/chatbot/shared/ChatBot.svelte
@@ -2,6 +2,7 @@
 	import {
 		format_chat_for_sharing,
 		type UndoRetryData,
+		type EditData,
 		is_last_bot_message,
 		group_messages,
 		load_components,
@@ -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 {
@@ -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";
@@ -69,6 +67,7 @@
 	export let generating = false;
 	export let selectable = false;
 	export let likeable = false;
+	export let editable: "user" | "all" | null = null;
 	export let show_share_button = false;
 	export let show_copy_all_button = false;
 	export let rtl = false;
@@ -91,6 +90,8 @@
 	export let root: string;
 
 	let target: HTMLElement | null = null;
+	let edit_index: number | null = null;
+	let edit_message = "";
 
 	onMount(() => {
 		target = document.querySelector("div.gradio-container");
@@ -104,6 +105,7 @@
 		change: undefined;
 		select: SelectData;
 		like: LikeData;
+		edit: EditData;
 		undo: UndoRetryData;
 		retry: UndoRetryData;
 		clear: undefined;
@@ -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;
@@ -177,14 +173,7 @@
 	$: 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
@@ -201,29 +190,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"
+				});
+			}
 		}
 	}
 
@@ -305,8 +304,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 == "user" &&
+							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)}
diff --git a/js/chatbot/shared/Message.svelte b/js/chatbot/shared/Message.svelte
index 5e4e6f43de..3ad1c001db 100644
--- a/js/chatbot/shared/Message.svelte
+++ b/js/chatbot/shared/Message.svelte
@@ -39,13 +39,27 @@
 	export let show_copy_button: boolean;
 	export let generating: boolean;
 	export let show_like: boolean;
+	export let show_edit: boolean;
 	export let show_retry: boolean;
 	export let show_undo: boolean;
 	export let msg_format: "tuples" | "messages";
 	export let handle_action: (selected: string | null) => void;
 	export let scroll: () => void;
 	export let allow_file_downloads: boolean;
+	export let in_edit_mode: boolean;
+	export let edit_message: string;
 	export let display_consecutive_in_same_bubble: boolean;
+	let messageElements: HTMLDivElement[] = [];
+	let previous_edit_mode = false;
+	let last_message_width = 0;
+	let last_message_height = 0;
+
+	$: if (in_edit_mode && !previous_edit_mode) {
+		last_message_width =
+			messageElements[messageElements.length - 1]?.clientWidth;
+		last_message_height =
+			messageElements[messageElements.length - 1]?.clientHeight;
+	}
 
 	function handle_select(i: number, message: NormalisedMessage): void {
 		dispatch("select", {
@@ -77,6 +91,8 @@
 		likeable: boolean;
 		show_retry: boolean;
 		show_undo: boolean;
+		show_edit: boolean;
+		in_edit_mode: boolean;
 		generating: boolean;
 		show_copy_button: boolean;
 		message: NormalisedMessage[] | NormalisedMessage;
@@ -92,6 +108,8 @@
 		likeable: show_like,
 		show_retry,
 		show_undo,
+		show_edit,
+		in_edit_mode,
 		generating,
 		show_copy_button,
 		message: msg_format === "tuples" ? messages[0] : messages,
@@ -131,29 +149,63 @@
 						message.content.component === "html"}
 					class:thought={thought_index > 0}
 				>
-					<button
-						data-testid={role}
-						class:latest={i === value.length - 1}
-						class:message-markdown-disabled={!render_markdown}
-						style:user-select="text"
-						class:selectable
-						style:cursor={selectable ? "pointer" : "default"}
-						style:text-align={rtl ? "right" : "left"}
-						on:click={() => handle_select(i, message)}
-						on:keydown={(e) => {
-							if (e.key === "Enter") {
-								handle_select(i, message);
-							}
-						}}
-						dir={rtl ? "rtl" : "ltr"}
-						aria-label={role + "'s message: " + get_message_label_data(message)}
-					>
-						{#if message?.metadata?.title}
-							<MessageBox
-								title={message.metadata.title}
-								expanded={is_last_bot_message([message], value)}
-								{rtl}
-							>
+					{#if in_edit_mode && thought_index === messages.length - 1 && message.type === "text"}
+						<!-- svelte-ignore a11y-autofocus -->
+						<textarea
+							class="edit-textarea"
+							style:width={`max(${last_message_width}px, 160px)`}
+							style:min-height={`${last_message_height}px`}
+							autofocus
+							bind:value={edit_message}
+						/>
+					{:else}
+						<!-- svelte-ignore a11y-no-static-element-interactions -->
+						<div
+							data-testid={role}
+							class:latest={i === value.length - 1}
+							class:message-markdown-disabled={!render_markdown}
+							style:user-select="text"
+							class:selectable
+							style:cursor={selectable ? "pointer" : "auto"}
+							style:text-align={rtl ? "right" : "left"}
+							bind:this={messageElements[thought_index]}
+							on:click={() => handle_select(i, message)}
+							on:keydown={(e) => {
+								if (e.key === "Enter") {
+									handle_select(i, message);
+								}
+							}}
+							dir={rtl ? "rtl" : "ltr"}
+							aria-label={role +
+								"'s message: " +
+								get_message_label_data(message)}
+						>
+							{#if message?.metadata?.title}
+								<MessageBox
+									title={message.metadata.title}
+									expanded={is_last_bot_message([message], value)}
+									{rtl}
+								>
+									<MessageContent
+										{message}
+										{sanitize_html}
+										{latex_delimiters}
+										{render_markdown}
+										{_components}
+										{upload}
+										{thought_index}
+										{target}
+										{root}
+										{theme_mode}
+										{_fetch}
+										{scroll}
+										{allow_file_downloads}
+										{display_consecutive_in_same_bubble}
+										{i18n}
+										{line_breaks}
+									/>
+								</MessageBox>
+							{:else}
 								<MessageContent
 									{message}
 									{sanitize_html}
@@ -172,37 +224,19 @@
 									{i18n}
 									{line_breaks}
 								/>
-							</MessageBox>
-						{:else}
-							<MessageContent
-								{message}
-								{sanitize_html}
-								{latex_delimiters}
-								{render_markdown}
-								{_components}
-								{upload}
-								{thought_index}
-								{target}
-								{root}
-								{theme_mode}
-								{_fetch}
-								{scroll}
-								{allow_file_downloads}
-								{display_consecutive_in_same_bubble}
-								{i18n}
-								{line_breaks}
-							/>
-						{/if}
-					</button>
+							{/if}
+						</div>
+					{/if}
 				</div>
+
+				{#if layout === "panel"}
+					<ButtonPanel
+						{...button_panel_props}
+						on:copy={(e) => dispatch("copy", e.detail)}
+					/>
+				{/if}
 			{/each}
 		</div>
-		{#if layout === "panel"}
-			<ButtonPanel
-				{...button_panel_props}
-				on:copy={(e) => dispatch("copy", e.detail)}
-			/>
-		{/if}
 	</div>
 </div>
 
@@ -508,7 +542,7 @@
 		border-radius: var(--radius-lg);
 	}
 
-	.message > button {
+	.message > div {
 		width: 100%;
 	}
 	.html {
@@ -528,6 +562,26 @@
 		background-color: var(--background-fill-secondary);
 	}
 
+	textarea {
+		background: none;
+		border-radius: var(--radius-lg);
+		border: none;
+		display: block;
+		max-width: 100%;
+	}
+	.user textarea {
+		border-bottom-right-radius: 0;
+	}
+	.bot textarea {
+		border-bottom-left-radius: 0;
+	}
+	.user textarea:focus {
+		outline: 2px solid var(--border-color-accent);
+	}
+	.bot textarea:focus {
+		outline: 2px solid var(--border-color-primary);
+	}
+
 	.panel.user-row {
 		background-color: var(--color-accent-soft);
 	}
diff --git a/js/chatbot/shared/utils.ts b/js/chatbot/shared/utils.ts
index e2fea9d548..6a6989743d 100644
--- a/js/chatbot/shared/utils.ts
+++ b/js/chatbot/shared/utils.ts
@@ -66,6 +66,11 @@ export interface UndoRetryData {
 	value: string | FileData | ComponentData;
 }
 
+export interface EditData {
+	index: number | [number, number];
+	value: string;
+}
+
 const redirect_src_url = (src: string, root: string): string =>
 	src.replace('src="/file', `src="${root}file`);
 
diff --git a/js/spa/test/chatbot_editable.spec.ts b/js/spa/test/chatbot_editable.spec.ts
new file mode 100644
index 0000000000..3f333ff4e2
--- /dev/null
+++ b/js/spa/test/chatbot_editable.spec.ts
@@ -0,0 +1,31 @@
+import { test, expect } from "@self/tootils";
+
+test("test editing chatbot messages", async ({ page }) => {
+	await page.getByRole("button", { name: "Add Message" }).click();
+	await expect(page.getByLabel("Concatenated Chat 1")).toHaveValue(
+		"I'm a user|I'm a bot"
+	);
+	await page.getByRole("button", { name: "Add Message" }).click();
+	await expect(page.getByLabel("Concatenated Chat 1")).toHaveValue(
+		"I'm a user|I'm a bot|I'm a user|I'm a bot"
+	);
+	await page.getByLabel("Edit").nth(0).click();
+	await page.locator("textarea").first().fill("GRADIO");
+	await page.getByLabel("Submit").click();
+	await expect(page.getByLabel("Edited Message")).toHaveValue("GRADIO at 0");
+	await page.getByLabel("Edit").nth(2).click();
+	await page.locator("textarea").first().fill("FAIL");
+	await page.getByLabel("Cancel").click();
+	await page.getByLabel("Edit").nth(3).click();
+	await page.locator("textarea").first().fill("SUCCESS");
+	await page.getByLabel("Submit").click();
+	await expect(page.getByLabel("Edited Message")).toHaveValue(
+		"SUCCESS at [1, 0]"
+	);
+	await expect(page.getByLabel("Concatenated Chat 1")).toHaveValue(
+		"GRADIO|I'm a bot|I'm a user|I'm a bot"
+	);
+	await expect(page.getByLabel("Concatenated Chat 2")).toHaveValue(
+		"I'm a user|I'm a bot|SUCCESS|I'm a bot"
+	);
+});
diff --git a/js/spa/test/chatbot_with_tools.spec.ts b/js/spa/test/chatbot_with_tools.spec.ts
index b19f72d1c5..1ecfe1728b 100644
--- a/js/spa/test/chatbot_with_tools.spec.ts
+++ b/js/spa/test/chatbot_with_tools.spec.ts
@@ -10,7 +10,7 @@ test("Chatbot can support agentic demos by displaying messages with metadata", a
 	await expect(
 		page.locator("span").filter({ hasText: "🛠️ Used tool" })
 	).toBeVisible();
-	await expect(
-		page.locator("button").filter({ hasText: "It's a sunny day in San" })
-	).toBeVisible();
+	await expect(page.locator(".bot")).toContainText(
+		"It's a sunny day in San Francisco"
+	);
 });
diff --git a/test/components/test_chatbot.py b/test/components/test_chatbot.py
index 13f5980e43..6d03c38f13 100644
--- a/test/components/test_chatbot.py
+++ b/test/components/test_chatbot.py
@@ -39,6 +39,7 @@ def test_component_functions(self):
             "visible": True,
             "elem_id": None,
             "elem_classes": [],
+            "editable": None,
             "container": True,
             "min_width": 160,
             "group_consecutive_messages": True,


Code review comments:
diff_hunk: @@ -371,6 +371,38 @@ def __init__(self, target: Block | None, data: Any):
         """
 
 
+@document()
+class EditData(EventData):
+    """
+    The gr.UndoData class is a subclass of gr.Event data that specifically carries information about the `.edit()` event. When gr.EditData
path: gradio/events.py
text: @user1:
```suggestion
    The gr.EditData class is a subclass of gr.Event data that specifically carries information about the `.edit()` event. When gr.EditData
```