Add allow_file_downloads param to allow downloading image/video/audio media in chatbot (#9905)

* add allow_file_downloads param

* add download btn for markdown images

* add changeset

* tweak

* fix test

* remove param

* revert param removal

* fix show logic

* rename show_download_button to allow_file_downloads

* change default to True

---------

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-11-15 23:50:32 +00:00 committed by GitHub
parent da6f191554
commit 08f4b8b000
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 63 additions and 63 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/chatbot": patch
"gradio": patch
---
fix:Add `allow_file_downloads` param to allow downloading image/video/audio media in chatbot

View File

@ -184,6 +184,7 @@ class Chatbot(Component):
placeholder: str | None = None, placeholder: str | None = None,
examples: list[ExampleMessage] | None = None, examples: list[ExampleMessage] | None = None,
show_copy_all_button=False, show_copy_all_button=False,
allow_file_downloads=True,
): ):
""" """
Parameters: Parameters:
@ -218,6 +219,7 @@ class Chatbot(Component):
placeholder: a placeholder message to display in the chatbot when it is empty. Centered vertically and horizontally in the Chatbot. Supports Markdown and HTML. If None, no placeholder is displayed. placeholder: a placeholder message to display in the chatbot when it is empty. Centered vertically and horizontally in the Chatbot. Supports Markdown and HTML. If None, no placeholder is displayed.
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. 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. 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.
""" """
if type is None: if type is None:
warnings.warn( warnings.warn(
@ -259,6 +261,7 @@ class Chatbot(Component):
self.line_breaks = line_breaks self.line_breaks = line_breaks
self.layout = layout self.layout = layout
self.show_copy_all_button = show_copy_all_button self.show_copy_all_button = show_copy_all_button
self.allow_file_downloads = allow_file_downloads
super().__init__( super().__init__(
label=label, label=label,
every=every, every=every,

View File

@ -58,7 +58,7 @@
<Story <Story
name="Chatbot with math disabled, small height" name="Chatbot with math disabled, small height"
args={{ latex_delimiters: [], height: 200 }} args={{ latex_delimiters: [], height: 200, show_copy_button: false }}
/> />
<Story <Story
@ -85,24 +85,10 @@
/> />
<Story <Story
name="Chatbot with copy button" name="Chatbot with chat bubble full width disabled and copy button"
args={{
show_copy_button: true
}}
/>
<Story
name="Chatbot with chat bubble full width disabled"
args={{
bubble_full_width: false
}}
/>
<Story
name="Chatbot with panel layout enabled"
args={{ args={{
bubble_full_width: false, bubble_full_width: false,
layout: "panel" show_copy_button: true
}} }}
/> />

View File

@ -79,6 +79,7 @@
export let placeholder: string | null = null; export let placeholder: string | null = null;
export let examples: ExampleMessage[] | null = null; export let examples: ExampleMessage[] | null = null;
export let theme_mode: "system" | "light" | "dark"; export let theme_mode: "system" | "light" | "dark";
export let allow_file_downloads = true;
</script> </script>
<Block <Block
@ -157,6 +158,7 @@
load_component={gradio.load_component} load_component={gradio.load_component}
msg_format={type} msg_format={type}
root={gradio.root} root={gradio.root}
{allow_file_downloads}
/> />
</div> </div>
</Block> </Block>

View File

@ -13,7 +13,6 @@
export let show_retry: boolean; export let show_retry: boolean;
export let show_undo: boolean; export let show_undo: boolean;
export let show_copy_button: boolean; export let show_copy_button: boolean;
export let show: boolean;
export let message: NormalisedMessage | NormalisedMessage[]; export let message: NormalisedMessage | NormalisedMessage[];
export let position: "right" | "left"; export let position: "right" | "left";
export let avatar: FileData | null; export let avatar: FileData | null;
@ -48,7 +47,7 @@
message.content.value?.url; message.content.value?.url;
</script> </script>
{#if show} {#if show_copy || show_retry || show_undo || likeable}
<div <div
class="message-buttons-{position} {layout} message-buttons {avatar !== class="message-buttons-{position} {layout} message-buttons {avatar !==
null && 'with-avatar'}" null && 'with-avatar'}"
@ -57,14 +56,6 @@
{#if show_copy} {#if show_copy}
<Copy value={message_text} /> <Copy value={message_text} />
{/if} {/if}
{#if show_download && !Array.isArray(message) && is_component_message(message)}
<DownloadLink
href={message?.content?.value.url}
download={message.content.value.orig_name || "image"}
>
<IconButton Icon={DownloadIcon} />
</DownloadLink>
{/if}
{#if show_retry} {#if show_retry}
<IconButton <IconButton
Icon={Retry} Icon={Retry}

View File

@ -10,6 +10,7 @@
import type { NormalisedMessage } from "../types"; import type { NormalisedMessage } from "../types";
import { copy } from "@gradio/utils"; import { copy } from "@gradio/utils";
import Message from "./Message.svelte"; import Message from "./Message.svelte";
import { DownloadLink } from "@gradio/wasm/svelte";
import { dequal } from "dequal/lite"; import { dequal } from "dequal/lite";
import { import {
@ -22,7 +23,13 @@
} from "svelte"; } from "svelte";
import { Image } from "@gradio/image/shared"; import { Image } from "@gradio/image/shared";
import { Clear, Trash, Community, ScrollDownArrow } from "@gradio/icons"; import {
Clear,
Trash,
Community,
ScrollDownArrow,
Download
} from "@gradio/icons";
import { IconButtonWrapper, IconButton } from "@gradio/atoms"; import { IconButtonWrapper, IconButton } from "@gradio/atoms";
import type { SelectData, LikeData } from "@gradio/utils"; import type { SelectData, LikeData } from "@gradio/utils";
import type { ExampleMessage } from "../types"; import type { ExampleMessage } from "../types";
@ -40,6 +47,7 @@
export let _fetch: typeof fetch; export let _fetch: typeof fetch;
export let load_component: Gradio["load_component"]; export let load_component: Gradio["load_component"];
export let allow_file_downloads: boolean;
let _components: Record<string, ComponentType<SvelteComponent>> = {}; let _components: Record<string, ComponentType<SvelteComponent>> = {};
@ -292,6 +300,14 @@
on:click={() => (is_image_preview_open = false)} on:click={() => (is_image_preview_open = false)}
label={"Clear"} label={"Clear"}
/> />
{#if allow_file_downloads}
<DownloadLink
href={image_preview_source}
download={image_preview_source_alt || "image"}
>
<IconButton Icon={Download} label={"Download"} />
</DownloadLink>
{/if}
</IconButtonWrapper> </IconButtonWrapper>
</div> </div>
{/if} {/if}
@ -326,6 +342,7 @@
{show_copy_button} {show_copy_button}
handle_action={(selected) => handle_like(i, messages[0], selected)} handle_action={(selected) => handle_like(i, messages[0], selected)}
scroll={is_browser ? scroll : () => {}} scroll={is_browser ? scroll : () => {}}
{allow_file_downloads}
/> />
{/each} {/each}
{#if pending_message} {#if pending_message}

View File

@ -8,6 +8,7 @@
export let i18n; export let i18n;
export let upload; export let upload;
export let _fetch; export let _fetch;
export let allow_file_downloads: boolean;
</script> </script>
{#if type === "gallery"} {#if type === "gallery"}
@ -45,7 +46,7 @@
label="" label=""
waveform_settings={{}} waveform_settings={{}}
waveform_options={{}} waveform_options={{}}
show_download_button={false} show_download_button={allow_file_downloads}
on:load on:load
/> />
{:else if type === "video"} {:else if type === "video"}
@ -57,7 +58,7 @@
show_share_button={true} show_share_button={true}
{i18n} {i18n}
{upload} {upload}
show_download_button={false} show_download_button={allow_file_downloads}
on:load on:load
> >
<track kind="captions" /> <track kind="captions" />
@ -68,7 +69,7 @@
{value} {value}
show_label={false} show_label={false}
label="chatbot-image" label="chatbot-image"
show_download_button={false} show_download_button={allow_file_downloads}
on:load on:load
{i18n} {i18n}
/> />

View File

@ -44,6 +44,7 @@
export let msg_format: "tuples" | "messages"; export let msg_format: "tuples" | "messages";
export let handle_action: (selected: string | null) => void; export let handle_action: (selected: string | null) => void;
export let scroll: () => void; export let scroll: () => void;
export let allow_file_downloads: boolean;
function handle_select(i: number, message: NormalisedMessage): void { function handle_select(i: number, message: NormalisedMessage): void {
dispatch("select", { dispatch("select", {
@ -71,7 +72,6 @@
} }
type ButtonPanelProps = { type ButtonPanelProps = {
show: boolean;
handle_action: (selected: string | null) => void; handle_action: (selected: string | null) => void;
likeable: boolean; likeable: boolean;
show_retry: boolean; show_retry: boolean;
@ -86,7 +86,6 @@
let button_panel_props: ButtonPanelProps; let button_panel_props: ButtonPanelProps;
$: button_panel_props = { $: button_panel_props = {
show: show_like || show_retry || show_undo || show_copy_button,
handle_action, handle_action,
likeable: show_like, likeable: show_like,
show_retry, show_retry,
@ -147,11 +146,23 @@
aria-label={role + "'s message: " + get_message_label_data(message)} aria-label={role + "'s message: " + get_message_label_data(message)}
> >
{#if message.type === "text"} {#if message.type === "text"}
{#if message.metadata.title} <div class="message-content">
<MessageBox {#if message.metadata.title}
title={message.metadata.title} <MessageBox
expanded={is_last_bot_message([message], value)} 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 <Markdown
message={message.content} message={message.content}
{latex_delimiters} {latex_delimiters}
@ -161,18 +172,8 @@
on:load={scroll} on:load={scroll}
{root} {root}
/> />
</MessageBox> {/if}
{:else} </div>
<Markdown
message={message.content}
{latex_delimiters}
{sanitize_html}
{render_markdown}
{line_breaks}
on:load={scroll}
{root}
/>
{/if}
{:else if message.type === "component" && message.content.component in _components} {:else if message.type === "component" && message.content.component in _components}
<Component <Component
{target} {target}
@ -185,6 +186,7 @@
{upload} {upload}
{_fetch} {_fetch}
on:load={() => scroll()} on:load={() => scroll()}
{allow_file_downloads}
/> />
{:else if message.type === "component" && message.content.component === "file"} {:else if message.type === "component" && message.content.component === "file"}
<a <a
@ -316,7 +318,6 @@
box-shadow: var(--shadow-drop); box-shadow: var(--shadow-drop);
align-self: flex-start; align-self: flex-start;
text-align: right; text-align: right;
padding: var(--spacing-sm) var(--spacing-xl);
border-color: var(--border-color-accent-subdued); border-color: var(--border-color-accent-subdued);
background-color: var(--color-accent-soft); background-color: var(--color-accent-soft);
} }
@ -330,7 +331,6 @@
box-shadow: var(--shadow-drop); box-shadow: var(--shadow-drop);
align-self: flex-start; align-self: flex-start;
text-align: right; text-align: right;
padding: var(--spacing-sm) var(--spacing-xl);
} }
.panel .user :global(*) { .panel .user :global(*) {
@ -417,17 +417,6 @@
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.user {
border-width: 1px;
border-radius: var(--radius-md);
align-self: flex-start;
border-bottom-right-radius: 0;
box-shadow: var(--shadow-drop);
text-align: right;
padding: var(--spacing-sm) var(--spacing-xl);
border-color: var(--border-color-accent-subdued);
background-color: var(--color-accent-soft);
}
@media (max-width: 480px) { @media (max-width: 480px) {
.user-row.bubble { .user-row.bubble {
align-self: flex-end; align-self: flex-end;
@ -441,6 +430,10 @@
} }
} }
.message-content {
padding: var(--spacing-sm) var(--spacing-xl);
}
.avatar-container { .avatar-container {
align-self: flex-start; align-self: flex-start;
position: relative; position: relative;

View File

@ -51,6 +51,7 @@ class TestChatbot:
"_selectable": False, "_selectable": False,
"_retryable": False, "_retryable": False,
"_undoable": False, "_undoable": False,
"allow_file_downloads": True,
"key": None, "key": None,
"type": "tuples", "type": "tuples",
"latex_delimiters": [{"display": True, "left": "$$", "right": "$$"}], "latex_delimiters": [{"display": True, "left": "$$", "right": "$$"}],