Repository: gradio-app/gradio
Pull request: Group chatbot messages by default
## Description

Closes: #8850

By default, consecutive messages are displayed in the same bubble. This is controlled by the new `display_consecutive_in_same_bubble` param of Chatbot.

![consecutive_messages](https://github.com/user-attachments/assets/30ef5973-3349-4d9a-85a1-d53c70dbae8f)


## 🎯 PRs Should Target Issues

Before your create a PR, please check to see if there is [an existing issue](https://github.com/gradio-app/gradio/issues) for this change. If not, please create an issue before you create this PR, unless the fix is very small. 

Not adhering to this guideline will result in the PR being closed. 

## Testing and Formatting Your Code

1. PRs will only be merged if tests pass on CI. We recommend at least running the backend tests locally, please set up [your Gradio environment locally](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md) and run the backed tests: `bash scripts/run_backend_tests.sh`

2. Please run these bash scripts to automatically format your code: `bash scripts/format_backend.sh`, and (if you made any changes to non-Python files) `bash scripts/format_frontend.sh`
  


Problem statement:
Be able to control grouping of chatbot messages
- [ ] I have searched to see if a similar issue already exists.


**Is your feature request related to a problem? Please describe.**  
By default, consecutive messages with the same role are grouped in the same bubble. Some people may want to control the grouping behavior / opt out of it.

**Describe the solution you'd like**  


**Additional context**  
Add any other context or screenshots about the feature request here.

Hints from issue discussion:
Consecutive messages are no longer grouped by default as of version 5.0

Commit message: Revert "remove thought css"

This reverts commit f1ea8f88f6ced9dc8e8897abdfd529952f851954.

Patch to review:
diff --git a/.changeset/solid-hands-nail.md b/.changeset/solid-hands-nail.md
new file mode 100644
index 0000000000..d9ee961e3c
--- /dev/null
+++ b/.changeset/solid-hands-nail.md
@@ -0,0 +1,6 @@
+---
+"@gradio/chatbot": minor
+"gradio": minor
+---
+
+feat:By default, consecutive messages are displayed in the same bubble. This is controlled by the new `display_consecutive_in_same_bubble` param of Chatbot.
diff --git a/gradio/components/chatbot.py b/gradio/components/chatbot.py
index 4dcd500d49..96c6ee9c9f 100644
--- a/gradio/components/chatbot.py
+++ b/gradio/components/chatbot.py
@@ -198,6 +198,7 @@ def __init__(
         examples: list[ExampleMessage] | None = None,
         show_copy_all_button=False,
         allow_file_downloads=True,
+        display_consecutive_in_same_bubble: bool = True,
     ):
         """
         Parameters:
@@ -233,6 +234,7 @@ def __init__(
             examples: A list of example messages to display in the chatbot before any user/assistant messages are shown. Each example should be a dictionary with an optional "text" key representing the message that should be populated in the Chatbot when clicked, an optional "files" key, whose value should be a list of files to populate in the Chatbot, an optional "icon" key, whose value should be a filepath or URL to an image to display in the example box, and an optional "display_text" key, whose value should be the text to display in the example box. If "display_text" is not provided, the value of "text" will be displayed.
             show_copy_all_button: If True, will show a copy all button that copies all chatbot messages to the clipboard.
             allow_file_downloads: If True, will show a download button for chatbot messages that contain media. Defaults to True.
+            display_consecutive_in_same_bubble: If True, will display consecutive messages from the same role in the same bubble. If False, will display each message in a separate bubble. Defaults to True.
         """
         if type is None:
             warnings.warn(
@@ -256,6 +258,7 @@ def __init__(
         self.max_height = max_height
         self.min_height = min_height
         self.rtl = rtl
+        self.display_consecutive_in_same_bubble = display_consecutive_in_same_bubble
         if latex_delimiters is None:
             latex_delimiters = [{"left": "$$", "right": "$$", "display": True}]
         self.latex_delimiters = latex_delimiters
diff --git a/js/chatbot/Chatbot.stories.svelte b/js/chatbot/Chatbot.stories.svelte
index c968da32e5..2c1a47eaf4 100644
--- a/js/chatbot/Chatbot.stories.svelte
+++ b/js/chatbot/Chatbot.stories.svelte
@@ -223,3 +223,67 @@ This document is a showcase of various Markdown capabilities.`,
 		]
 	}}
 />
+
+<Story
+	name="Consecutive messages grouped in same bubble"
+	args={{
+		type: "messages",
+		display_consecutive_in_same_bubble: true,
+		value: [
+			{
+				role: "user",
+				content: "Show me the file."
+			},
+			{
+				role: "user",
+				content: "Second user message"
+			},
+			{
+				role: "assistant",
+				content: "Here is the file you requested"
+			},
+			{
+				role: "assistant",
+				content: {
+					file: {
+						path: "abc/qwerty.txt",
+						url: ""
+					},
+					alt_text: null
+				}
+			}
+		]
+	}}
+/>
+
+<Story
+	name="Consecutive messages not grouped in same bubble"
+	args={{
+		type: "messages",
+		display_consecutive_in_same_bubble: false,
+		value: [
+			{
+				role: "user",
+				content: "Show me the file."
+			},
+			{
+				role: "user",
+				content: "Second user message"
+			},
+			{
+				role: "assistant",
+				content: "Here is the file you requested"
+			},
+			{
+				role: "assistant",
+				content: {
+					file: {
+						path: "abc/qwerty.txt",
+						url: ""
+					},
+					alt_text: null
+				}
+			}
+		]
+	}}
+/>
diff --git a/js/chatbot/Index.svelte b/js/chatbot/Index.svelte
index 3ff3572234..4259a1d66e 100644
--- a/js/chatbot/Index.svelte
+++ b/js/chatbot/Index.svelte
@@ -44,6 +44,7 @@
 	export let autoscroll = true;
 	export let _retryable = false;
 	export let _undoable = false;
+	export let display_consecutive_in_same_bubble = true;
 	export let latex_delimiters: {
 		left: string;
 		right: string;
@@ -125,6 +126,7 @@
 			{show_copy_all_button}
 			value={_value}
 			{latex_delimiters}
+			{display_consecutive_in_same_bubble}
 			{render_markdown}
 			{theme_mode}
 			pending_message={loading_status?.status === "pending"}
diff --git a/js/chatbot/shared/ChatBot.svelte b/js/chatbot/shared/ChatBot.svelte
index 99b5a723cb..8b1cd8121c 100644
--- a/js/chatbot/shared/ChatBot.svelte
+++ b/js/chatbot/shared/ChatBot.svelte
@@ -42,6 +42,7 @@
 	export let _fetch: typeof fetch;
 	export let load_component: Gradio["load_component"];
 	export let allow_file_downloads: boolean;
+	export let display_consecutive_in_same_bubble: boolean;
 
 	let _components: Record<string, ComponentType<SvelteComponent>> = {};
 
@@ -276,6 +277,7 @@
 				{@const opposite_avatar_img = avatar_images[role === "user" ? 0 : 1]}
 				<Message
 					{messages}
+					{display_consecutive_in_same_bubble}
 					{opposite_avatar_img}
 					{avatar_img}
 					{role}
diff --git a/js/chatbot/shared/Message.svelte b/js/chatbot/shared/Message.svelte
index 1da382be03..c4296ae0ca 100644
--- a/js/chatbot/shared/Message.svelte
+++ b/js/chatbot/shared/Message.svelte
@@ -45,6 +45,7 @@
 	export let handle_action: (selected: string | null) => void;
 	export let scroll: () => void;
 	export let allow_file_downloads: boolean;
+	export let display_consecutive_in_same_bubble: boolean;
 
 	function handle_select(i: number, message: NormalisedMessage): void {
 		dispatch("select", {
@@ -116,42 +117,55 @@
 		class="flex-wrap"
 		class:component-wrap={messages[0].type === "component"}
 	>
-		{#each messages as message, thought_index}
-			<div
-				class="message {role} {is_component_message(message)
-					? message?.content.component
-					: ''}"
-				class:panel-full-width={true}
-				class:message-markdown-disabled={!render_markdown}
-				class:component={message.type === "component"}
-				class:html={is_component_message(message) &&
-					message.content.component === "html"}
-				class:thought={thought_index > 0}
-			>
-				<button
-					data-testid={role}
-					class:latest={i === value.length - 1}
+		<div
+			class:message={display_consecutive_in_same_bubble}
+			class={display_consecutive_in_same_bubble ? role : ""}
+		>
+			{#each messages as message, thought_index}
+				<div
+					class="message {!display_consecutive_in_same_bubble ? role : ''}"
+					class:panel-full-width={true}
 					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)}
+					class:component={message.type === "component"}
+					class:html={is_component_message(message) &&
+						message.content.component === "html"}
+					class:thought={thought_index > 0}
 				>
-					{#if message.type === "text"}
-						<div class="message-content">
-							{#if message.metadata.title}
-								<MessageBox
-									title={message.metadata.title}
-									expanded={is_last_bot_message([message], value)}
-								>
+					<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.type === "text"}
+							<div class="message-content">
+								{#if message?.metadata?.title}
+									<MessageBox
+										title={message.metadata.title}
+										expanded={is_last_bot_message([message], value)}
+									>
+										<Markdown
+											message={message.content}
+											{latex_delimiters}
+											{sanitize_html}
+											{render_markdown}
+											{line_breaks}
+											on:load={scroll}
+											{root}
+										/>
+									</MessageBox>
+								{:else}
 									<Markdown
 										message={message.content}
 										{latex_delimiters}
@@ -161,79 +175,68 @@
 										on:load={scroll}
 										{root}
 									/>
-								</MessageBox>
-							{:else}
-								<Markdown
-									message={message.content}
-									{latex_delimiters}
-									{sanitize_html}
-									{render_markdown}
-									{line_breaks}
-									on:load={scroll}
-									{root}
-								/>
-							{/if}
-						</div>
-					{:else if message.type === "component" && message.content.component in _components}
-						<Component
-							{target}
-							{theme_mode}
-							props={message.content.props}
-							type={message.content.component}
-							components={_components}
-							value={message.content.value}
-							{i18n}
-							{upload}
-							{_fetch}
-							on:load={() => scroll()}
-							{allow_file_downloads}
-						/>
-					{:else if message.type === "component" && message.content.component === "file"}
-						<div class="file-container">
-							<div class="file-icon">
-								<File />
+								{/if}
 							</div>
-							<div class="file-info">
-								<a
-									data-testid="chatbot-file"
-									class="file-link"
-									href={message.content.value.url}
-									target="_blank"
-									download={window.__is_colab__
-										? null
-										: message.content.value?.orig_name ||
-											message.content.value?.path.split("/").pop() ||
-											"file"}
-								>
-									<span class="file-name"
-										>{message.content.value?.orig_name ||
-											message.content.value?.path.split("/").pop() ||
-											"file"}</span
+						{:else if message.type === "component" && message.content.component in _components}
+							<Component
+								{target}
+								{theme_mode}
+								props={message.content.props}
+								type={message.content.component}
+								components={_components}
+								value={message.content.value}
+								{i18n}
+								{upload}
+								{_fetch}
+								on:load={() => scroll()}
+								{allow_file_downloads}
+							/>
+						{:else if message.type === "component" && message.content.component === "file"}
+							<div class="file-container">
+								<div class="file-icon">
+									<File />
+								</div>
+								<div class="file-info">
+									<a
+										data-testid="chatbot-file"
+										class="file-link"
+										href={message.content.value.url}
+										target="_blank"
+										download={window.__is_colab__
+											? null
+											: message.content.value?.orig_name ||
+												message.content.value?.path.split("/").pop() ||
+												"file"}
+									>
+										<span class="file-name"
+											>{message.content.value?.orig_name ||
+												message.content.value?.path.split("/").pop() ||
+												"file"}</span
+										>
+									</a>
+									<span class="file-type"
+										>{(
+											message.content.value?.orig_name ||
+											message.content.value?.path ||
+											""
+										)
+											.split(".")
+											.pop()
+											.toUpperCase()}</span
 									>
-								</a>
-								<span class="file-type"
-									>{(
-										message.content.value?.orig_name ||
-										message.content.value?.path ||
-										""
-									)
-										.split(".")
-										.pop()
-										.toUpperCase()}</span
-								>
+								</div>
 							</div>
-						</div>
-					{/if}
-				</button>
-			</div>
-
-			{#if layout === "panel"}
-				<ButtonPanel
-					{...button_panel_props}
-					on:copy={(e) => dispatch("copy", e.detail)}
-				/>
-			{/if}
-		{/each}
+						{/if}
+					</button>
+				</div>
+			{/each}
+		</div>
+		{#if layout === "panel"}
+			<ButtonPanel
+				{...button_panel_props}
+				on:copy={(e) => dispatch("copy", e.detail)}
+			/>
+		{/if}
 	</div>
 </div>
 
diff --git a/test/components/test_chatbot.py b/test/components/test_chatbot.py
index 79c9dc0539..fec400216f 100644
--- a/test/components/test_chatbot.py
+++ b/test/components/test_chatbot.py
@@ -41,6 +41,7 @@ def test_component_functions(self):
             "elem_classes": [],
             "container": True,
             "min_width": 160,
+            "display_consecutive_in_same_bubble": True,
             "scale": None,
             "placeholder": None,
             "height": 400,


Code review comments:
diff_hunk: @@ -198,6 +198,7 @@ def __init__(
         examples: list[ExampleMessage] | None = None,
         show_copy_all_button=False,
         allow_file_downloads=True,
+        display_consecutive_in_same_bubble: bool = True,
path: gradio/components/chatbot.py
text: @user1:
Nit: the parameter name is kinda long. How about `group_consecutive_messages`?