|
<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; |
|
} |
|
|
|
|
|
|
|
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; |
|
} |
|
}); |
|
|
|
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"); |
|
} |
|
|
|
|
|
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"> |
|
|
|
|
|
|
|
<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> |
|
|