gradio/js/chatbot/static/ChatBot.svelte
Jiri Puc 80be7a1ca4
chatbot conversation nodes can contain a copy button (#5125)
* chatbot conversation nodes can contain a copy button

* add changeset

* the newly added chatbot copy message button is now called show_copy_button

* chatbot's Copy component styling improved

* chatbot's Copy component - typing fix

---------

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
2023-08-10 10:11:26 -04:00

430 lines
9.2 KiB
Svelte

<script lang="ts">
import { copy, format_chat_for_sharing } from "../utils";
import "katex/dist/katex.min.css";
import { beforeUpdate, afterUpdate, createEventDispatcher } from "svelte";
import { ShareButton } from "@gradio/atoms";
import type { SelectData } from "@gradio/utils";
import type { ThemeMode } from "js/app/src/components/types";
import type { FileData } from "@gradio/upload";
import Markdown from "./MarkdownCode.svelte";
import Copy from "./Copy.svelte";
const code_highlight_css = {
light: (): Promise<typeof import("prismjs/themes/prism.css")> =>
import("prismjs/themes/prism.css"),
dark: (): Promise<typeof import("prismjs/themes/prism.css")> =>
import("prismjs/themes/prism-dark.css")
};
export let value:
| [string | FileData | null, string | FileData | null][]
| null;
let old_value: [string | FileData | null, string | FileData | null][] | null =
null;
export let latex_delimiters: {
left: string;
right: string;
display: boolean;
}[];
export let pending_message = false;
export let feedback: string[] | null = null;
export let selectable = false;
export let show_share_button = false;
export let theme_mode: ThemeMode;
export let rtl = false;
export let show_copy_button = false;
$: if (theme_mode == "dark") {
code_highlight_css.dark();
} else {
code_highlight_css.light();
}
let div: HTMLDivElement;
let autoscroll: boolean;
const dispatch = createEventDispatcher<{
change: undefined;
select: SelectData;
}>();
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 (value !== old_value) {
old_value = value;
dispatch("change");
}
}
function handle_select(
i: number,
j: number,
message: string | FileData | null
): void {
dispatch("select", {
index: [i, j],
value: message
});
}
</script>
{#if show_share_button && value !== null && value.length > 0}
<div class="icon-button">
<ShareButton
on:error
on:share
formatter={format_chat_for_sharing}
{value}
/>
</div>
{/if}
<div class="wrap" bind:this={div}>
<div class="message-wrap" use:copy>
{#if value !== null}
{#each value as message_pair, i}
{#each message_pair as message, j}
<!-- TODO: fix-->
<!-- svelte-ignore a11y-no-static-element-interactions-->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
data-testid={j == 0 ? "user" : "bot"}
class:latest={i === value.length - 1}
class="message {j == 0 ? 'user' : 'bot'}"
class:hide={message === null}
class:selectable
on:click={() => handle_select(i, j, message)}
dir={rtl ? "rtl" : "ltr"}
>
{#if typeof message === "string"}
<Markdown {message} {latex_delimiters} on:load={scroll} />
{#if feedback && j == 1}
<div class="feedback">
{#each feedback as f}
<button>{f}</button>
{/each}
</div>
{/if}
{#if show_copy_button && message}
<div class="icon-button">
<Copy value={message} />
</div>
{/if}
{:else if message !== null && message.mime_type?.includes("audio")}
<audio
data-testid="chatbot-audio"
controls
preload="metadata"
src={message.data}
title={message.alt_text}
on:play
on:pause
on:ended
/>
{:else if message !== null && message.mime_type?.includes("video")}
<video
data-testid="chatbot-video"
controls
src={message.data}
title={message.alt_text}
preload="auto"
on:play
on:pause
on:ended
>
<track kind="captions" />
</video>
{:else if message !== null && message.mime_type?.includes("image")}
<img
data-testid="chatbot-image"
src={message.data}
alt={message.alt_text}
/>
{:else if message !== null && message.data !== null}
<a
data-testid="chatbot-file"
href={message.data}
target="_blank"
download={window.__is_colab__
? null
: message.orig_name || message.name}
>
{message.orig_name || message.name}
</a>
{/if}
</div>
{/each}
{/each}
{/if}
{#if pending_message}
<div class="message pending">
<div class="dot-flashing" />
&nbsp;
<div class="dot-flashing" />
&nbsp;
<div class="dot-flashing" />
</div>
{/if}
</div>
</div>
<style>
.wrap {
padding: var(--block-padding);
width: 100%;
overflow-y: auto;
}
.message-wrap {
display: flex;
flex-direction: column;
gap: var(--spacing-xxl);
}
.message-wrap > div :global(img) {
border-radius: 13px;
max-width: 30vw;
}
.message-wrap > div :global(p:not(:first-child)) {
margin-top: var(--spacing-xxl);
}
.message-wrap :global(audio) {
width: 100%;
}
.message {
position: relative;
align-self: flex-start;
border-width: 1px;
border-radius: var(--radius-xxl);
background: var(--background-fill-secondary);
padding: var(--spacing-xxl);
width: calc(100% - var(--spacing-xxl));
color: var(--body-text-color);
font-size: var(--text-lg);
line-height: var(--line-lg);
overflow-wrap: break-word;
}
.user {
align-self: flex-end;
border-bottom-right-radius: 0;
}
.bot {
border-bottom-left-radius: 0;
padding-left: calc(2 * var(--spacing-xxl));
}
@media (max-width: 480px) {
.message {
width: auto;
}
.bot {
padding-left: var(--spacing-xxl);
}
}
/* Colors */
.bot,
.pending {
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);
}
.feedback {
display: flex;
position: absolute;
top: var(--spacing-xl);
right: calc(var(--spacing-xxl) + var(--spacing-xl));
gap: var(--spacing-lg);
font-size: var(--text-sm);
}
.feedback button {
color: var(--body-text-color-subdued);
}
.feedback button:hover {
color: var(--body-text-color);
}
.selectable {
cursor: pointer;
}
.pending {
display: flex;
justify-content: center;
align-items: center;
align-self: center;
gap: 2px;
}
.dot-flashing {
animation: dot-flashing 1s infinite linear alternate;
border-radius: 5px;
background-color: var(--body-text-color);
width: 5px;
height: 5px;
color: var(--body-text-color);
}
.dot-flashing:nth-child(2) {
animation-delay: 0.33s;
}
.dot-flashing:nth-child(3) {
animation-delay: 0.66s;
}
/* Small screen */
@media (max-width: 480px) {
.user {
align-self: flex-end;
}
.bot {
align-self: flex-start;
padding-left: var(--size-3);
}
}
@keyframes dot-flashing {
0% {
opacity: 0.8;
}
50% {
opacity: 0.5;
}
100% {
opacity: 0.8;
}
}
.message-wrap .message :global(img) {
margin: var(--size-2);
max-height: 200px;
}
.message-wrap .message :global(a) {
color: var(--color-text-link);
text-decoration: underline;
}
.hide {
display: none;
}
/* Code blocks */
.message-wrap :global(pre[class*="language-"]),
.message-wrap :global(pre) {
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
box-shadow: none;
border: none;
border-radius: var(--radius-md);
background-color: var(--chatbot-code-background-color);
padding: var(--spacing-xl) 10px;
direction: ltr;
}
.message-wrap :global(code) {
font-size: var(--text-md);
}
/* Tables */
.message-wrap :global(table),
.message-wrap :global(tr),
.message-wrap :global(td),
.message-wrap :global(th) {
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
padding: var(--spacing-xl);
}
.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(code > 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: 22px;
height: 22px;
}
.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;
}
.icon-button {
position: absolute;
top: 6px;
right: 6px;
}
</style>