mirror of
https://github.com/gradio-app/gradio.git
synced 2025-01-30 11:00:11 +08:00
69291ff89d
* improve error UX * revert demo * scroll errors into view if necessary * changelog * changelog * fix error * fix * revert demo change * implement review suggestions * ensure error message are only trimmed if the begin and end with single quotes * changelog * attempt 2 at iframe scrolling * tweak
609 lines
14 KiB
Svelte
609 lines
14 KiB
Svelte
<script lang="ts">
|
|
import { tick } from "svelte";
|
|
import { _ } from "svelte-i18n";
|
|
import type { client } from "@gradio/client";
|
|
|
|
import { component_map } from "./components/directory";
|
|
import {
|
|
create_loading_status_store,
|
|
app_state,
|
|
LoadingStatusCollection
|
|
} from "./stores";
|
|
|
|
import type {
|
|
ComponentMeta,
|
|
Dependency,
|
|
LayoutNode,
|
|
Documentation
|
|
} from "./components/types";
|
|
import { setupi18n } from "./i18n";
|
|
import Render from "./Render.svelte";
|
|
import { ApiDocs } from "./api_docs/";
|
|
import type { ThemeMode } from "./components/types";
|
|
import Toast from "./components/StatusTracker/Toast.svelte";
|
|
import type { ToastMessage } from "./components/StatusTracker/types";
|
|
|
|
import logo from "./images/logo.svg";
|
|
import api_logo from "./api_docs/img/api-logo.svg";
|
|
|
|
setupi18n();
|
|
|
|
export let root: string;
|
|
export let components: Array<ComponentMeta>;
|
|
export let layout: LayoutNode;
|
|
export let dependencies: Array<Dependency>;
|
|
|
|
export let title: string = "Gradio";
|
|
export let analytics_enabled: boolean = false;
|
|
export let target: HTMLElement;
|
|
export let autoscroll: boolean;
|
|
export let show_api: boolean = true;
|
|
export let show_footer: boolean = true;
|
|
export let control_page_title = false;
|
|
export let app_mode: boolean;
|
|
export let theme_mode: ThemeMode;
|
|
export let app: Awaited<ReturnType<typeof client>>;
|
|
|
|
let loading_status = create_loading_status_store();
|
|
|
|
$: app_state.update((s) => ({ ...s, autoscroll }));
|
|
|
|
let rootNode: ComponentMeta = {
|
|
id: layout.id,
|
|
type: "column",
|
|
props: {},
|
|
has_modes: false,
|
|
instance: {} as ComponentMeta["instance"],
|
|
component: {} as ComponentMeta["component"]
|
|
};
|
|
|
|
components.push(rootNode);
|
|
|
|
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";
|
|
const set_api_docs_visible = (visible: boolean) => {
|
|
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: Array<Dependency>
|
|
) {
|
|
for (const dep of deps) {
|
|
for (const dep_item of dep[type]) {
|
|
if (dep_item === id) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const dynamic_ids: Set<number> = new Set();
|
|
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);
|
|
}
|
|
}
|
|
|
|
function has_no_default_value(value: any) {
|
|
return (
|
|
(Array.isArray(value) && value.length === 0) ||
|
|
value === "" ||
|
|
value === 0 ||
|
|
!value
|
|
);
|
|
}
|
|
|
|
let instance_map = components.reduce((acc, next) => {
|
|
acc[next.id] = next;
|
|
return acc;
|
|
}, {} as { [id: number]: ComponentMeta });
|
|
|
|
type LoadedComponent = {
|
|
Component: ComponentMeta["component"];
|
|
modes?: Array<string>;
|
|
document?: (arg0: Record<string, unknown>) => Documentation;
|
|
};
|
|
|
|
async function load_component<T extends ComponentMeta["type"]>(
|
|
name: T
|
|
): Promise<{
|
|
name: T;
|
|
component: LoadedComponent;
|
|
}> {
|
|
try {
|
|
const c = await component_map[name]();
|
|
return {
|
|
name,
|
|
component: c as LoadedComponent
|
|
};
|
|
} catch (e) {
|
|
console.error(`failed to load: ${name}`);
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const component_set = new Set<
|
|
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
|
|
>();
|
|
const _component_map = new Map<
|
|
ComponentMeta["type"],
|
|
Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
|
|
>();
|
|
|
|
async function walk_layout(node: LayoutNode) {
|
|
let instance = instance_map[node.id];
|
|
const _component = (await _component_map.get(instance.type))!.component;
|
|
instance.component = _component.Component;
|
|
if (_component.document) {
|
|
instance.documentation = _component.document(instance.props);
|
|
}
|
|
if (_component.modes && _component.modes.length > 1) {
|
|
instance.has_modes = true;
|
|
}
|
|
|
|
if (node.children) {
|
|
instance.children = node.children.map((v) => instance_map[v.id]);
|
|
await Promise.all(node.children.map((v) => walk_layout(v)));
|
|
}
|
|
}
|
|
|
|
components.forEach(async (c) => {
|
|
const _c = load_component(c.type);
|
|
component_set.add(_c);
|
|
_component_map.set(c.type, _c);
|
|
});
|
|
|
|
export let ready = false;
|
|
Promise.all(Array.from(component_set)).then(() => {
|
|
walk_layout(layout)
|
|
.then(async () => {
|
|
ready = true;
|
|
})
|
|
.catch((e) => {
|
|
console.error(e);
|
|
});
|
|
});
|
|
|
|
function handle_update(data: any, fn_index: number) {
|
|
const outputs = dependencies[fn_index].outputs;
|
|
data?.forEach((value: any, i: number) => {
|
|
const output = instance_map[outputs[i]];
|
|
output.props.value_is_output = true;
|
|
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;
|
|
}
|
|
}
|
|
} else {
|
|
output.props.value = value;
|
|
}
|
|
});
|
|
rootNode = rootNode;
|
|
}
|
|
|
|
let submit_map: Map<number, ReturnType<typeof app.submit>> = new Map();
|
|
|
|
function set_prop<T extends ComponentMeta>(obj: T, prop: string, val: any) {
|
|
if (!obj?.props) {
|
|
obj.props = {};
|
|
}
|
|
obj.props[prop] = val;
|
|
rootNode = rootNode;
|
|
}
|
|
let handled_dependencies: Array<number[]> = [];
|
|
|
|
let messages: (ToastMessage & { fn_index: number })[] = [];
|
|
let _error_id = -1;
|
|
const MESSAGE_QUOTE_RE = /^'([^]+)'$/;
|
|
|
|
const trigger_api_call = async (
|
|
dep_index: number,
|
|
event_data: unknown = null
|
|
) => {
|
|
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") {
|
|
return;
|
|
}
|
|
|
|
let payload = {
|
|
fn_index: dep_index,
|
|
data: dep.inputs.map((id) => instance_map[id].props.value),
|
|
event_data: dep.collects_event_data ? event_data : null
|
|
};
|
|
|
|
if (dep.frontend_fn) {
|
|
dep
|
|
.frontend_fn(
|
|
payload.data.concat(
|
|
dep.outputs.map((id) => instance_map[id].props.value)
|
|
)
|
|
)
|
|
.then((v: []) => {
|
|
if (dep.backend_fn) {
|
|
payload.data = v;
|
|
make_prediction();
|
|
} else {
|
|
handle_update(v, dep_index);
|
|
}
|
|
});
|
|
} else {
|
|
if (dep.backend_fn) {
|
|
make_prediction();
|
|
}
|
|
}
|
|
|
|
function make_prediction() {
|
|
const submission = app
|
|
.submit(payload.fn_index, payload.data as unknown[], payload.event_data)
|
|
.on("data", ({ data, fn_index }) => {
|
|
handle_update(data, fn_index);
|
|
})
|
|
.on("status", ({ fn_index, ...status }) => {
|
|
loading_status.update({
|
|
...status,
|
|
status: status.stage,
|
|
progress: status.progress_data,
|
|
fn_index
|
|
});
|
|
|
|
if (status.stage === "complete") {
|
|
dependencies.map(async (dep, i) => {
|
|
if (dep.trigger_after === fn_index) {
|
|
trigger_api_call(i);
|
|
}
|
|
});
|
|
|
|
submission.destroy();
|
|
}
|
|
|
|
if (status.stage === "error") {
|
|
if (status.message) {
|
|
const _message = status.message.replace(
|
|
MESSAGE_QUOTE_RE,
|
|
(_, b) => b
|
|
);
|
|
messages = [
|
|
{
|
|
type: "error",
|
|
message: _message,
|
|
id: ++_error_id,
|
|
fn_index
|
|
},
|
|
...messages
|
|
];
|
|
}
|
|
dependencies.map(async (dep, i) => {
|
|
if (
|
|
dep.trigger_after === fn_index &&
|
|
!dep.trigger_only_on_success
|
|
) {
|
|
trigger_api_call(i);
|
|
}
|
|
});
|
|
|
|
submission.destroy();
|
|
}
|
|
});
|
|
|
|
submit_map.set(dep_index, submission);
|
|
}
|
|
};
|
|
|
|
function handle_error_close(e: Event & { detail: number }) {
|
|
const _id = e.detail;
|
|
messages = messages.filter((m) => m.id !== _id);
|
|
}
|
|
|
|
const is_external_url = (link: string | null) =>
|
|
link && new URL(link, location.href).origin !== location.origin;
|
|
|
|
async function handle_mount() {
|
|
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");
|
|
}
|
|
|
|
dependencies.forEach((dep, i) => {
|
|
let { targets, trigger, inputs, outputs } = dep;
|
|
const target_instances: [number, ComponentMeta][] = targets.map((t) => [
|
|
t,
|
|
instance_map[t]
|
|
]);
|
|
|
|
// page events
|
|
if (
|
|
targets.length === 0 &&
|
|
!handled_dependencies[i]?.includes(-1) &&
|
|
trigger === "load" &&
|
|
// check all input + output elements are on the page
|
|
outputs.every((v) => instance_map?.[v].instance) &&
|
|
inputs.every((v) => instance_map?.[v].instance)
|
|
) {
|
|
trigger_api_call(i);
|
|
handled_dependencies[i] = [-1];
|
|
}
|
|
|
|
target_instances
|
|
.filter((v) => !!v && !!v[1])
|
|
.forEach(([id, { instance }]: [number, ComponentMeta]) => {
|
|
if (handled_dependencies[i]?.includes(id) || !instance) return;
|
|
instance?.$on(trigger, (event_data) => {
|
|
trigger_api_call(i, event_data.detail);
|
|
});
|
|
|
|
if (!handled_dependencies[i]) handled_dependencies[i] = [];
|
|
handled_dependencies[i].push(id);
|
|
});
|
|
});
|
|
}
|
|
|
|
function handle_destroy(id: number) {
|
|
handled_dependencies = handled_dependencies.map((dep) => {
|
|
return dep.filter((_id) => _id !== id);
|
|
});
|
|
}
|
|
|
|
$: set_status($loading_status);
|
|
|
|
dependencies.forEach((v, i) => {
|
|
loading_status.register(i, v.inputs, v.outputs);
|
|
});
|
|
|
|
function set_status(statuses: LoadingStatusCollection) {
|
|
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");
|
|
}
|
|
}
|
|
</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");
|
|
</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}
|
|
<Render
|
|
has_modes={rootNode.has_modes}
|
|
component={rootNode.component}
|
|
id={rootNode.id}
|
|
props={rootNode.props}
|
|
children={rootNode.children}
|
|
{dynamic_ids}
|
|
{instance_map}
|
|
{root}
|
|
{target}
|
|
{theme_mode}
|
|
on:mount={handle_mount}
|
|
on:destroy={({ detail }) => handle_destroy(detail)}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if show_footer}
|
|
<footer>
|
|
{#if show_api}
|
|
<button
|
|
on:click={() => {
|
|
set_api_docs_visible(!api_docs_visible);
|
|
}}
|
|
class="show-api"
|
|
>
|
|
Use via API <img src={api_logo} alt="" />
|
|
</button>
|
|
<div>·</div>
|
|
{/if}
|
|
<a
|
|
href="https://gradio.app"
|
|
class="built-with"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Built with Gradio
|
|
<img src={logo} alt="logo" />
|
|
</a>
|
|
</footer>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if api_docs_visible && ready}
|
|
<div class="api-docs">
|
|
<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-2);
|
|
width: var(--size-3);
|
|
}
|
|
|
|
.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>
|