freddyaboulton's picture
Upload folder using huggingface_hub
4a093d2 verified
<script lang="ts">
import { format_chat_for_sharing } from "./utils";
import { copy } from "@gradio/utils";
import { dequal } from "dequal/lite";
import { beforeUpdate, afterUpdate, createEventDispatcher } from "svelte";
import { ShareButton } from "@gradio/atoms";
import { Audio } from "@gradio/audio/shared";
import { Image } from "@gradio/image/shared";
import { Video } from "@gradio/video/shared";
import type { SelectData, LikeData } from "@gradio/utils";
import type { ChatMessage, ChatFileMessage, Message, MessageRole } from "../types";
import { MarkdownCode as Markdown } from "@gradio/markdown";
import { FileData } from "@gradio/client";
import Copy from "./Copy.svelte";
import type { I18nFormatter } from "js/app/src/gradio_helper";
import LikeDislike from "./LikeDislike.svelte";
import Pending from "./Pending.svelte";
import ToolMessage from "./ToolMessage.svelte";
import ErrorMessage from "./ErrorMessage.svelte";
export let value: (ChatMessage | ChatFileMessage)[] = [];
let old_value: (ChatMessage | ChatFileMessage)[] | null = null;
export let latex_delimiters: {
left: string;
right: string;
display: boolean;
}[];
export let pending_message = false;
export let selectable = false;
export let likeable = false;
export let show_share_button = false;
export let rtl = false;
export let show_copy_button = false;
export let avatar_images: [FileData | null, FileData | null] = [null, null];
export let sanitize_html = true;
export let bubble_full_width = true;
export let render_markdown = true;
export let line_breaks = true;
export let i18n: I18nFormatter;
export let layout: "bubble" | "panel" = "bubble";
export let placeholder: string | null = null;
let div: HTMLDivElement;
let autoscroll: boolean;
$: adjust_text_size = () => {
let style = getComputedStyle(document.body);
let body_text_size = style.getPropertyValue("--body-text-size");
let updated_text_size;
switch (body_text_size) {
case "13px":
updated_text_size = 14;
break;
case "14px":
updated_text_size = 16;
break;
case "16px":
updated_text_size = 20;
break;
default:
updated_text_size = 14;
break;
}
document.body.style.setProperty(
"--chatbot-body-text-size",
updated_text_size + "px"
);
};
$: adjust_text_size();
const dispatch = createEventDispatcher<{
change: undefined;
select: SelectData;
like: LikeData;
}>();
beforeUpdate(() => {
autoscroll =
div && div.offsetHeight + div.scrollTop > div.scrollHeight - 100;
});
const scroll = (): void => {
if (autoscroll) {
div.scrollTo(0, div.scrollHeight);
}
};
afterUpdate(() => {
if (autoscroll) {
scroll();
div.querySelectorAll("img").forEach((n) => {
n.addEventListener("load", () => {
scroll();
});
});
}
});
$: {
if (!dequal(value, old_value)) {
old_value = value;
dispatch("change");
}
}
function handle_select(
i: number,
message: Message
): void {
dispatch("select", {
index: i,
value: (message as ChatMessage).content || (message as ChatFileMessage).file?.url
});
}
function handle_like(
i: number,
message: Message | null,
selected: string | null
): void {
dispatch("like", {
index: i,
value: (message as ChatMessage).content || (message as ChatFileMessage).file?.url,
liked: selected === "like"
});
}
function isFileMessage(
message: ChatMessage | ChatFileMessage
): message is ChatFileMessage {
return "file" in message;
}
function groupMessages(messages: (ChatMessage | ChatFileMessage)[]): (ChatMessage | ChatFileMessage)[][] {
const groupedMessages: (ChatMessage | ChatFileMessage)[][] = [];
let currentGroup: (ChatMessage | ChatFileMessage)[] = [];
let currentRole: MessageRole | null = null;
for (const message of messages) {
if (message.role === currentRole) {
currentGroup.push(message);
} else {
if (currentGroup.length > 0) {
groupedMessages.push(currentGroup);
}
currentGroup = [message];
currentRole = message.role;
}
}
if (currentGroup.length > 0) {
groupedMessages.push(currentGroup);
}
return groupedMessages;
}
</script>
{#if show_share_button && value !== null && value.length > 0}
<div class="share-button">
<ShareButton
{i18n}
on:error
on:share
formatter={format_chat_for_sharing}
{value}
/>
</div>
{/if}
<div
class={layout === "bubble" ? "bubble-wrap" : "panel-wrap"}
class:placeholder-container={value === null || value.length === 0}
bind:this={div}
role="log"
aria-label="chatbot conversation"
aria-live="polite"
>
<div class="message-wrap" class:bubble-gap={layout === "bubble"} use:copy>
{#if value !== null && value.length > 0}
{@const groupedMessages = groupMessages(value)}
{#each groupedMessages as messages, i}
{#if messages.length}
{@const role = messages[0].role === "user" ? 'user' : 'bot'}
{@const avatar_img = avatar_images[role === "user" ? 0 : 1]}
<div class="message-row {layout} {role === "user" ? 'user-row' : 'bot-row'}">
{#if avatar_img}
<div class="avatar-container">
<Image
class="avatar-image"
src={avatar_img.url}
alt="{role} avatar"
/>
</div>
{/if}
<div
class="message {role === "user" ? 'user' : 'bot'}"
class:message-fit={layout === "bubble" && !bubble_full_width}
class:panel-full-width={layout === "panel"}
class:message-bubble-border={layout === "bubble"}
class:message-markdown-disabled={!render_markdown}
style:text-align={rtl && role == 'bot' ? "left" : "right"}
>
<button
data-testid={role}
class:latest={i === groupedMessages.length - 1}
class:message-markdown-disabled={!render_markdown}
style:user-select="text"
class:selectable
style:text-align={rtl ? "right" : "left"}
on:click={() => handle_select(i, messages[0])}
on:keydown={(e) => {
if (e.key === "Enter") {
handle_select(i, messages[0]);
}
}}
dir={rtl ? "rtl" : "ltr"}
>
{#each messages as message, thought_index}
{#if !isFileMessage(message)}
<div class:thought={thought_index > 0}>
{#if message.thought_metadata.tool_name}
<ToolMessage
title={`Used tool ${message.thought_metadata.tool_name}`}
>
<!-- {message.content} -->
<Markdown
message={message.content}
{latex_delimiters}
{sanitize_html}
{render_markdown}
{line_breaks}
on:load={scroll}
/>
</ToolMessage>
{:else if message.thought_metadata.error}
<ErrorMessage
>
<!-- {message.content} -->
<Markdown
message={message.content}
{latex_delimiters}
{sanitize_html}
{render_markdown}
{line_breaks}
on:load={scroll}
/>
</ErrorMessage>
{:else}
<!-- {message.content} -->
<Markdown
message={message.content}
{latex_delimiters}
{sanitize_html}
{render_markdown}
{line_breaks}
on:load={scroll}
/>
{/if}
</div>
{:else}
{#if message.file.mime_type?.includes("audio")}
<Audio
data-testid="chatbot-audio"
controls
preload="metadata"
src={message.file?.url}
title={message.alt_text}
on:play
on:pause
on:ended
/>
{:else if message !== null && message.file?.mime_type?.includes("video")}
<Video
data-testid="chatbot-video"
controls
src={message.file?.url}
title={message.alt_text}
preload="auto"
on:play
on:pause
on:ended
>
<track kind="captions" />
</Video>
{:else if message !== null && message.file?.mime_type?.includes("image")}
<Image
data-testid="chatbot-image"
src={message.file?.url}
alt={message.alt_text}
/>
{:else if message !== null && message.file?.url !== null}
<a
data-testid="chatbot-file"
href={message.file?.url}
target="_blank"
download={window.__is_colab__
? null
: message.file?.orig_name || message.file?.path}
>
{message.file?.orig_name || message.file?.path}
</a>
{/if}
{/if}
{/each}
</button>
</div>
<!-- {#if (likeable && role === 'bot') || (show_copy_button && message && typeof message === "string")}
<div
class="message-buttons-{role} message-buttons-{layout} {avatar_images[j] !==
null && 'with-avatar'}"
class:message-buttons-fit={layout === "bubble" &&
!bubble_full_width}
class:bubble-buttons-user={layout === "bubble"}
>
{#if likeable && role === 'bot'}
<LikeDislike
handle_action={(selected) =>
handle_like(i, message, selected)}
/>
{/if}
{#if show_copy_button && message && typeof message === "string"}
<Copy value={message} />
{/if}
</div>
{/if} -->
</div>
{/if}
{/each}
{#if pending_message}
<Pending {layout} />
{/if}
{:else if placeholder !== null}
<center>
<Markdown message={placeholder} {latex_delimiters} />
</center>
{/if}
</div>
</div>
<style>
.placeholder-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.bubble-wrap {
padding: var(--block-padding);
width: 100%;
overflow-y: auto;
}
.panel-wrap {
width: 100%;
overflow-y: auto;
}
.message-wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bubble-gap {
gap: calc(var(--spacing-xxl) + var(--spacing-lg));
}
.message-wrap > div :not(.avatar-container) :global(img) {
border-radius: 13px;
margin: var(--size-2);
width: 400px;
max-width: 30vw;
max-height: auto;
}
.message-wrap > div :global(p:not(:first-child)) {
margin-top: var(--spacing-xxl);
}
.message {
position: relative;
display: flex;
flex-direction: column;
align-self: flex-end;
background: var(--background-fill-secondary);
width: calc(100% - var(--spacing-xxl));
color: var(--body-text-color);
font-size: var(--chatbot-body-text-size);
overflow-wrap: break-word;
overflow-x: hidden;
padding-right: calc(var(--spacing-xxl) + var(--spacing-md));
padding: calc(var(--spacing-xxl) + var(--spacing-sm));
}
.thought {
margin-top: var(--spacing-xxl);
}
.message :global(.prose) {
font-size: var(--chatbot-body-text-size);
}
.message-bubble-border {
border-width: 1px;
border-radius: var(--radius-xxl);
}
.message-fit {
width: fit-content !important;
}
.panel-full-width {
padding: calc(var(--spacing-xxl) * 2);
width: 100%;
}
.message-markdown-disabled {
white-space: pre-line;
}
@media (max-width: 480px) {
.panel-full-width {
padding: calc(var(--spacing-xxl) * 2);
}
}
.user {
align-self: flex-start;
border-bottom-right-radius: 0;
text-align: right;
}
.bot {
border-bottom-left-radius: 0;
text-align: left;
}
/* Colors */
.bot {
border-color: var(--border-color-primary);
background: var(--background-fill-secondary);
}
.user {
border-color: var(--border-color-accent-subdued);
background-color: var(--color-accent-soft);
}
.message-row {
display: flex;
flex-direction: row;
position: relative;
}
.message-row.panel.user-row {
background: var(--color-accent-soft);
}
.message-row.panel.bot-row {
background: var(--background-fill-secondary);
}
.message-row:last-of-type {
margin-bottom: var(--spacing-xxl);
}
.user-row.bubble {
flex-direction: row;
justify-content: flex-end;
}
@media (max-width: 480px) {
.user-row.bubble {
align-self: flex-end;
}
.bot-row.bubble {
align-self: flex-start;
}
.message {
width: auto;
}
}
.avatar-container {
align-self: flex-end;
position: relative;
justify-content: center;
width: 35px;
height: 35px;
flex-shrink: 0;
bottom: 0;
}
.user-row.bubble > .avatar-container {
order: 2;
margin-left: 10px;
}
.bot-row.bubble > .avatar-container {
margin-right: 10px;
}
.panel > .avatar-container {
margin-left: 25px;
align-self: center;
}
.avatar-container :global(img) {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.message-buttons-user,
.message-buttons-bot {
border-radius: var(--radius-md);
display: flex;
align-items: center;
bottom: 0;
height: var(--size-7);
align-self: self-end;
position: absolute;
bottom: -15px;
margin: 2px;
padding-left: 5px;
z-index: 1;
}
.message-buttons-bot {
left: 10px;
}
.message-buttons-user {
right: 5px;
}
.message-buttons-bot.message-buttons-bubble.with-avatar {
left: 50px;
}
.message-buttons-user.message-buttons-bubble.with-avatar {
right: 50px;
}
.message-buttons-bubble {
border: 1px solid var(--border-color-accent);
background: var(--background-fill-secondary);
}
.message-buttons-panel {
left: unset;
right: 0px;
top: 0px;
}
.share-button {
position: absolute;
top: 4px;
right: 6px;
}
.selectable {
cursor: pointer;
}
@keyframes dot-flashing {
0% {
opacity: 0.8;
}
50% {
opacity: 0.5;
}
100% {
opacity: 0.8;
}
}
.message-wrap .message :global(a) {
color: var(--color-text-link);
text-decoration: underline;
}
.message-wrap .bot :global(table),
.message-wrap .bot :global(tr),
.message-wrap .bot :global(td),
.message-wrap .bot :global(th) {
border: 1px solid var(--border-color-primary);
}
.message-wrap .user :global(table),
.message-wrap .user :global(tr),
.message-wrap .user :global(td),
.message-wrap .user :global(th) {
border: 1px solid var(--border-color-accent);
}
/* Lists */
.message-wrap :global(ol),
.message-wrap :global(ul) {
padding-inline-start: 2em;
}
/* KaTeX */
.message-wrap :global(span.katex) {
font-size: var(--text-lg);
direction: ltr;
}
/* Copy button */
.message-wrap :global(div[class*="code_wrap"] > button) {
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
z-index: 1;
cursor: pointer;
border-bottom-left-radius: var(--radius-sm);
padding: 5px;
padding: var(--spacing-md);
width: 25px;
height: 25px;
}
.message-wrap :global(code > button > span) {
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
width: 12px;
height: 12px;
}
.message-wrap :global(.check) {
position: absolute;
top: 0;
right: 0;
opacity: 0;
z-index: var(--layer-top);
transition: opacity 0.2s;
background: var(--background-fill-primary);
padding: var(--size-1);
width: 100%;
height: 100%;
color: var(--body-text-color);
}
.message-wrap :global(pre) {
position: relative;
}
</style>