Allow Chatbot examples to show more than one image (#10111)

* change example image limit to 4

* add changeset

* refactor examples

* redesign examples

* aria improvements

* check for multimodal

* use multimedia icons

* clean up

* remove min-height

* add param

* fix test

* Revert "add param"

This reverts commit f858097240c3615c19650b5d5263c70d6a408fac.

* Revert "fix test"

This reverts commit 41570fa1004965479e7692ee83dd33b1d4c709b0.

* handle icon in chat_interface.py

* remove unused css class

* fix types

* format

* fix test

* - change logic to use example icon for text example if exists
- ensure mixed files can be displayed in multiples

* add stories

* tweaks

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
Hannah 2024-12-13 14:17:48 +00:00 committed by GitHub
parent 20b9d72ebb
commit 3665e81b53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 491 additions and 140 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/chatbot": minor
"gradio": minor
---
feat:Allow Chatbot examples to show more than one image

View File

@ -319,8 +319,8 @@ class ChatInterface(Blocks):
self.show_progress = show_progress
self._setup_events()
@staticmethod
def _setup_example_messages(
self,
examples: list[str] | list[MultimodalValue] | list[list] | None,
example_labels: list[str] | None = None,
example_icons: list[str] | None = None,
@ -338,8 +338,21 @@ class ChatInterface(Blocks):
example_message["files"] = example.get("files", [])
if example_labels:
example_message["display_text"] = example_labels[index]
if example_icons:
example_message["icon"] = example_icons[index]
if self.multimodal:
example_files = example_message.get("files")
if not example_files:
if example_icons:
example_message["icon"] = example_icons[index]
else:
example_message["icon"] = {
"path": "",
"url": None,
"orig_name": None,
"mime_type": "text", # for internal use, not a valid mime type
"meta": {"_type": "gradio.FileData"},
}
elif example_icons:
example_message["icon"] = example_icons[index]
examples_messages.append(example_message)
return examples_messages

View File

@ -223,3 +223,138 @@ This document is a showcase of various Markdown capabilities.`,
]
}}
/>
<Story
name="MultimodalChatbot with examples"
args={{
value: [],
examples: [
{
text: "What is machine learning?",
icon: { mime_type: "text" }
},
{
text: "Analyze this image",
files: [
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "bus.png"
}
]
},
{
text: "Process this document",
files: [
{
mime_type: "application/pdf",
url: "/document.pdf",
orig_name: "document.pdf"
}
]
},
{
text: "Compare these images",
files: [
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "image1.jpg"
},
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "image2.jpg"
},
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "image3.jpg"
}
]
},
{
text: "Process these files",
files: [
{
mime_type: "application/pdf",
url: "doc1.pdf",
orig_name: "document1.pdf"
},
{
mime_type: "application/pdf",
url: "/doc2.pdf",
orig_name: "document2.pdf"
},
{
mime_type: "application/pdf",
url: "/doc3.pdf",
orig_name: "document3.pdf"
}
]
},
{
text: "Analyze this dataset",
files: [
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "visualization.jpg"
},
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "visualization.jpg"
},
{
mime_type: "application/pdf",
url: "/data.pdf",
orig_name: "data.pdf"
},
{
mime_type: "audio/mp3",
url: "/audio.mp3",
orig_name: "recording.mp3"
},
{
mime_type: "video/mp4",
url: "/video.mp4",
orig_name: "video.mp4"
}
]
}
]
}}
/>
<Story
name="Chatbot with examples (not multimodal)"
args={{
value: [],
examples: [
{
text: "What is machine learning?"
},
{
text: "Analyze this image",
files: [
{
mime_type: "image/jpeg",
url: "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
orig_name: "bus.png"
}
]
},
{
text: "Process this document",
files: [
{
mime_type: "application/pdf",
url: "/document.pdf",
orig_name: "document.pdf"
}
]
}
]
}}
/>

View File

@ -34,6 +34,8 @@
import { ShareError } from "@gradio/utils";
import { Gradio } from "@gradio/utils";
import Examples from "./Examples.svelte";
export let value: NormalisedMessage[] | null = [];
let old_value: NormalisedMessage[] | null = null;
@ -328,55 +330,13 @@
{/if}
</div>
{:else}
<div class="placeholder-content">
{#if placeholder !== null}
<div class="placeholder">
<Markdown message={placeholder} {latex_delimiters} {root} />
</div>
{/if}
{#if examples !== null}
<div class="examples">
{#each examples as example, i}
<button
class="example"
on:click={() => handle_example_select(i, example)}
>
{#if example.icon !== undefined}
<div class="example-icon-container">
<Image
class="example-icon"
src={example.icon.url}
alt="example-icon"
/>
</div>
{/if}
{#if example.display_text !== undefined}
<span class="example-display-text">{example.display_text}</span>
{:else}
<span class="example-text">{example.text}</span>
{/if}
{#if example.files !== undefined && example.files.length > 1}
<span class="example-file"
><em>{example.files.length} Files</em></span
>
{:else if example.files !== undefined && example.files[0] !== undefined && example.files[0].mime_type?.includes("image")}
<div class="example-image-container">
<Image
class="example-image"
src={example.files[0].url}
alt="example-image"
/>
</div>
{:else if example.files !== undefined && example.files[0] !== undefined}
<span class="example-file"
><em>{example.files[0].orig_name}</em></span
>
{/if}
</button>
{/each}
</div>
{/if}
</div>
<Examples
{examples}
{placeholder}
{latex_delimiters}
{root}
on:example_select={(e) => dispatch("example_select", e.detail)}
/>
{/if}
</div>
@ -392,93 +352,6 @@
{/if}
<style>
.placeholder-content {
display: flex;
flex-direction: column;
height: 100%;
}
.placeholder {
align-items: center;
display: flex;
justify-content: center;
height: 100%;
flex-grow: 1;
}
.examples :global(img) {
pointer-events: none;
}
.examples {
margin: auto;
padding: var(--spacing-xxl);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-xxl);
max-width: calc(min(4 * 200px + 5 * var(--spacing-xxl), 100%));
}
.example {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--spacing-xl);
border: 0.05px solid var(--border-color-primary);
border-radius: var(--radius-md);
background-color: var(--background-fill-secondary);
cursor: pointer;
transition: var(--button-transition);
max-width: var(--size-56);
width: 100%;
justify-content: center;
}
.example:hover {
background-color: var(--color-accent-soft);
border-color: var(--border-color-accent);
}
.example-icon-container {
display: flex;
align-self: flex-start;
margin-left: var(--spacing-md);
width: var(--size-6);
height: var(--size-6);
}
.example-display-text,
.example-text,
.example-file {
font-size: var(--text-md);
width: 100%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
}
.example-display-text,
.example-file {
margin-top: var(--spacing-md);
}
.example-image-container {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
margin-top: var(--spacing-xl);
}
.example-image-container :global(img) {
max-height: 100%;
max-width: 100%;
height: var(--size-32);
width: 100%;
object-fit: cover;
border-radius: var(--radius-xl);
}
.panel-wrap {
width: 100%;
overflow-y: auto;

View File

@ -0,0 +1,324 @@
<script lang="ts">
import { Image } from "@gradio/image/shared";
import { MarkdownCode as Markdown } from "@gradio/markdown-code";
import { File, Music, Video } from "@gradio/icons";
import type { ExampleMessage } from "../types";
import { createEventDispatcher } from "svelte";
import type { SelectData } from "@gradio/utils";
import type { FileData } from "@gradio/client";
export let examples: ExampleMessage[] | null = null;
export let placeholder: string | null = null;
export let latex_delimiters: {
left: string;
right: string;
display: boolean;
}[];
export let root: string;
const dispatch = createEventDispatcher<{
example_select: SelectData;
}>();
function handle_example_select(
i: number,
example: ExampleMessage | string
): void {
const example_obj =
typeof example === "string" ? { text: example } : example;
dispatch("example_select", {
index: i,
value: { text: example_obj.text, files: example_obj.files }
});
}
</script>
<div class="placeholder-content" role="complementary">
{#if placeholder !== null}
<div class="placeholder">
<Markdown message={placeholder} {latex_delimiters} {root} />
</div>
{/if}
{#if examples !== null}
<div class="examples" role="list">
{#each examples as example, i}
<button
class="example"
on:click={() =>
handle_example_select(
i,
typeof example === "string" ? { text: example } : example
)}
aria-label={`Select example ${i + 1}: ${example.display_text || example.text}`}
>
<div class="example-content">
{#if example?.icon?.url}
<div class="example-image-container">
<Image
class="example-image"
src={example.icon.url}
alt="Example icon"
/>
</div>
{:else if example?.icon?.mime_type === "text"}
<div class="example-icon" aria-hidden="true">
<span class="text-icon-aa">Aa</span>
</div>
{:else if example.files !== undefined && example.files.length > 0}
{#if example.files.length > 1}
<div
class="example-icons-grid"
role="group"
aria-label="Example attachments"
>
{#each example.files.slice(0, 4) as file, i}
{#if file.mime_type?.includes("image")}
<div class="example-image-container">
<Image
class="example-image"
src={file.url}
alt={file.orig_name || `Example image ${i + 1}`}
/>
{#if i === 3 && example.files.length > 4}
<div
class="image-overlay"
role="status"
aria-label={`${example.files.length - 4} more files`}
>
+{example.files.length - 4}
</div>
{/if}
</div>
{:else if file.mime_type?.includes("video")}
<div class="example-image-container">
<video
class="example-image"
src={file.url}
aria-hidden="true"
/>
{#if i === 3 && example.files.length > 4}
<div
class="image-overlay"
role="status"
aria-label={`${example.files.length - 4} more files`}
>
+{example.files.length - 4}
</div>
{/if}
</div>
{:else}
<div
class="example-icon"
aria-label={`File: ${file.orig_name}`}
>
{#if file.mime_type?.includes("audio")}
<Music />
{:else}
<File />
{/if}
</div>
{/if}
{/each}
{#if example.files.length > 4}
<div class="example-icon">
<div
class="file-overlay"
role="status"
aria-label={`${example.files.length - 4} more files`}
>
+{example.files.length - 4}
</div>
</div>
{/if}
</div>
{:else if example.files[0].mime_type?.includes("image")}
<div class="example-image-container">
<Image
class="example-image"
src={example.files[0].url}
alt={example.files[0].orig_name || "Example image"}
/>
</div>
{:else if example.files[0].mime_type?.includes("video")}
<div class="example-image-container">
<video
class="example-image"
src={example.files[0].url}
aria-hidden="true"
/>
</div>
{:else if example.files[0].mime_type?.includes("audio")}
<div
class="example-icon"
aria-label={`File: ${example.files[0].orig_name}`}
>
<Music />
</div>
{:else}
<div
class="example-icon"
aria-label={`File: ${example.files[0].orig_name}`}
>
<File />
</div>
{/if}
{/if}
<div class="example-text-content">
<span class="example-text"
>{example.display_text || example.text}</span
>
</div>
</div>
</button>
{/each}
</div>
{/if}
</div>
<style>
.placeholder-content {
display: flex;
flex-direction: column;
height: 100%;
}
.placeholder {
align-items: center;
display: flex;
justify-content: center;
height: 100%;
flex-grow: 1;
}
.examples :global(img) {
pointer-events: none;
}
.examples {
margin: auto;
padding: var(--spacing-xxl);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--spacing-xl);
max-width: calc(min(4 * 240px + 5 * var(--spacing-xxl), 100%));
}
.example {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: var(--spacing-xxl);
border: none;
border-radius: var(--radius-lg);
background-color: var(--block-background-fill);
cursor: pointer;
transition: all 150ms ease-in-out;
width: 100%;
gap: var(--spacing-sm);
border: var(--block-border-width) solid var(--block-border-color);
transform: translateY(0px);
}
.example:hover {
transform: translateY(-2px);
background-color: var(--color-accent-soft);
}
.example-content {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
height: 100%;
}
.example-text-content {
margin-top: auto;
text-align: left;
}
.example-text {
font-size: var(--text-md);
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.example-icons-grid {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
width: 100%;
}
.example-icon {
flex-shrink: 0;
width: var(--size-8);
height: var(--size-8);
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
border: var(--block-border-width) solid var(--block-border-color);
background-color: var(--block-background-fill);
position: relative;
}
.example-icon :global(svg) {
width: var(--size-4);
height: var(--size-4);
color: var(--color-text-secondary);
}
.text-icon-aa {
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
color: var(--color-text-secondary);
line-height: 1;
}
.example-image-container {
width: var(--size-8);
height: var(--size-8);
border-radius: var(--radius-lg);
overflow: hidden;
position: relative;
margin-bottom: var(--spacing-lg);
}
.example-image-container :global(img) {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
border-radius: var(--radius-lg);
}
.file-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
border-radius: var(--radius-lg);
}
</style>

View File

@ -20,7 +20,7 @@ for (const test_case of cases) {
// Clear the chat and click on a different example, the input/response are correct
await page.getByLabel("Clear").click();
await page.getByRole("button", { name: "hola example-image" }).click();
await page.getByRole("button", { name: "Select example 2: hola" }).click();
await expect(page.locator(".user img")).toBeVisible();
await expect(page.locator(".user p")).toContainText("hola");
await expect(page.locator(".bot p")).toContainText(