Repository: gradio-app/gradio
Pull request: Adds a watermark parameter to `gr.Chatbot` that is added to copied text
In the spirit of letting developers easily build "good ml apps" that follow relevant regulations (see https://github.com/gradio-app/gradio/issues/10756), this PR adds a `watermark` parameter to `gr.Chatbot` analogous to https://github.com/gradio-app/gradio/pull/8900

Closes: https://github.com/gradio-app/gradio/issues/10756

Problem statement:
Add a `watermark` parameter to `gr.Chatbot` that is added to copied text 
When a user copies the text generated by the LLM, this triggers the event ChatBot.copy, which is great. But the listener only gives you the copied text, and does not let you modify it.

In order to comply with the EU AI Act, chatbots should give a kind of watermark on generated content. For text, a simple "Generated by AI" somewhere in the text is enough.

I tried something like this:

```python
        def add_ai_tag(message):
            message[0][1] = message[0][1] + "Generated by AI"
            return message
    
        chatbot.copy(add_ai_tag, chatbot, chatbot)
``` 

But this changes the text on the chatbot dialogue itself, and not the text being sent to the clipboard, making me believe that Chatbox.copy happens after the copy has happened, or I am clearly doing something silly and I couldn't understand the documentation (which is pretty difficult on the event listeners part).

Any hints are welcome, and an example on the documentation would be super useful (as everyone needs to do that in the EU now).

Hints from issue discussion:
Hi @surak yes it's not possible to modify the clipboard value in the `.copy()` method. I suppose you could do something like this:

```py
import gradio as gr

value = """
Hi <span style="display:none">Generated by AI</span>
"""

with gr.Blocks() as demo:
    c = gr.Chatbot([(value, value)], show_copy_button=True)

demo.launch()
```

But we could potentially include add a `copy_watermark` parameter in `gr.Chatbot` to get the nicer behavior you described automatically. Can you elaborate more on what the EU AI Act requires or link to a relevant explainer?
So, the EU AI act states (I can find the exact reference if needed) that all AI generated product should be clearly labeled as so. 

In the case of a chatbot, an explanation in the website that the chatbot is AI and not a person is enough. But when you take this chatbot's output out of the website and into somewhere else, you need a "watermark". Think of the little dall-e thing on the lower left corner of their images. But in the case of text, anything saying that the text was generated by AI is enough. It's then up to the user to remove it, which takes the liability to the user and not to the system.

If you need the exact links, I can find them with my lawyer.
Cool that's sufficiently clear, we can add this

Commit message: test

Patch to review:
diff --git a/.changeset/fifty-yaks-trade.md b/.changeset/fifty-yaks-trade.md
new file mode 100644
index 0000000000..64dc245556
--- /dev/null
+++ b/.changeset/fifty-yaks-trade.md
@@ -0,0 +1,6 @@
+---
+"@gradio/chatbot": minor
+"gradio": minor
+---
+
+feat:Adds a watermark parameter to `gr.Chatbot` that is added to copied text
diff --git a/gradio/components/chatbot.py b/gradio/components/chatbot.py
index e96f379a5a..6ef738f303 100644
--- a/gradio/components/chatbot.py
+++ b/gradio/components/chatbot.py
@@ -225,6 +225,7 @@ def __init__(
         rtl: bool = False,
         show_share_button: bool | None = None,
         show_copy_button: bool = False,
+        watermark: str | None = None,  # New parameter for watermark text
         avatar_images: tuple[str | Path | None, str | Path | None] | None = None,
         sanitize_html: bool = True,
         render_markdown: bool = True,
@@ -266,6 +267,7 @@ def __init__(
             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.
             show_copy_button: If True, will show a copy button for each chatbot message.
+            watermark: If provided, this text will be appended to the end of copied messages. Useful for indicating that the message is generated by an AI model.
             avatar_images: Tuple of two avatar image paths or URLs for user and bot (in that order). Pass None for either the user or bot image to skip. Must be within the working directory of the Gradio app or an external URL.
             sanitize_html: If False, will disable HTML sanitization for chatbot messages. This is not recommended, as it can lead to security vulnerabilities.
             render_markdown: If False, will disable Markdown rendering for chatbot messages.
@@ -325,6 +327,7 @@ def __init__(
         )
         self.render_markdown = render_markdown
         self.show_copy_button = show_copy_button
+        self.watermark = watermark
         self.sanitize_html = sanitize_html
         if bubble_full_width is not None:
             warnings.warn(
diff --git a/js/chatbot/Index.svelte b/js/chatbot/Index.svelte
index 612ac8cb7e..296cc3668b 100644
--- a/js/chatbot/Index.svelte
+++ b/js/chatbot/Index.svelte
@@ -88,6 +88,7 @@
 	export let examples: ExampleMessage[] | null = null;
 	export let theme_mode: "system" | "light" | "dark";
 	export let allow_file_downloads = true;
+	export let watermark: string | null = null;
 </script>
 
 <Block
@@ -186,6 +187,7 @@
 			root={gradio.root}
 			{allow_file_downloads}
 			{allow_tags}
+			{watermark}
 		/>
 	</div>
 </Block>
diff --git a/js/chatbot/shared/ButtonPanel.svelte b/js/chatbot/shared/ButtonPanel.svelte
index f9d2d2f18b..2a82a7f7e1 100644
--- a/js/chatbot/shared/ButtonPanel.svelte
+++ b/js/chatbot/shared/ButtonPanel.svelte
@@ -14,6 +14,7 @@
 	export let show_edit: boolean;
 	export let in_edit_mode: boolean;
 	export let show_copy_button: boolean;
+	export let watermark: string | null = null;
 	export let message: NormalisedMessage | NormalisedMessage[];
 	export let position: "right" | "left";
 	export let avatar: FileData | null;
@@ -52,6 +53,7 @@
 					<Copy
 						value={message_text}
 						on:copy={(e) => dispatch("copy", e.detail)}
+						{watermark}
 					/>
 				{/if}
 				{#if show_retry}
diff --git a/js/chatbot/shared/ChatBot.svelte b/js/chatbot/shared/ChatBot.svelte
index e77593a92d..5b21553b09 100644
--- a/js/chatbot/shared/ChatBot.svelte
+++ b/js/chatbot/shared/ChatBot.svelte
@@ -91,6 +91,7 @@
 	export let like_user_message = false;
 	export let root: string;
 	export let allow_tags: string[] | boolean = false;
+	export let watermark: string | null = null;
 
 	let target: HTMLElement | null = null;
 	let edit_index: number | null = null;
@@ -268,7 +269,7 @@
 		<IconButton Icon={Trash} on:click={() => dispatch("clear")} label={"Clear"}
 		></IconButton>
 		{#if show_copy_all_button}
-			<CopyAll {value} />
+			<CopyAll {value} {watermark} />
 		{/if}
 	</IconButtonWrapper>
 {/if}
@@ -321,6 +322,7 @@
 					{feedback_options}
 					{current_feedback}
 					{allow_tags}
+					{watermark}
 					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)}
diff --git a/js/chatbot/shared/Copy.svelte b/js/chatbot/shared/Copy.svelte
index 9e5801f077..306df11f07 100644
--- a/js/chatbot/shared/Copy.svelte
+++ b/js/chatbot/shared/Copy.svelte
@@ -11,6 +11,7 @@
 
 	let copied = false;
 	export let value: string;
+	export let watermark: string | null = null;
 	let timer: NodeJS.Timeout;
 
 	function copy_feedback(): void {
@@ -24,11 +25,13 @@
 	async function handle_copy(): Promise<void> {
 		if ("clipboard" in navigator) {
 			dispatch("copy", { value: value });
-			await navigator.clipboard.writeText(value);
+			const text_to_copy = watermark ? `${value}\n\n${watermark}` : value;
+			await navigator.clipboard.writeText(text_to_copy);
 			copy_feedback();
 		} else {
 			const textArea = document.createElement("textarea");
-			textArea.value = value;
+			const text_to_copy = watermark ? `${value}\n\n${watermark}` : value;
+			textArea.value = text_to_copy;
 
 			textArea.style.position = "absolute";
 			textArea.style.left = "-999999px";
diff --git a/js/chatbot/shared/CopyAll.svelte b/js/chatbot/shared/CopyAll.svelte
index aa05eecb05..e5a701289f 100644
--- a/js/chatbot/shared/CopyAll.svelte
+++ b/js/chatbot/shared/CopyAll.svelte
@@ -6,6 +6,7 @@
 
 	let copied = false;
 	export let value: NormalisedMessage[] | null;
+	export let watermark: string | null = null;
 
 	let timer: NodeJS.Timeout;
 
@@ -28,7 +29,11 @@
 				})
 				.join("\n\n");
 
-			navigator.clipboard.writeText(conversation_value).catch((err) => {
+			const text_to_copy = watermark
+				? `${conversation_value}\n\n${watermark}`
+				: conversation_value;
+
+			navigator.clipboard.writeText(text_to_copy).catch((err) => {
 				console.error("Failed to copy conversation: ", err);
 			});
 		}
diff --git a/js/chatbot/shared/Message.svelte b/js/chatbot/shared/Message.svelte
index f90aae923a..546623bbcd 100644
--- a/js/chatbot/shared/Message.svelte
+++ b/js/chatbot/shared/Message.svelte
@@ -51,6 +51,7 @@
 	export let display_consecutive_in_same_bubble: boolean;
 	export let current_feedback: string | null = null;
 	export let allow_tags: string[] | boolean = false;
+	export let watermark: string | null = null;
 	let messageElements: HTMLDivElement[] = [];
 	let previous_edit_mode = false;
 	let last_message_width = 0;
@@ -236,6 +237,7 @@
 					<ButtonPanel
 						{...button_panel_props}
 						{current_feedback}
+						{watermark}
 						on:copy={(e) => dispatch("copy", e.detail)}
 					/>
 				{/if}
diff --git a/test/components/test_chatbot.py b/test/components/test_chatbot.py
index f6bd22614f..21cf1930e2 100644
--- a/test/components/test_chatbot.py
+++ b/test/components/test_chatbot.py
@@ -72,6 +72,7 @@ def test_component_functions(self):
             "allow_tags": False,
             "show_copy_all_button": False,
             "examples": None,
+            "watermark": None,
         }
 
     def test_avatar_images_are_moved_to_cache(self):


Code review comments:
diff_hunk: @@ -225,6 +225,7 @@ def __init__(
         rtl: bool = False,
         show_share_button: bool | None = None,
         show_copy_button: bool = False,
+        watermark: str | None = None,  # New parameter for watermark text
path: gradio/components/chatbot.py
text: @user1:
remove comment