| <script context="module" lang="ts"> | |
| import { tick } from "svelte"; | |
| import { pretty_si } from "./utils"; | |
| let items: HTMLDivElement[] = []; | |
| let called = false; | |
| async function scroll_into_view( | |
| el: HTMLDivElement, | |
| enable: boolean | null = true | |
| ): Promise<void> { | |
| if ( | |
| window.__gradio_mode__ === "website" || | |
| (window.__gradio_mode__ !== "app" && enable !== true) | |
| ) { | |
| return; | |
| } | |
| items.push(el); | |
| if (!called) called = true; | |
| else return; | |
| await tick(); | |
| requestAnimationFrame(() => { | |
| let min = [0, 0]; | |
| for (let i = 0; i < items.length; i++) { | |
| const element = items[i]; | |
| const box = element.getBoundingClientRect(); | |
| if (i === 0 || box.top + window.scrollY <= min[0]) { | |
| min[0] = box.top + window.scrollY; | |
| min[1] = i; | |
| } | |
| } | |
| window.scrollTo({ top: min[0] - 20, behavior: "smooth" }); | |
| called = false; | |
| items = []; | |
| }); | |
| } | |
| </script> | |
| <script lang="ts"> | |
| import { onDestroy } from "svelte"; | |
| import Loader from "./Loader.svelte"; | |
| import type { LoadingStatus } from "./types"; | |
| import type { I18nFormatter } from "@gradio/utils"; | |
| export let i18n: I18nFormatter; | |
| export let eta: number | null = null; | |
| export let queue_position: number | null; | |
| export let queue_size: number | null; | |
| export let status: "complete" | "pending" | "error" | "generating"; | |
| export let scroll_to_output = false; | |
| export let timer = true; | |
| export let show_progress: "full" | "minimal" | "hidden" = "full"; | |
| export let message: string | null = null; | |
| export let progress: LoadingStatus["progress"] | null | undefined = null; | |
| export let variant: "default" | "center" = "default"; | |
| export let loading_text = "Loading..."; | |
| export let absolute = true; | |
| export let translucent = false; | |
| export let border = false; | |
| export let autoscroll: boolean; | |
| let el: HTMLDivElement; | |
| let _timer = false; | |
| let timer_start = 0; | |
| let timer_diff = 0; | |
| let old_eta: number | null = null; | |
| let eta_from_start: number | null = null; | |
| let message_visible = false; | |
| let eta_level: number | null = 0; | |
| let progress_level: (number | undefined)[] | null = null; | |
| let last_progress_level: number | undefined = undefined; | |
| let progress_bar: HTMLElement | null = null; | |
| let show_eta_bar = true; | |
| $: eta_level = | |
| eta_from_start === null || eta_from_start <= 0 || !timer_diff | |
| ? null | |
| : Math.min(timer_diff / eta_from_start, 1); | |
| $: if (progress != null) { | |
| show_eta_bar = false; | |
| } | |
| $: { | |
| if (progress != null) { | |
| progress_level = progress.map((p) => { | |
| if (p.index != null && p.length != null) { | |
| return p.index / p.length; | |
| } else if (p.progress != null) { | |
| return p.progress; | |
| } | |
| return undefined; | |
| }); | |
| } else { | |
| progress_level = null; | |
| } | |
| if (progress_level) { | |
| last_progress_level = progress_level[progress_level.length - 1]; | |
| if (progress_bar) { | |
| if (last_progress_level === 0) { | |
| progress_bar.style.transition = "0"; | |
| } else { | |
| progress_bar.style.transition = "150ms"; | |
| } | |
| } | |
| } else { | |
| last_progress_level = undefined; | |
| } | |
| } | |
| const start_timer = (): void => { | |
| eta = old_eta = formatted_eta = null; | |
| timer_start = performance.now(); | |
| timer_diff = 0; | |
| _timer = true; | |
| run(); | |
| }; | |
| function run(): void { | |
| requestAnimationFrame(() => { | |
| timer_diff = (performance.now() - timer_start) / 1000; | |
| if (_timer) run(); | |
| }); | |
| } | |
| function stop_timer(): void { | |
| timer_diff = 0; | |
| eta = old_eta = formatted_eta = null; | |
| if (!_timer) return; | |
| _timer = false; | |
| } | |
| onDestroy(() => { | |
| if (_timer) stop_timer(); | |
| }); | |
| $: { | |
| if (status === "pending") { | |
| start_timer(); | |
| } else { | |
| stop_timer(); | |
| } | |
| } | |
| $: el && | |
| scroll_to_output && | |
| (status === "pending" || status === "complete") && | |
| scroll_into_view(el, autoscroll); | |
| let formatted_eta: string | null = null; | |
| $: { | |
| if (eta === null) { | |
| eta = old_eta; | |
| } | |
| if (eta != null && old_eta !== eta) { | |
| eta_from_start = (performance.now() - timer_start) / 1000 + eta; | |
| formatted_eta = eta_from_start.toFixed(1); | |
| old_eta = eta; | |
| } | |
| } | |
| let show_message_timeout: NodeJS.Timeout | null = null; | |
| function close_message(): void { | |
| message_visible = false; | |
| if (show_message_timeout !== null) { | |
| clearTimeout(show_message_timeout); | |
| } | |
| } | |
| $: { | |
| close_message(); | |
| if (status === "error" && message) { | |
| message_visible = true; | |
| } | |
| } | |
| $: formatted_timer = timer_diff.toFixed(1); | |
| </script> | |
| <div | |
| class="wrap {variant} {show_progress}" | |
| class:hide={!status || status === "complete" || show_progress === "hidden"} | |
| class:translucent={(variant === "center" && | |
| (status === "pending" || status === "error")) || | |
| translucent || | |
| show_progress === "minimal"} | |
| class:generating={status === "generating"} | |
| class:border | |
| style:position={absolute ? "absolute" : "static"} | |
| style:padding={absolute ? "0" : "var(--size-8) 0"} | |
| bind:this={el} | |
| > | |
| {#if status === "pending"} | |
| {#if variant === "default" && show_eta_bar && show_progress === "full"} | |
| <div | |
| class="eta-bar" | |
| style:transform="translateX({(eta_level || 0) * 100 - 100}%)" | |
| /> | |
| {/if} | |
| <div | |
| class:meta-text-center={variant === "center"} | |
| class:meta-text={variant === "default"} | |
| class="progress-text" | |
| > | |
| {#if progress} | |
| {#each progress as p} | |
| {#if p.index != null} | |
| {#if p.length != null} | |
| {pretty_si(p.index || 0)}/{pretty_si(p.length)} | |
| {:else} | |
| {pretty_si(p.index || 0)} | |
| {/if} | |
| {p.unit} | {" "} | |
| {/if} | |
| {/each} | |
| {:else if queue_position !== null && queue_size !== undefined && queue_position >= 0} | |
| queue: {queue_position + 1}/{queue_size} | | |
| {:else if queue_position === 0} | |
| processing | | |
| {/if} | |
| {#if timer} | |
| {formatted_timer}{eta ? `/${formatted_eta}` : ""}s | |
| {/if} | |
| </div> | |
| {#if last_progress_level != null} | |
| <div class="progress-level"> | |
| <div class="progress-level-inner"> | |
| {#if progress != null} | |
| {#each progress as p, i} | |
| {#if p.desc != null || (progress_level && progress_level[i] != null)} | |
| {#if i !== 0} | |
| / | |
| {/if} | |
| {#if p.desc != null} | |
| {p.desc} | |
| {/if} | |
| {#if p.desc != null && progress_level && progress_level[i] != null} | |
| - | |
| {/if} | |
| {#if progress_level != null} | |
| {(100 * (progress_level[i] || 0)).toFixed(1)}% | |
| {/if} | |
| {/if} | |
| {/each} | |
| {/if} | |
| </div> | |
| <div class="progress-bar-wrap"> | |
| <div | |
| bind:this={progress_bar} | |
| class="progress-bar" | |
| style:width="{last_progress_level * 100}%" | |
| /> | |
| </div> | |
| </div> | |
| {:else if show_progress === "full"} | |
| <Loader margin={variant === "default"} /> | |
| {/if} | |
| {#if !timer} | |
| <p class="loading">{loading_text}</p> | |
| {/if} | |
| {:else if status === "error"} | |
| <span class="error">{i18n("common.error")}</span> | |
| <slot name="error" /> | |
| {/if} | |
| </div> | |
| <style> | |
| .wrap { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: var(--layer-top); | |
| transition: opacity 0.1s ease-in-out; | |
| border-radius: var(--block-radius); | |
| background: var(--block-background-fill); | |
| padding: 0 var(--size-6); | |
| max-height: var(--size-screen-h); | |
| overflow: hidden; | |
| pointer-events: none; | |
| } | |
| .wrap.center { | |
| top: 0; | |
| right: 0px; | |
| left: 0px; | |
| } | |
| .wrap.default { | |
| top: 0px; | |
| right: 0px; | |
| bottom: 0px; | |
| left: 0px; | |
| } | |
| .hide { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .generating { | |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| border: 2px solid var(--color-accent); | |
| background: transparent; | |
| } | |
| .translucent { | |
| background: none; | |
| } | |
| @keyframes pulse { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| } | |
| .loading { | |
| z-index: var(--layer-2); | |
| color: var(--body-text-color); | |
| } | |
| .eta-bar { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| bottom: 0; | |
| left: 0; | |
| transform-origin: left; | |
| opacity: 0.8; | |
| z-index: var(--layer-1); | |
| transition: 10ms; | |
| background: var(--background-fill-secondary); | |
| } | |
| .progress-bar-wrap { | |
| border: 1px solid var(--border-color-primary); | |
| background: var(--background-fill-primary); | |
| width: 55.5%; | |
| height: var(--size-4); | |
| } | |
| .progress-bar { | |
| transform-origin: left; | |
| background-color: var(--loader-color); | |
| width: var(--size-full); | |
| height: var(--size-full); | |
| } | |
| .progress-level { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1; | |
| z-index: var(--layer-2); | |
| width: var(--size-full); | |
| } | |
| .progress-level-inner { | |
| margin: var(--size-2) auto; | |
| color: var(--body-text-color); | |
| font-size: var(--text-sm); | |
| font-family: var(--font-mono); | |
| } | |
| .meta-text { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| z-index: var(--layer-2); | |
| padding: var(--size-1) var(--size-2); | |
| font-size: var(--text-sm); | |
| font-family: var(--font-mono); | |
| } | |
| .meta-text-center { | |
| display: flex; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| justify-content: center; | |
| align-items: center; | |
| transform: translateY(var(--size-6)); | |
| z-index: var(--layer-2); | |
| padding: var(--size-1) var(--size-2); | |
| font-size: var(--text-sm); | |
| font-family: var(--font-mono); | |
| text-align: center; | |
| } | |
| .error { | |
| box-shadow: var(--shadow-drop); | |
| border: solid 1px var(--error-border-color); | |
| border-radius: var(--radius-full); | |
| background: var(--error-background-fill); | |
| padding-right: var(--size-4); | |
| padding-left: var(--size-4); | |
| color: var(--error-text-color); | |
| font-weight: var(--weight-semibold); | |
| font-size: var(--text-lg); | |
| line-height: var(--line-lg); | |
| font-family: var(--font); | |
| } | |
| .minimal .progress-text { | |
| background: var(--block-background-fill); | |
| } | |
| .border { | |
| border: 1px solid var(--border-color-primary); | |
| } | |
| </style> | |