|
<script context="module" lang="ts"> |
|
import { writable } from "svelte/store"; |
|
import { mount_css as default_mount_css, prefix_css } from "./css"; |
|
|
|
import type { ComponentMeta, Dependency, LayoutNode } from "./types"; |
|
|
|
declare let BUILD_MODE: string; |
|
interface Config { |
|
auth_required: boolean | undefined; |
|
auth_message: string; |
|
components: ComponentMeta[]; |
|
css: string | null; |
|
js: string | null; |
|
head: string | null; |
|
dependencies: Dependency[]; |
|
dev_mode: boolean; |
|
enable_queue: boolean; |
|
layout: LayoutNode; |
|
mode: "blocks" | "interface"; |
|
root: string; |
|
theme: string; |
|
title: string; |
|
version: string; |
|
space_id: string | null; |
|
is_colab: boolean; |
|
show_api: boolean; |
|
stylesheets?: string[]; |
|
path: string; |
|
app_id?: string; |
|
} |
|
|
|
let id = -1; |
|
|
|
function create_intersection_store(): { |
|
register: (n: number, el: HTMLDivElement) => void; |
|
subscribe: (typeof intersecting)["subscribe"]; |
|
} { |
|
const intersecting = writable<Record<string, boolean>>({}); |
|
|
|
const els = new Map<HTMLDivElement, number>(); |
|
|
|
const observer = new IntersectionObserver((entries) => { |
|
entries.forEach((entry) => { |
|
if (entry.isIntersecting) { |
|
let _el: number | undefined = els.get(entry.target as HTMLDivElement); |
|
if (_el !== undefined) |
|
intersecting.update((s) => ({ ...s, [_el as number]: true })); |
|
} |
|
}); |
|
}); |
|
|
|
function register(_id: number, el: HTMLDivElement): void { |
|
els.set(el, _id); |
|
observer.observe(el); |
|
} |
|
|
|
return { register, subscribe: intersecting.subscribe }; |
|
} |
|
|
|
const intersecting = create_intersection_store(); |
|
</script> |
|
|
|
<script lang="ts"> |
|
import { onMount, setContext } from "svelte"; |
|
import type { api_factory, SpaceStatus } from "@gradio/client"; |
|
import Embed from "./Embed.svelte"; |
|
import type { ThemeMode } from "./types"; |
|
import { StatusTracker } from "@gradio/statustracker"; |
|
import { _ } from "svelte-i18n"; |
|
import { setupi18n } from "./i18n"; |
|
import type { WorkerProxy } from "@gradio/wasm"; |
|
import { setWorkerProxyContext } from "@gradio/wasm/svelte"; |
|
|
|
setupi18n(); |
|
|
|
export let autoscroll: boolean; |
|
export let version: string; |
|
export let initial_height: string; |
|
export let app_mode: boolean; |
|
export let is_embed: boolean; |
|
export let theme_mode: ThemeMode | null = "system"; |
|
export let control_page_title: boolean; |
|
export let container: boolean; |
|
export let info: boolean; |
|
export let eager: boolean; |
|
let eventSource: EventSource; |
|
|
|
// These utilities are exported to be injectable for the Wasm version. |
|
export let mount_css: typeof default_mount_css = default_mount_css; |
|
export let client: ReturnType<typeof api_factory>["client"]; |
|
export let upload_files: ReturnType<typeof api_factory>["upload_files"]; |
|
export let worker_proxy: WorkerProxy | undefined = undefined; |
|
if (worker_proxy) { |
|
setWorkerProxyContext(worker_proxy); |
|
|
|
worker_proxy.addEventListener("progress-update", (event) => { |
|
loading_text = (event as CustomEvent).detail + "..."; |
|
}); |
|
} |
|
export let fetch_implementation: typeof fetch = fetch; |
|
setContext("fetch_implementation", fetch_implementation); |
|
export let EventSource_factory: (url: URL) => EventSource = (url) => |
|
new EventSource(url); |
|
setContext("EventSource_factory", EventSource_factory); |
|
|
|
export let space: string | null; |
|
export let host: string | null; |
|
export let src: string | null; |
|
|
|
let _id = id++; |
|
|
|
let loader_status: "pending" | "error" | "complete" | "generating" = |
|
"pending"; |
|
let app_id: string | null = null; |
|
let wrapper: HTMLDivElement; |
|
let ready = false; |
|
let render_complete = false; |
|
let config: Config; |
|
let loading_text = $_("common.loading") + "..."; |
|
let active_theme_mode: ThemeMode; |
|
let api_url: string; |
|
|
|
$: if (config?.app_id) { |
|
app_id = config.app_id; |
|
} |
|
|
|
let css_text_stylesheet: HTMLStyleElement | null = null; |
|
async function mount_custom_css(css_string: string | null): Promise<void> { |
|
if (css_string) { |
|
css_text_stylesheet = prefix_css( |
|
css_string, |
|
version, |
|
css_text_stylesheet || undefined |
|
); |
|
} |
|
await mount_css(config.root + "/theme.css", document.head); |
|
if (!config.stylesheets) return; |
|
|
|
await Promise.all( |
|
config.stylesheets.map((stylesheet) => { |
|
let absolute_link = |
|
stylesheet.startsWith("http:") || stylesheet.startsWith("https:"); |
|
if (absolute_link) { |
|
return mount_css(stylesheet, document.head); |
|
} |
|
|
|
return fetch(config.root + "/" + stylesheet) |
|
.then((response) => response.text()) |
|
.then((css_string) => { |
|
prefix_css(css_string, version); |
|
}); |
|
}) |
|
); |
|
} |
|
async function add_custom_html_head( |
|
head_string: string | null |
|
): Promise<void> { |
|
if (head_string) { |
|
const parser = new DOMParser(); |
|
const parsed_head_html = Array.from( |
|
parser.parseFromString(head_string, "text/html").head.children |
|
); |
|
|
|
if (parsed_head_html) { |
|
for (let head_element of parsed_head_html) { |
|
let newElement = document.createElement(head_element.tagName); |
|
Array.from(head_element.attributes).forEach((attr) => { |
|
newElement.setAttribute(attr.name, attr.value); |
|
}); |
|
newElement.textContent = head_element.textContent; |
|
document.head.appendChild(newElement); |
|
} |
|
} |
|
} |
|
} |
|
|
|
function handle_darkmode(target: HTMLDivElement): "light" | "dark" { |
|
let url = new URL(window.location.toString()); |
|
let url_color_mode: ThemeMode | null = url.searchParams.get( |
|
"__theme" |
|
) as ThemeMode | null; |
|
active_theme_mode = theme_mode || url_color_mode || "system"; |
|
|
|
if (active_theme_mode === "dark" || active_theme_mode === "light") { |
|
darkmode(target, active_theme_mode); |
|
} else { |
|
active_theme_mode = use_system_theme(target); |
|
} |
|
return active_theme_mode; |
|
} |
|
|
|
function use_system_theme(target: HTMLDivElement): "light" | "dark" { |
|
const theme = update_scheme(); |
|
window |
|
?.matchMedia("(prefers-color-scheme: dark)") |
|
?.addEventListener("change", update_scheme); |
|
|
|
function update_scheme(): "light" | "dark" { |
|
let _theme: "light" | "dark" = window?.matchMedia?.( |
|
"(prefers-color-scheme: dark)" |
|
).matches |
|
? "dark" |
|
: "light"; |
|
|
|
darkmode(target, _theme); |
|
return _theme; |
|
} |
|
return theme; |
|
} |
|
|
|
function darkmode(target: HTMLDivElement, theme: "dark" | "light"): void { |
|
const dark_class_element = is_embed ? target.parentElement! : document.body; |
|
const bg_element = is_embed ? target : target.parentElement!; |
|
bg_element.style.background = "var(--body-background-fill)"; |
|
if (theme === "dark") { |
|
dark_class_element.classList.add("dark"); |
|
} else { |
|
dark_class_element.classList.remove("dark"); |
|
} |
|
} |
|
|
|
let status: SpaceStatus = { |
|
message: "", |
|
load_status: "pending", |
|
status: "sleeping", |
|
detail: "SLEEPING" |
|
}; |
|
|
|
let app: Awaited<ReturnType<typeof client>>; |
|
let css_ready = false; |
|
function handle_status(_status: SpaceStatus): void { |
|
status = _status; |
|
} |
|
onMount(async () => { |
|
if (window.__gradio_mode__ !== "website") { |
|
active_theme_mode = handle_darkmode(wrapper); |
|
} |
|
|
|
//@ts-ignore |
|
const gradio_dev_mode = window.__GRADIO_DEV__; |
|
//@ts-ignore |
|
const server_port = window.__GRADIO__SERVER_PORT__; |
|
|
|
api_url = |
|
BUILD_MODE === "dev" || gradio_dev_mode === "dev" |
|
? `http://localhost:${ |
|
typeof server_port === "number" ? server_port : 7860 |
|
}` |
|
: host || space || src || location.origin; |
|
|
|
app = await client(api_url, { |
|
status_callback: handle_status, |
|
normalise_files: false |
|
}); |
|
config = app.config; |
|
window.__gradio_space__ = config.space_id; |
|
|
|
status = { |
|
message: "", |
|
load_status: "complete", |
|
status: "running", |
|
detail: "RUNNING" |
|
}; |
|
|
|
await mount_custom_css(config.css); |
|
await add_custom_html_head(config.head); |
|
css_ready = true; |
|
window.__is_colab__ = config.is_colab; |
|
|
|
if (config.dev_mode) { |
|
setTimeout(() => { |
|
const { host } = new URL(api_url); |
|
let url = new URL(`http://${host}/dev/reload`); |
|
eventSource = new EventSource(url); |
|
eventSource.onmessage = async function (event) { |
|
if (event.data === "CHANGE") { |
|
app = await client(api_url, { |
|
status_callback: handle_status, |
|
normalise_files: false |
|
}); |
|
|
|
config = app.config; |
|
window.__gradio_space__ = config.space_id; |
|
await mount_custom_css(config.css); |
|
} |
|
}; |
|
}, 200); |
|
} |
|
}); |
|
|
|
setContext("upload_files", upload_files); |
|
|
|
$: loader_status = |
|
!ready && status.load_status !== "error" |
|
? "pending" |
|
: !ready && status.load_status === "error" |
|
? "error" |
|
: status.load_status; |
|
|
|
$: config && (eager || $intersecting[_id]) && load_demo(); |
|
|
|
let Blocks: typeof import("./Blocks.svelte").default; |
|
let Login: typeof import("./Login.svelte").default; |
|
|
|
async function get_blocks(): Promise<void> { |
|
Blocks = (await import("./Blocks.svelte")).default; |
|
} |
|
async function get_login(): Promise<void> { |
|
Login = (await import("./Login.svelte")).default; |
|
} |
|
|
|
function load_demo(): void { |
|
if (config.auth_required) get_login(); |
|
else get_blocks(); |
|
} |
|
|
|
type error_types = |
|
| "NO_APP_FILE" |
|
| "CONFIG_ERROR" |
|
| "BUILD_ERROR" |
|
| "RUNTIME_ERROR" |
|
| "PAUSED"; |
|
|
|
// todo @hannahblair: translate these messages |
|
const discussion_message = { |
|
readable_error: { |
|
NO_APP_FILE: $_("errors.no_app_file"), |
|
CONFIG_ERROR: $_("errors.config_error"), |
|
BUILD_ERROR: $_("errors.build_error"), |
|
RUNTIME_ERROR: $_("errors.runtime_error"), |
|
PAUSED: $_("errors.space_paused") |
|
} as const, |
|
title(error: error_types): string { |
|
return encodeURIComponent($_("errors.space_not_working")); |
|
}, |
|
description(error: error_types, site: string): string { |
|
return encodeURIComponent( |
|
`Hello,\n\nFirstly, thanks for creating this space!\n\nI noticed that the space isn't working correctly because there is ${ |
|
this.readable_error[error] || "an error" |
|
}.\n\nIt would be great if you could take a look at this because this space is being embedded on ${site}.\n\nThanks!` |
|
); |
|
} |
|
}; |
|
|
|
onMount(async () => { |
|
intersecting.register(_id, wrapper); |
|
}); |
|
|
|
$: if (render_complete) { |
|
wrapper.dispatchEvent( |
|
new CustomEvent("render", { |
|
bubbles: true, |
|
cancelable: false, |
|
composed: true |
|
}) |
|
); |
|
} |
|
</script> |
|
|
|
<Embed |
|
display={container && is_embed} |
|
{is_embed} |
|
info={!!space && info} |
|
{version} |
|
{initial_height} |
|
{space} |
|
loaded={loader_status === "complete"} |
|
bind:wrapper |
|
> |
|
{#if (loader_status === "pending" || loader_status === "error") && !(config && config?.auth_required)} |
|
<StatusTracker |
|
absolute={!is_embed} |
|
status={loader_status} |
|
timer={false} |
|
queue_position={null} |
|
queue_size={null} |
|
translucent={true} |
|
{loading_text} |
|
i18n={$_} |
|
{autoscroll} |
|
> |
|
<!-- todo: translate message text --> |
|
<div class="error" slot="error"> |
|
<p><strong>{status?.message || ""}</strong></p> |
|
{#if (status.status === "space_error" || status.status === "paused") && status.discussions_enabled} |
|
<p> |
|
Please <a |
|
href="https://huggingface.co/spaces/{space}/discussions/new?title={discussion_message.title( |
|
status?.detail |
|
)}&description={discussion_message.description( |
|
status?.detail, |
|
location.origin |
|
)}" |
|
> |
|
contact the author of the space</a |
|
> to let them know. |
|
</p> |
|
{:else} |
|
<p>{$_("errors.contact_page_author")}</p> |
|
{/if} |
|
</div> |
|
</StatusTracker> |
|
{/if} |
|
{#if config?.auth_required && Login} |
|
<Login |
|
auth_message={config.auth_message} |
|
root={config.root} |
|
space_id={space} |
|
{app_mode} |
|
/> |
|
{:else if config && Blocks && css_ready} |
|
<Blocks |
|
{app} |
|
{...config} |
|
theme_mode={active_theme_mode} |
|
{control_page_title} |
|
target={wrapper} |
|
{autoscroll} |
|
bind:ready |
|
bind:render_complete |
|
show_footer={!is_embed} |
|
{app_mode} |
|
{version} |
|
/> |
|
{/if} |
|
</Embed> |
|
|
|
<style> |
|
.error { |
|
position: relative; |
|
padding: var(--size-4); |
|
color: var(--body-text-color); |
|
text-align: center; |
|
} |
|
|
|
.error > * { |
|
margin-top: var(--size-4); |
|
} |
|
|
|
a { |
|
color: var(--link-text-color); |
|
} |
|
|
|
a:hover { |
|
color: var(--link-text-color-hover); |
|
text-decoration: underline; |
|
} |
|
|
|
a:visited { |
|
color: var(--link-text-color-visited); |
|
} |
|
|
|
a:active { |
|
color: var(--link-text-color-active); |
|
} |
|
</style> |
|
|