2
import { load_component } from "virtual:component-loader";
4
import { tick } from "svelte";
5
import { _ } from "svelte-i18n";
6
import type { client } from "@gradio/client";
8
import { create_loading_status_store } from "./stores";
9
import type { LoadingStatusCollection } from "./stores";
11
import type { ComponentMeta, Dependency, LayoutNode } from "./types";
12
import { setupi18n } from "./i18n";
13
import { ApiDocs } from "./api_docs/";
14
import type { ThemeMode, Payload } from "./types";
15
import { Toast } from "@gradio/statustracker";
16
import type { ToastMessage } from "@gradio/statustracker";
17
import type { ShareData } from "@gradio/utils";
18
import MountComponents from "./MountComponents.svelte";
20
import logo from "./images/logo.svg";
21
import api_logo from "./api_docs/img/api-logo.svg";
25
export let root: string;
26
export let components: ComponentMeta[];
27
export let layout: LayoutNode;
28
export let dependencies: Dependency[];
29
export let title = "Gradio";
30
export let analytics_enabled = false;
31
export let target: HTMLElement;
32
export let autoscroll: boolean;
33
export let show_api = true;
34
export let show_footer = true;
35
export let control_page_title = false;
36
export let app_mode: boolean;
37
export let theme_mode: ThemeMode;
38
export let app: Awaited<ReturnType<typeof client>>;
39
export let space_id: string | null;
40
export let version: string;
41
export let js: string | null;
42
export let fill_height = false;
44
let loading_status = create_loading_status_store();
46
let rootNode: ComponentMeta = {
49
props: { interactive: false, scale: fill_height ? 1 : null },
51
instance: null as unknown as ComponentMeta["instance"],
52
component: null as unknown as ComponentMeta["component"],
53
component_class_id: ""
56
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
57
dependencies.forEach((d) => {
59
const wrap = d.backend_fn
60
? d.inputs.length === 1
61
: d.outputs.length === 1;
63
d.frontend_fn = new AsyncFunction(
65
`let result = await (${d.js})(...__fn_args);
66
return (${wrap} && !Array.isArray(result)) ? [result] : result;`
69
console.error("Could not parse custom js method.");
75
let params = new URLSearchParams(window.location.search);
76
let api_docs_visible = params.get("view") === "api" && show_api;
77
function set_api_docs_visible(visible: boolean): void {
78
api_docs_visible = visible;
79
let params = new URLSearchParams(window.location.search);
81
params.set("view", "api");
83
params.delete("view");
85
history.replaceState(null, "", "?" + params.toString());
90
type: "inputs" | "outputs",
93
for (const dep of deps) {
94
for (const dep_item of dep[type]) {
95
if (dep_item === id) return true;
101
let dynamic_ids: Set<number> = new Set();
103
function has_no_default_value(value: any): boolean {
105
(Array.isArray(value) && value.length === 0) ||
112
let instance_map: { [id: number]: ComponentMeta };
114
type LoadedComponent = {
115
default: ComponentMeta["component"];
118
let component_set = new Set<
119
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
122
let _component_map = new Map<
123
`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
124
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
127
async function walk_layout(
129
type_map: Map<number, ComponentMeta["props"]["interactive"]>,
130
instance_map: { [id: number]: ComponentMeta },
132
`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
133
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
137
let instance = instance_map[node.id];
139
const _component = (await component_map.get(
140
`${instance.type}_${type_map.get(node.id) || "false"}`
142
instance.component = _component.default;
145
instance.children = node.children.map((v) => instance_map[v.id]);
147
node.children.map((v) =>
148
walk_layout(v, type_map, instance_map, component_map)
154
export let ready = false;
155
export let render_complete = false;
157
$: components, layout, prepare_components();
159
let target_map: Record<number, Record<string, number[]>> = {};
161
function prepare_components(): void {
162
target_map = dependencies.reduce(
164
dep.targets.forEach(([id, trigger]) => {
168
if (acc[id]?.[trigger]) {
169
acc[id][trigger].push(i);
171
acc[id][trigger] = [i];
177
{} as Record<number, Record<string, number[]>>
179
loading_status = create_loading_status_store();
181
dependencies.forEach((v, i) => {
182
loading_status.register(i, v.inputs, v.outputs);
185
const _dynamic_ids = new Set<number>();
186
for (const comp of components) {
187
const { id, props } = comp;
188
const is_input = is_dep(id, "inputs", dependencies);
191
(!is_dep(id, "outputs", dependencies) &&
192
has_no_default_value(props?.value))
194
_dynamic_ids.add(id);
198
dynamic_ids = _dynamic_ids;
200
const _rootNode: typeof rootNode = {
203
props: { interactive: false, scale: fill_height ? 1 : null },
205
instance: null as unknown as ComponentMeta["instance"],
206
component: null as unknown as ComponentMeta["component"],
207
component_class_id: ""
209
components.push(_rootNode);
210
const _component_set = new Set<
211
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
213
const __component_map = new Map<
214
`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
215
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
217
const __type_for_id = new Map<
219
ComponentMeta["props"]["interactive"]
221
const _instance_map = components.reduce(
226
{} as { [id: number]: ComponentMeta }
228
components.forEach((c) => {
229
if ((c.props as any).interactive === false) {
230
(c.props as any).interactive = false;
231
} else if ((c.props as any).interactive === true) {
232
(c.props as any).interactive = true;
233
} else if (dynamic_ids.has(c.id)) {
234
(c.props as any).interactive = true;
236
(c.props as any).interactive = false;
239
if ((c.props as any).server_fns) {
240
let server: Record<string, (...args: any[]) => Promise<any>> = {};
241
(c.props as any).server_fns.forEach((fn: string) => {
242
server[fn] = async (...args: any[]) => {
243
if (args.length === 1) {
246
const result = await app.component_server(c.id, fn, args);
250
(c.props as any).server = server;
253
if (target_map[c.id]) {
254
c.props.attached_events = Object.keys(target_map[c.id]);
256
__type_for_id.set(c.id, c.props.interactive);
258
if (c.type === "dataset") {
259
const example_component_map = new Map();
261
(c.props.components as string[]).forEach((name: string) => {
262
if (example_component_map.has(name)) {
267
const matching_component = components.find((c) => c.type === name);
268
if (matching_component) {
269
_c = load_component({
272
id: matching_component.component_class_id,
275
example_component_map.set(name, _c);
279
c.props.component_map = example_component_map;
284
const _c = load_component({
287
id: c.component_class_id,
290
_component_set.add(_c);
291
__component_map.set(`${c.type}_${c.props.interactive}`, _c);
294
Promise.all(Array.from(_component_set)).then(() => {
295
walk_layout(layout, __type_for_id, _instance_map, __component_map)
298
component_set = _component_set;
299
_component_map = __component_map;
300
instance_map = _instance_map;
301
rootNode = _rootNode;
309
function throttle<T extends (...args: any[]) => any>(
312
): (...funcArgs: Parameters<T>) => void {
313
let lastFunc: ReturnType<typeof setTimeout>;
316
let lastArgs: IArguments | null;
318
return function (this: any, ...args: Parameters<T>) {
320
func.apply(this, args);
321
lastRan = Date.now();
323
clearTimeout(lastFunc);
325
lastArgs = arguments;
327
lastFunc = setTimeout(
329
if (Date.now() - lastRan >= limit) {
331
func.apply(lastThis, Array.prototype.slice.call(lastArgs));
333
lastRan = Date.now();
336
Math.max(limit - (Date.now() - lastRan), 0)
342
const refresh = throttle(() => {
346
async function handle_update(data: any, fn_index: number): Promise<void> {
347
const outputs = dependencies[fn_index].outputs;
349
data?.forEach((value: any, i: number) => {
350
const output = instance_map[outputs[i]];
351
output.props.value_is_output = true;
356
data?.forEach((value: any, i: number) => {
357
const output = instance_map[outputs[i]];
359
typeof value === "object" &&
361
value.__type__ === "update"
363
for (const [update_key, update_value] of Object.entries(value)) {
364
if (update_key === "__type__") {
367
output.props[update_key] = update_value;
371
output.props.value = value;
377
let submit_map: Map<number, ReturnType<typeof app.submit>> = new Map();
379
function set_prop<T extends ComponentMeta>(
388
obj.props[prop] = val;
391
let handled_dependencies: number[][] = [];
393
let messages: (ToastMessage & { fn_index: number })[] = [];
394
function new_message(
397
type: ToastMessage["type"]
398
): ToastMessage & { fn_index: number } {
409
let user_left_page = false;
410
document.addEventListener("visibilitychange", function () {
411
if (document.visibilityState === "hidden") {
412
user_left_page = true;
416
const MESSAGE_QUOTE_RE = /^'([^]+)'$/;
418
const DUPLICATE_MESSAGE = $_("blocks.long_requests_queue");
419
const MOBILE_QUEUE_WARNING = $_("blocks.connection_can_break");
420
const MOBILE_RECONNECT_MESSAGE = $_("blocks.lost_connection");
421
const SHOW_DUPLICATE_MESSAGE_ON_ETA = 15;
422
const SHOW_MOBILE_QUEUE_WARNING_ON_ETA = 10;
423
const is_mobile_device =
424
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
427
let showed_duplicate_message = false;
428
let showed_mobile_warning = false;
430
function get_data(comp: ComponentMeta): any | Promise<any> {
431
if (comp.instance.get_value) {
432
return comp.instance.get_value() as Promise<any>;
434
return comp.props.value;
437
async function trigger_api_call(
439
trigger_id: number | null = null,
440
event_data: unknown = null
442
let dep = dependencies[dep_index];
443
const current_status = loading_status.get_status_for_fn(dep_index);
444
messages = messages.filter(({ fn_index }) => fn_index !== dep_index);
447
dep.cancels.map(async (fn_index) => {
448
const submission = submit_map.get(fn_index);
449
submission?.cancel();
454
if (current_status === "pending" || current_status === "generating") {
455
dep.pending_request = true;
458
let payload: Payload = {
460
data: await Promise.all(
461
dep.inputs.map((id) => get_data(instance_map[id]))
463
event_data: dep.collects_event_data ? event_data : null,
464
trigger_id: trigger_id
467
if (dep.frontend_fn) {
472
dep.inputs.map((id) => get_data(instance_map[id]))
476
.then((v: unknown[]) => {
477
if (dep.backend_fn) {
479
make_prediction(payload);
481
handle_update(v, dep_index);
485
if (dep.backend_fn) {
486
if (dep.trigger_mode === "once") {
487
if (!dep.pending_request) make_prediction(payload);
488
} else if (dep.trigger_mode === "multiple") {
489
make_prediction(payload);
490
} else if (dep.trigger_mode === "always_last") {
491
if (!dep.pending_request) {
492
make_prediction(payload);
494
dep.final_event = payload;
500
function make_prediction(payload: Payload): void {
501
const submission = app
504
payload.data as unknown[],
508
.on("data", ({ data, fn_index }) => {
509
if (dep.pending_request && dep.final_event) {
510
dep.pending_request = false;
511
make_prediction(dep.final_event);
513
dep.pending_request = false;
514
handle_update(data, fn_index);
516
.on("status", ({ fn_index, ...status }) => {
519
loading_status.update({
521
status: status.stage,
522
progress: status.progress_data,
526
!showed_duplicate_message &&
528
status.position !== undefined &&
529
status.position >= 2 &&
530
status.eta !== undefined &&
531
status.eta > SHOW_DUPLICATE_MESSAGE_ON_ETA
533
showed_duplicate_message = true;
535
new_message(DUPLICATE_MESSAGE, fn_index, "warning"),
540
!showed_mobile_warning &&
542
status.eta !== undefined &&
543
status.eta > SHOW_MOBILE_QUEUE_WARNING_ON_ETA
545
showed_mobile_warning = true;
547
new_message(MOBILE_QUEUE_WARNING, fn_index, "warning"),
552
if (status.stage === "complete") {
553
dependencies.map(async (dep, i) => {
554
if (dep.trigger_after === fn_index) {
555
trigger_api_call(i, payload.trigger_id);
559
submission.destroy();
561
if (status.broken && is_mobile_device && user_left_page) {
562
window.setTimeout(() => {
564
new_message(MOBILE_RECONNECT_MESSAGE, fn_index, "error"),
568
trigger_api_call(dep_index, payload.trigger_id, event_data);
569
user_left_page = false;
570
} else if (status.stage === "error") {
571
if (status.message) {
572
const _message = status.message.replace(
577
new_message(_message, fn_index, "error"),
581
dependencies.map(async (dep, i) => {
583
dep.trigger_after === fn_index &&
584
!dep.trigger_only_on_success
586
trigger_api_call(i, payload.trigger_id);
590
submission.destroy();
594
.on("log", ({ log, fn_index, level }) => {
595
messages = [new_message(log, fn_index, level), ...messages];
598
submit_map.set(dep_index, submission);
602
function trigger_share(title: string | undefined, description: string): void {
603
if (space_id === null) {
606
const discussion_url = new URL(
607
`https://huggingface.co/spaces/${space_id}/discussions/new`
609
if (title !== undefined && title.length > 0) {
610
discussion_url.searchParams.set("title", title);
612
discussion_url.searchParams.set("description", description);
613
window.open(discussion_url.toString(), "_blank");
616
function handle_error_close(e: Event & { detail: number }): void {
617
const _id = e.detail;
618
messages = messages.filter((m) => m.id !== _id);
621
const is_external_url = (link: string | null): boolean =>
622
!!(link && new URL(link, location.href).origin !== location.origin);
624
async function handle_mount(): Promise<void> {
626
let blocks_frontend_fn = new AsyncFunction(
627
`let result = await (${js})();
628
return (!Array.isArray(result)) ? [result] : result;`
630
blocks_frontend_fn();
635
var a = target.getElementsByTagName("a");
637
for (var i = 0; i < a.length; i++) {
638
const _target = a[i].getAttribute("target");
639
const _link = a[i].getAttribute("href");
641
// only target anchor tags with external links
642
if (is_external_url(_link) && _target !== "_blank")
643
a[i].setAttribute("target", "_blank");
646
// handle load triggers
647
dependencies.forEach((dep, i) => {
648
if (dep.targets[0][1] === "load") {
653
if (render_complete) return;
654
target.addEventListener("gradio", (e: Event) => {
655
if (!isCustomEvent(e)) throw new Error("not a custom event");
657
const { id, event, data } = e.detail;
659
if (event === "share") {
660
const { title, description } = data as ShareData;
661
trigger_share(title, description);
662
} else if (event === "error" || event === "warning") {
663
messages = [new_message(data, -1, event), ...messages];
665
const deps = target_map[id]?.[event];
666
deps?.forEach((dep_id) => {
667
trigger_api_call(dep_id, id, data);
672
render_complete = true;
675
function handle_destroy(id: number): void {
676
handled_dependencies = handled_dependencies.map((dep) => {
677
return dep.filter((_id) => _id !== id);
681
$: set_status($loading_status);
683
function set_status(statuses: LoadingStatusCollection): void {
684
for (const id in statuses) {
685
let loading_status = statuses[id];
686
let dependency = dependencies[loading_status.fn_index];
687
loading_status.scroll_to_output = dependency.scroll_to_output;
688
loading_status.show_progress = dependency.show_progress;
690
set_prop(instance_map[id], "loading_status", loading_status);
692
const inputs_to_update = loading_status.get_inputs_to_update();
693
for (const [id, pending_status] of inputs_to_update) {
694
set_prop(instance_map[id], "pending", pending_status === "pending");
698
function isCustomEvent(event: Event): event is CustomEvent {
699
return "detail" in event;
704
{#if control_page_title}
705
<title>{title}</title>
707
{#if analytics_enabled}
711
src="https://www.googletagmanager.com/gtag/js?id=UA-156449732-1"
714
window.dataLayer = window.dataLayer || [];
716
dataLayer.push(arguments);
718
gtag("js", new Date());
719
gtag("config", "UA-156449732-1", {
720
cookie_flags: "samesite=none;secure"
726
<div class="wrap" style:min-height={app_mode ? "100%" : "auto"}>
727
<div class="contain" style:flex-grow={app_mode ? "1" : "auto"}>
736
on:mount={handle_mount}
737
on:destroy={({ detail }) => handle_destroy(detail)}
749
set_api_docs_visible(!api_docs_visible);
753
{$_("errors.use_via_api")}
754
<img src={api_logo} alt={$_("common.logo")} />
759
href="https://gradio.app"
764
{$_("common.built_with_gradio")}
765
<img src={logo} alt={$_("common.logo")} />
771
{#if api_docs_visible && ready}
772
<div class="api-docs">
774
<!-- svelte-ignore a11y-click-events-have-key-events-->
775
<!-- svelte-ignore a11y-no-static-element-interactions-->
779
set_api_docs_visible(false);
782
<div class="api-docs-wrap">
785
set_api_docs_visible(false);
798
<Toast {messages} on:close={handle_error_close} />
805
flex-direction: column;
806
width: var(--size-full);
807
font-weight: var(--body-text-weight);
808
font-size: var(--body-text-size);
813
flex-direction: column;
818
justify-content: center;
819
margin-top: var(--size-4);
820
color: var(--body-text-color-subdued);
824
margin-left: var(--size-2);
832
color: var(--body-text-color);
836
margin-right: var(--size-1);
837
margin-left: var(--size-2);
838
width: var(--size-3);
847
color: var(--body-text-color);
851
margin-right: var(--size-1);
852
margin-left: var(--size-1);
854
width: var(--size-4);
862
z-index: var(--layer-5);
863
background: rgba(0, 0, 0, 0.5);
864
width: var(--size-screen);
865
height: var(--size-screen-h);
870
-webkit-backdrop-filter: blur(4px);
871
backdrop-filter: blur(4px);
875
box-shadow: var(--shadow-drop-lg);
876
background: var(--background-fill-primary);
881
@media (--screen-md) {
883
border-top-left-radius: var(--radius-lg);
884
border-bottom-left-radius: var(--radius-lg);
889
@media (--screen-xxl) {