gradio / js /app /src /Blocks.svelte
mindmime's picture
Upload folder using huggingface_hub
a03b3ba verified
<script lang="ts">
import { load_component } from "virtual:component-loader";
import { tick } from "svelte";
import { _ } from "svelte-i18n";
import type { client } from "@gradio/client";
import { create_loading_status_store } from "./stores";
import type { LoadingStatusCollection } from "./stores";
import type { ComponentMeta, Dependency, LayoutNode } from "./types";
import { setupi18n } from "./i18n";
import { ApiDocs } from "./api_docs/";
import type { ThemeMode, Payload } from "./types";
import { Toast } from "@gradio/statustracker";
import type { ToastMessage } from "@gradio/statustracker";
import type { ShareData } from "@gradio/utils";
import MountComponents from "./MountComponents.svelte";
import logo from "./images/logo.svg";
import api_logo from "./api_docs/img/api-logo.svg";
setupi18n();
export let root: string;
export let components: ComponentMeta[];
export let layout: LayoutNode;
export let dependencies: Dependency[];
export let title = "Gradio";
export let analytics_enabled = false;
export let target: HTMLElement;
export let autoscroll: boolean;
export let show_api = true;
export let show_footer = true;
export let control_page_title = false;
export let app_mode: boolean;
export let theme_mode: ThemeMode;
export let app: Awaited<ReturnType<typeof client>>;
export let space_id: string | null;
export let version: string;
export let js: string | null;
let loading_status = create_loading_status_store();
let rootNode: ComponentMeta = {
id: layout.id,
type: "column",
props: { interactive: false },
has_modes: false,
instance: null as unknown as ComponentMeta["instance"],
component: null as unknown as ComponentMeta["component"],
component_class_id: ""
};
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
dependencies.forEach((d) => {
if (d.js) {
const wrap = d.backend_fn
? d.inputs.length === 1
: d.outputs.length === 1;
try {
d.frontend_fn = new AsyncFunction(
"__fn_args",
`let result = await (${d.js})(...__fn_args);
return (${wrap} && !Array.isArray(result)) ? [result] : result;`
);
} catch (e) {
console.error("Could not parse custom js method.");
console.error(e);
}
}
});
let params = new URLSearchParams(window.location.search);
let api_docs_visible = params.get("view") === "api" && show_api;
function set_api_docs_visible(visible: boolean): void {
api_docs_visible = visible;
let params = new URLSearchParams(window.location.search);
if (visible) {
params.set("view", "api");
} else {
params.delete("view");
}
history.replaceState(null, "", "?" + params.toString());
}
function is_dep(
id: number,
type: "inputs" | "outputs",
deps: Dependency[]
): boolean {
for (const dep of deps) {
for (const dep_item of dep[type]) {
if (dep_item === id) return true;
}
}
return false;
}
let dynamic_ids: Set<number> = new Set();
function has_no_default_value(value: any): boolean {
return (
(Array.isArray(value) && value.length === 0) ||
value === "" ||
value === 0 ||
!value
);
}
let instance_map: { [id: number]: ComponentMeta };
type LoadedComponent = {
default: ComponentMeta["component"];
};
let component_set = new Set<
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
>();
let _component_map = new Map<
`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
>();
async function walk_layout(
node: LayoutNode,
type_map: Map<number, ComponentMeta["props"]["interactive"]>,
instance_map: { [id: number]: ComponentMeta },
component_map: Map<
`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
>
): Promise<void> {
ready = false;
let instance = instance_map[node.id];
const _component = (await component_map.get(
`${instance.type}_${type_map.get(node.id) || "false"}`
))!.component;
instance.component = _component.default;
if (node.children) {
instance.children = node.children.map((v) => instance_map[v.id]);
await Promise.all(
node.children.map((v) =>
walk_layout(v, type_map, instance_map, component_map)
)
);
}
}
export let ready = false;
export let render_complete = false;
$: components, layout, prepare_components();
let target_map: Record<number, Record<string, number[]>> = {};
function prepare_components(): void {
target_map = dependencies.reduce(
(acc, dep, i) => {
dep.targets.forEach(([id, trigger]) => {
if (!acc[id]) {
acc[id] = {};
}
if (acc[id]?.[trigger]) {
acc[id][trigger].push(i);
} else {
acc[id][trigger] = [i];
}
});
return acc;
},
{} as Record<number, Record<string, number[]>>
);
loading_status = create_loading_status_store();
dependencies.forEach((v, i) => {
loading_status.register(i, v.inputs, v.outputs);
});
const _dynamic_ids = new Set<number>();
for (const comp of components) {
const { id, props } = comp;
const is_input = is_dep(id, "inputs", dependencies);
if (
is_input ||
(!is_dep(id, "outputs", dependencies) &&
has_no_default_value(props?.value))
) {
_dynamic_ids.add(id);
}
}
dynamic_ids = _dynamic_ids;
const _rootNode: typeof rootNode = {
id: layout.id,
type: "column",
props: { interactive: false },
has_modes: false,
instance: null as unknown as ComponentMeta["instance"],
component: null as unknown as ComponentMeta["component"],
component_class_id: ""
};
components.push(_rootNode);
const _component_set = new Set<
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
>();
const __component_map = new Map<
`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
>();
const __type_for_id = new Map<
number,
ComponentMeta["props"]["interactive"]
>();
const _instance_map = components.reduce(
(acc, next) => {
acc[next.id] = next;
return acc;
},
{} as { [id: number]: ComponentMeta }
);
components.forEach((c) => {
if ((c.props as any).interactive === false) {
(c.props as any).interactive = false;
} else if ((c.props as any).interactive === true) {
(c.props as any).interactive = true;
} else if (dynamic_ids.has(c.id)) {
(c.props as any).interactive = true;
} else {
(c.props as any).interactive = false;
}
if ((c.props as any).server_fns) {
let server: Record<string, (...args: any[]) => Promise<any>> = {};
(c.props as any).server_fns.forEach((fn: string) => {
server[fn] = async (...args: any[]) => {
if (args.length === 1) {
args = args[0];
}
const result = await app.component_server(c.id, fn, args);
return result;
};
});
(c.props as any).server = server;
}
if (target_map[c.id]) {
c.props.attached_events = Object.keys(target_map[c.id]);
}
__type_for_id.set(c.id, c.props.interactive);
if (c.type === "dataset") {
const example_component_map = new Map();
(c.props.components as string[]).forEach((name: string) => {
if (example_component_map.has(name)) {
return;
}
let _c;
const matching_component = components.find((c) => c.type === name);
if (matching_component) {
_c = load_component({
api_url: root,
name,
id: matching_component.component_class_id,
variant: "example"
});
example_component_map.set(name, _c);
}
});
c.props.component_map = example_component_map;
}
// maybe load custom
const _c = load_component({
api_url: root,
name: c.type,
id: c.component_class_id,
variant: "component"
});
_component_set.add(_c);
__component_map.set(`${c.type}_${c.props.interactive}`, _c);
});
Promise.all(Array.from(_component_set)).then(() => {
walk_layout(layout, __type_for_id, _instance_map, __component_map)
.then(async () => {
ready = true;
component_set = _component_set;
_component_map = __component_map;
instance_map = _instance_map;
rootNode = _rootNode;
})
.catch((e) => {
console.error(e);
});
});
}
function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...funcArgs: Parameters<T>) => void {
let lastFunc: ReturnType<typeof setTimeout>;
let lastRan: number;
let lastThis: any;
let lastArgs: IArguments | null;
return function (this: any, ...args: Parameters<T>) {
if (!lastRan) {
func.apply(this, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastThis = this;
lastArgs = arguments;
lastFunc = setTimeout(
() => {
if (Date.now() - lastRan >= limit) {
if (lastArgs) {
func.apply(lastThis, Array.prototype.slice.call(lastArgs));
}
lastRan = Date.now();
}
},
Math.max(limit - (Date.now() - lastRan), 0)
);
}
};
}
const refresh = throttle(() => {
rootNode = rootNode;
}, 50);
async function handle_update(
data: any,
fn_index: number,
outputs_set_to_non_interactive: number[]
): Promise<void> {
const outputs = dependencies[fn_index].outputs;
data?.forEach((value: any, i: number) => {
const output = instance_map[outputs[i]];
output.props.value_is_output = true;
});
refresh();
await tick();
data?.forEach((value: any, i: number) => {
const output = instance_map[outputs[i]];
if (
typeof value === "object" &&
value !== null &&
value.__type__ === "update"
) {
for (const [update_key, update_value] of Object.entries(value)) {
if (update_key === "__type__") {
continue;
} else {
output.props[update_key] = update_value;
if (update_key == "interactive" && !update_value) {
outputs_set_to_non_interactive.push(outputs[i]);
}
}
}
} else {
output.props.value = value;
}
});
refresh();
}
let submit_map: Map<number, ReturnType<typeof app.submit>> = new Map();
function set_prop<T extends ComponentMeta>(
obj: T,
prop: string,
val: any
): void {
if (!obj?.props) {
// @ts-ignore
obj.props = {};
}
obj.props[prop] = val;
refresh();
}
let handled_dependencies: number[][] = [];
let messages: (ToastMessage & { fn_index: number })[] = [];
function new_message(
message: string,
fn_index: number,
type: ToastMessage["type"]
): ToastMessage & { fn_index: number } {
return {
message,
fn_index,
type,
id: ++_error_id
};
}
let _error_id = -1;
let user_left_page = false;
document.addEventListener("visibilitychange", function () {
if (document.visibilityState === "hidden") {
user_left_page = true;
}
});
const MESSAGE_QUOTE_RE = /^'([^]+)'$/;
const DUPLICATE_MESSAGE = $_("blocks.long_requests_queue");
const MOBILE_QUEUE_WARNING = $_("blocks.connection_can_break");
const MOBILE_RECONNECT_MESSAGE = $_("blocks.lost_connection");
const SHOW_DUPLICATE_MESSAGE_ON_ETA = 15;
const SHOW_MOBILE_QUEUE_WARNING_ON_ETA = 10;
const is_mobile_device =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
let showed_duplicate_message = false;
let showed_mobile_warning = false;
function get_data(comp: ComponentMeta): any | Promise<any> {
if (comp.instance.get_value) {
return comp.instance.get_value() as Promise<any>;
}
return comp.props.value;
}
async function trigger_api_call(
dep_index: number,
trigger_id: number | null = null,
event_data: unknown = null
): Promise<void> {
let dep = dependencies[dep_index];
const current_status = loading_status.get_status_for_fn(dep_index);
messages = messages.filter(({ fn_index }) => fn_index !== dep_index);
if (dep.cancels) {
await Promise.all(
dep.cancels.map(async (fn_index) => {
const submission = submit_map.get(fn_index);
submission?.cancel();
return submission;
})
);
}
if (current_status === "pending" || current_status === "generating") {
dep.pending_request = true;
}
let payload: Payload = {
fn_index: dep_index,
data: await Promise.all(
dep.inputs.map((id) => get_data(instance_map[id]))
),
event_data: dep.collects_event_data ? event_data : null,
trigger_id: trigger_id
};
if (dep.frontend_fn) {
dep
.frontend_fn(
payload.data.concat(
await Promise.all(
dep.inputs.map((id) => get_data(instance_map[id]))
)
)
)
.then((v: unknown[]) => {
if (dep.backend_fn) {
payload.data = v;
make_prediction(payload);
} else {
handle_update(v, dep_index, []);
}
});
} else {
if (dep.backend_fn) {
if (dep.trigger_mode === "once") {
if (!dep.pending_request) make_prediction(payload);
} else if (dep.trigger_mode === "multiple") {
make_prediction(payload);
} else if (dep.trigger_mode === "always_last") {
if (!dep.pending_request) {
make_prediction(payload);
} else {
dep.final_event = payload;
}
}
}
}
function make_prediction(payload: Payload): void {
const pending_outputs: number[] = [];
let outputs_set_to_non_interactive: number[] = [];
const submission = app
.submit(
payload.fn_index,
payload.data as unknown[],
payload.event_data,
payload.trigger_id
)
.on("data", ({ data, fn_index }) => {
if (dep.pending_request && dep.final_event) {
dep.pending_request = false;
make_prediction(dep.final_event);
}
dep.pending_request = false;
handle_update(data, fn_index, outputs_set_to_non_interactive);
})
.on("status", ({ fn_index, ...status }) => {
tick().then(() => {
const outputs = dependencies[fn_index].outputs;
outputs.forEach((id) => {
if (
instance_map[id].props.interactive &&
status.stage === "pending" &&
dep.targets[0][1] !== "focus"
) {
pending_outputs.push(id);
instance_map[id].props.interactive = false;
} else if (
["complete", "error"].includes(status.stage) &&
pending_outputs.includes(id) &&
!outputs_set_to_non_interactive.includes(id)
) {
instance_map[id].props.interactive = true;
}
});
//@ts-ignore
loading_status.update({
...status,
status: status.stage,
progress: status.progress_data,
fn_index
});
if (
!showed_duplicate_message &&
space_id !== null &&
status.position !== undefined &&
status.position >= 2 &&
status.eta !== undefined &&
status.eta > SHOW_DUPLICATE_MESSAGE_ON_ETA
) {
showed_duplicate_message = true;
messages = [
new_message(DUPLICATE_MESSAGE, fn_index, "warning"),
...messages
];
}
if (
!showed_mobile_warning &&
is_mobile_device &&
status.eta !== undefined &&
status.eta > SHOW_MOBILE_QUEUE_WARNING_ON_ETA
) {
showed_mobile_warning = true;
messages = [
new_message(MOBILE_QUEUE_WARNING, fn_index, "warning"),
...messages
];
}
if (status.stage === "complete") {
dependencies.map(async (dep, i) => {
if (dep.trigger_after === fn_index) {
trigger_api_call(i, payload.trigger_id);
}
});
submission.destroy();
}
if (status.broken && is_mobile_device && user_left_page) {
window.setTimeout(() => {
messages = [
new_message(MOBILE_RECONNECT_MESSAGE, fn_index, "error"),
...messages
];
}, 0);
trigger_api_call(dep_index, payload.trigger_id, event_data);
user_left_page = false;
} else if (status.stage === "error") {
if (status.message) {
const _message = status.message.replace(
MESSAGE_QUOTE_RE,
(_, b) => b
);
messages = [
new_message(_message, fn_index, "error"),
...messages
];
}
dependencies.map(async (dep, i) => {
if (
dep.trigger_after === fn_index &&
!dep.trigger_only_on_success
) {
trigger_api_call(i, payload.trigger_id);
}
});
submission.destroy();
}
});
})
.on("log", ({ log, fn_index, level }) => {
messages = [new_message(log, fn_index, level), ...messages];
});
submit_map.set(dep_index, submission);
}
}
function trigger_share(title: string | undefined, description: string): void {
if (space_id === null) {
return;
}
const discussion_url = new URL(
`https://huggingface.co/spaces/${space_id}/discussions/new`
);
if (title !== undefined && title.length > 0) {
discussion_url.searchParams.set("title", title);
}
discussion_url.searchParams.set("description", description);
window.open(discussion_url.toString(), "_blank");
}
function handle_error_close(e: Event & { detail: number }): void {
const _id = e.detail;
messages = messages.filter((m) => m.id !== _id);
}
const is_external_url = (link: string | null): boolean =>
!!(link && new URL(link, location.href).origin !== location.origin);
async function handle_mount(): Promise<void> {
if (js) {
let blocks_frontend_fn = new AsyncFunction(
`let result = await (${js})();
return (!Array.isArray(result)) ? [result] : result;`
);
blocks_frontend_fn();
}
await tick();
var a = target.getElementsByTagName("a");
for (var i = 0; i < a.length; i++) {
const _target = a[i].getAttribute("target");
const _link = a[i].getAttribute("href");
// only target anchor tags with external links
if (is_external_url(_link) && _target !== "_blank")
a[i].setAttribute("target", "_blank");
}
// handle load triggers
dependencies.forEach((dep, i) => {
if (dep.targets[0][1] === "load") {
trigger_api_call(i);
}
});
if (render_complete) return;
target.addEventListener("gradio", (e: Event) => {
if (!isCustomEvent(e)) throw new Error("not a custom event");
const { id, event, data } = e.detail;
if (event === "share") {
const { title, description } = data as ShareData;
trigger_share(title, description);
} else if (event === "error" || event === "warning") {
messages = [new_message(data, -1, event), ...messages];
} else {
const deps = target_map[id]?.[event];
deps?.forEach((dep_id) => {
trigger_api_call(dep_id, id, data);
});
}
});
render_complete = true;
}
function handle_destroy(id: number): void {
handled_dependencies = handled_dependencies.map((dep) => {
return dep.filter((_id) => _id !== id);
});
}
$: set_status($loading_status);
function set_status(statuses: LoadingStatusCollection): void {
for (const id in statuses) {
let loading_status = statuses[id];
let dependency = dependencies[loading_status.fn_index];
loading_status.scroll_to_output = dependency.scroll_to_output;
loading_status.show_progress = dependency.show_progress;
set_prop(instance_map[id], "loading_status", loading_status);
}
const inputs_to_update = loading_status.get_inputs_to_update();
for (const [id, pending_status] of inputs_to_update) {
set_prop(instance_map[id], "pending", pending_status === "pending");
}
}
function isCustomEvent(event: Event): event is CustomEvent {
return "detail" in event;
}
</script>
<svelte:head>
{#if control_page_title}
<title>{title}</title>
{/if}
{#if analytics_enabled}
<script
async
defer
src="https://www.googletagmanager.com/gtag/js?id=UA-156449732-1"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "UA-156449732-1", {
cookie_flags: "samesite=none;secure"
});
</script>
{/if}
</svelte:head>
<div class="wrap" style:min-height={app_mode ? "100%" : "auto"}>
<div class="contain" style:flex-grow={app_mode ? "1" : "auto"}>
{#if ready}
<MountComponents
{rootNode}
{dynamic_ids}
{instance_map}
{root}
{target}
{theme_mode}
on:mount={handle_mount}
on:destroy={({ detail }) => handle_destroy(detail)}
{version}
{autoscroll}
/>
{/if}
</div>
{#if show_footer}
<footer>
{#if show_api}
<button
on:click={() => {
set_api_docs_visible(!api_docs_visible);
}}
class="show-api"
>
{$_("errors.use_via_api")}
<img src={api_logo} alt={$_("common.logo")} />
</button>
<div>·</div>
{/if}
<a
href="https://gradio.app"
class="built-with"
target="_blank"
rel="noreferrer"
>
{$_("common.built_with_gradio")}
<img src={logo} alt={$_("common.logo")} />
</a>
</footer>
{/if}
</div>
{#if api_docs_visible && ready}
<div class="api-docs">
<!-- TODO: fix -->
<!-- svelte-ignore a11y-click-events-have-key-events-->
<!-- svelte-ignore a11y-no-static-element-interactions-->
<div
class="backdrop"
on:click={() => {
set_api_docs_visible(false);
}}
/>
<div class="api-docs-wrap">
<ApiDocs
on:close={() => {
set_api_docs_visible(false);
}}
{instance_map}
{dependencies}
{root}
{app}
/>
</div>
</div>
{/if}
{#if messages}
<Toast {messages} on:close={handle_error_close} />
{/if}
<style>
.wrap {
display: flex;
flex-grow: 1;
flex-direction: column;
width: var(--size-full);
font-weight: var(--body-text-weight);
font-size: var(--body-text-size);
}
footer {
display: flex;
justify-content: center;
margin-top: var(--size-4);
color: var(--body-text-color-subdued);
}
footer > * + * {
margin-left: var(--size-2);
}
.show-api {
display: flex;
align-items: center;
}
.show-api:hover {
color: var(--body-text-color);
}
.show-api img {
margin-right: var(--size-1);
margin-left: var(--size-2);
width: var(--size-3);
}
.built-with {
display: flex;
align-items: center;
}
.built-with:hover {
color: var(--body-text-color);
}
.built-with img {
margin-right: var(--size-1);
margin-left: var(--size-1);
margin-bottom: 1px;
width: var(--size-4);
}
.api-docs {
display: flex;
position: fixed;
top: 0;
right: 0;
z-index: var(--layer-5);
background: rgba(0, 0, 0, 0.5);
width: var(--size-screen);
height: var(--size-screen-h);
}
.backdrop {
flex: 1 1 0%;
backdrop-filter: blur(4px);
}
.api-docs-wrap {
box-shadow: var(--shadow-drop-lg);
background: var(--background-fill-primary);
overflow-x: hidden;
overflow-y: auto;
}
@media (--screen-md) {
.api-docs-wrap {
border-top-left-radius: var(--radius-lg);
border-bottom-left-radius: var(--radius-lg);
width: 950px;
}
}
@media (--screen-xxl) {
.api-docs-wrap {
width: 1150px;
}
}
</style>