Repository: gradio-app/gradio

Issue:
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).

Discussion:
@abidlabs: 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?@surak: 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.@abidlabs: Cool that's sufficiently clear, we can add this@learnasteve: Would love the watermark parameter to be added to gr.Textbox too!@abidlabs: @learnasteve please feel free to create an issue!

Solution patch:
diff --git a/.changeset/fifty-yaks-trade.md b/.changeset/fifty-yaks-trade.md
new file mode 100644
index 000000000..64dc24555
--- /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 e96f379a5..8bbf6f18f 100644
--- a/gradio/components/chatbot.py
+++ b/gradio/components/chatbot.py
@@ -225,6 +225,7 @@ class Chatbot(Component):
         rtl: bool = False,
         show_share_button: bool | None = None,
         show_copy_button: bool = False,
+        watermark: str | None = None,
         avatar_images: tuple[str | Path | None, str | Path | None] | None = None,
         sanitize_html: bool = True,
         render_markdown: bool = True,
@@ -266,6 +267,7 @@ class Chatbot(Component):
             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 messages copied from the chatbot, after a blank line. 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 @@ class Chatbot(Component):
         )
         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 612ac8cb7..296cc3668 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 f9d2d2f18..2a82a7f7e 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 e77593a92..5b21553b0 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 9e5801f07..306df11f0 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 aa05eecb0..e5a701289 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 f90aae923..95c40777f 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;
@@ -104,6 +105,7 @@
 		avatar: FileData | null;
 		dispatch: any;
 		current_feedback: string | null;
+		watermark: string | null;
 	};
 
 	let button_panel_props: ButtonPanelProps;
@@ -122,7 +124,8 @@
 		avatar: avatar_img,
 		layout,
 		dispatch,
-		current_feedback
+		current_feedback,
+		watermark
 	};
 </script>
 
@@ -236,6 +239,7 @@
 					<ButtonPanel
 						{...button_panel_props}
 						{current_feedback}
+						{watermark}
 						on:copy={(e) => dispatch("copy", e.detail)}
 					/>
 				{/if}


Test patch:
diff --git a/test/components/test_chatbot.py b/test/components/test_chatbot.py
index f6bd22614..21cf1930e 100644
--- a/test/components/test_chatbot.py
+++ b/test/components/test_chatbot.py
@@ -72,6 +72,7 @@ class TestChatbot:
             "allow_tags": False,
             "show_copy_all_button": False,
             "examples": None,
+            "watermark": None,
         }
 
     def test_avatar_images_are_moved_to_cache(self):