|
<script lang="ts"> |
|
import { tick, onMount } from "svelte"; |
|
import { _ } from "svelte-i18n"; |
|
import { Client } from "@gradio/client"; |
|
|
|
import type { LoadingStatus, LoadingStatusCollection } from "./stores"; |
|
|
|
import type { ComponentMeta, Dependency, LayoutNode } from "./types"; |
|
import type { UpdateTransaction } from "./init"; |
|
import { setupi18n } from "./i18n"; |
|
import { ApiDocs, ApiRecorder } from "./api_docs/"; |
|
import type { ThemeMode, Payload } from "./types"; |
|
import { Toast } from "@gradio/statustracker"; |
|
import type { ToastMessage } from "@gradio/statustracker"; |
|
import type { ShareData, ValueData } from "@gradio/utils"; |
|
import MountComponents from "./MountComponents.svelte"; |
|
import { prefix_css } from "./css"; |
|
|
|
import logo from "./images/logo.svg"; |
|
import api_logo from "./api_docs/img/api-logo.svg"; |
|
import { create_components, AsyncFunction } from "./init"; |
|
import type { |
|
LogMessage, |
|
RenderMessage, |
|
StatusMessage |
|
} from "@gradio/client"; |
|
|
|
setupi18n(); |
|
|
|
export let root: string; |
|
export let components: ComponentMeta[]; |
|
export let layout: LayoutNode; |
|
export let dependencies: Dependency[]; |
|
export let title = "Gradio"; |
|
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.connect>>; |
|
export let space_id: string | null; |
|
export let version: string; |
|
export let js: string | null; |
|
export let fill_height = false; |
|
export let ready: boolean; |
|
export let username: string | null; |
|
export let api_prefix = ""; |
|
export let max_file_size: number | undefined = undefined; |
|
export let initial_layout: ComponentMeta | undefined = undefined; |
|
export let css: string | null | undefined = null; |
|
let { |
|
layout: _layout, |
|
targets, |
|
update_value, |
|
get_data, |
|
modify_stream, |
|
get_stream_state, |
|
set_time_limit, |
|
loading_status, |
|
scheduled_updates, |
|
create_layout, |
|
rerender_layout |
|
} = create_components(initial_layout); |
|
|
|
$: components, layout, dependencies, root, app, fill_height, target, run(); |
|
|
|
$: { |
|
ready = !!$_layout; |
|
} |
|
|
|
async function run(): Promise<void> { |
|
await create_layout({ |
|
components, |
|
layout, |
|
dependencies, |
|
root: root + api_prefix, |
|
app, |
|
options: { |
|
fill_height |
|
} |
|
}); |
|
} |
|
|
|
export let search_params: URLSearchParams; |
|
let api_docs_visible = search_params.get("view") === "api" && show_api; |
|
let api_recorder_visible = |
|
search_params.get("view") === "api-recorder" && show_api; |
|
function set_api_docs_visible(visible: boolean): void { |
|
api_recorder_visible = false; |
|
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()); |
|
} |
|
let api_calls: Payload[] = []; |
|
|
|
export let render_complete = false; |
|
async function handle_update(data: any, fn_index: number): Promise<void> { |
|
const outputs = dependencies.find((dep) => dep.id == fn_index)!.outputs; |
|
|
|
const meta_updates = data?.map((value: any, i: number) => { |
|
return { |
|
id: outputs[i], |
|
prop: "value_is_output", |
|
value: true |
|
}; |
|
}); |
|
|
|
update_value(meta_updates); |
|
|
|
await tick(); |
|
|
|
const updates: UpdateTransaction[] = []; |
|
|
|
data?.forEach((value: any, i: number) => { |
|
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 { |
|
updates.push({ |
|
id: outputs[i], |
|
prop: update_key, |
|
value: update_value |
|
}); |
|
} |
|
} |
|
} else { |
|
updates.push({ |
|
id: outputs[i], |
|
prop: "value", |
|
value |
|
}); |
|
} |
|
}); |
|
update_value(updates); |
|
|
|
await tick(); |
|
} |
|
|
|
let submit_map: Map<number, ReturnType<typeof app.submit>> = new Map(); |
|
|
|
let messages: (ToastMessage & { fn_index: number })[] = []; |
|
function new_message( |
|
message: string, |
|
fn_index: number, |
|
type: ToastMessage["type"], |
|
duration: number | null = 10, |
|
visible = true |
|
): ToastMessage & { fn_index: number } { |
|
return { |
|
message, |
|
fn_index, |
|
type, |
|
id: ++_error_id, |
|
duration, |
|
visible |
|
}; |
|
} |
|
|
|
export function add_new_message( |
|
message: string, |
|
type: ToastMessage["type"] |
|
): void { |
|
messages = [new_message(message, -1, type), ...messages]; |
|
} |
|
|
|
let _error_id = -1; |
|
|
|
let user_left_page = false; |
|
|
|
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 WAITING_FOR_INPUTS_MESSAGE = $_("blocks.waiting_for_inputs"); |
|
const SHOW_DUPLICATE_MESSAGE_ON_ETA = 15; |
|
const SHOW_MOBILE_QUEUE_WARNING_ON_ETA = 10; |
|
let is_mobile_device = false; |
|
let showed_duplicate_message = false; |
|
let showed_mobile_warning = false; |
|
let inputs_waiting: number[] = []; |
|
|
|
|
|
function wait_then_trigger_api_call( |
|
dep_index: number, |
|
trigger_id: number | null = null, |
|
event_data: unknown = null |
|
): void { |
|
let _unsub = (): void => {}; |
|
function unsub(): void { |
|
_unsub(); |
|
} |
|
if ($scheduled_updates) { |
|
_unsub = scheduled_updates.subscribe((updating) => { |
|
if (!updating) { |
|
tick().then(() => { |
|
trigger_api_call(dep_index, trigger_id, event_data); |
|
unsub(); |
|
}); |
|
} |
|
}); |
|
} else { |
|
trigger_api_call(dep_index, trigger_id, event_data); |
|
} |
|
} |
|
|
|
async function get_component_value_or_event_data( |
|
component_id: number, |
|
trigger_id: number | null, |
|
event_data: unknown |
|
): Promise<any> { |
|
if ( |
|
component_id === trigger_id && |
|
event_data && |
|
(event_data as ValueData).is_value_data === true |
|
) { |
|
// @ts-ignore |
|
return event_data.value; |
|
} |
|
return get_data(component_id); |
|
} |
|
|
|
async function trigger_api_call( |
|
dep_index: number, |
|
trigger_id: number | null = null, |
|
event_data: unknown = null |
|
): Promise<void> { |
|
let dep = dependencies.find((dep) => dep.id === dep_index)!; |
|
if (inputs_waiting.length > 0) { |
|
for (const input of inputs_waiting) { |
|
if (dep.inputs.includes(input)) { |
|
add_new_message(WAITING_FOR_INPUTS_MESSAGE, "warning"); |
|
return; |
|
} |
|
} |
|
} |
|
const current_status = loading_status.get_status_for_fn(dep_index); |
|
messages = messages.filter(({ fn_index }) => fn_index !== dep_index); |
|
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_component_value_or_event_data(id, trigger_id, event_data) |
|
) |
|
), |
|
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.outputs.map((id) => get_data(id))) |
|
) |
|
) |
|
.then((v: unknown[]) => { |
|
if (dep.backend_fn) { |
|
payload.data = v; |
|
trigger_prediction(dep, payload); |
|
} else { |
|
handle_update(v, dep_index); |
|
} |
|
}); |
|
} else if (dep.types.cancel && dep.cancels) { |
|
await Promise.all( |
|
dep.cancels.map(async (fn_index) => { |
|
const submission = submit_map.get(fn_index); |
|
submission?.cancel(); |
|
return submission; |
|
}) |
|
); |
|
} else { |
|
if (dep.backend_fn) { |
|
trigger_prediction(dep, payload); |
|
} |
|
} |
|
|
|
function trigger_prediction(dep: Dependency, payload: Payload): void { |
|
if (dep.trigger_mode === "once") { |
|
if (!dep.pending_request) |
|
make_prediction(payload, dep.connection == "stream"); |
|
} else if (dep.trigger_mode === "multiple") { |
|
make_prediction(payload, dep.connection == "stream"); |
|
} else if (dep.trigger_mode === "always_last") { |
|
if (!dep.pending_request) { |
|
make_prediction(payload, dep.connection == "stream"); |
|
} else { |
|
dep.final_event = payload; |
|
} |
|
} |
|
} |
|
|
|
async function make_prediction( |
|
payload: Payload, |
|
streaming = false |
|
): Promise<void> { |
|
if (api_recorder_visible) { |
|
api_calls = [...api_calls, JSON.parse(JSON.stringify(payload))]; |
|
} |
|
|
|
let submission: ReturnType<typeof app.submit>; |
|
app.set_current_payload(payload); |
|
if (streaming) { |
|
if (!submit_map.has(dep_index)) { |
|
dep.inputs.forEach((id) => modify_stream(id, "waiting")); |
|
} else if ( |
|
submit_map.has(dep_index) && |
|
dep.inputs.some((id) => get_stream_state(id) === "waiting") |
|
) { |
|
return; |
|
} else if ( |
|
submit_map.has(dep_index) && |
|
dep.inputs.some((id) => get_stream_state(id) === "open") |
|
) { |
|
await app.send_ws_message( |
|
// @ts-ignore |
|
`${app.config.root + app.config.api_prefix}/stream/${submit_map.get(dep_index).event_id()}`, |
|
{ ...payload, session_hash: app.session_hash } |
|
); |
|
return; |
|
} |
|
} |
|
try { |
|
submission = app.submit( |
|
payload.fn_index, |
|
payload.data as unknown[], |
|
payload.event_data, |
|
payload.trigger_id |
|
); |
|
} catch (e) { |
|
const fn_index = 0; // Mock value for fn_index |
|
messages = [new_message(String(e), fn_index, "error"), ...messages]; |
|
loading_status.update({ |
|
status: "error", |
|
fn_index, |
|
eta: 0, |
|
queue: false, |
|
queue_position: null |
|
}); |
|
set_status($loading_status); |
|
return; |
|
} |
|
|
|
submit_map.set(dep_index, submission); |
|
|
|
for await (const message of submission) { |
|
if (message.type === "data") { |
|
handle_data(message); |
|
} else if (message.type === "render") { |
|
handle_render(message); |
|
} else if (message.type === "status") { |
|
handle_status_update(message); |
|
} else if (message.type === "log") { |
|
handle_log(message); |
|
} |
|
} |
|
|
|
function handle_data(message: Payload): void { |
|
const { data, fn_index } = message; |
|
if (dep.pending_request && dep.final_event) { |
|
dep.pending_request = false; |
|
make_prediction(dep.final_event, dep.connection == "stream"); |
|
} |
|
dep.pending_request = false; |
|
handle_update(data, fn_index); |
|
set_status($loading_status); |
|
} |
|
|
|
function handle_render(message: RenderMessage): void { |
|
const { data } = message; |
|
let _components: ComponentMeta[] = data.components; |
|
let render_layout: LayoutNode = data.layout; |
|
let _dependencies: Dependency[] = data.dependencies; |
|
let render_id = data.render_id; |
|
|
|
let deps_to_remove: number[] = []; |
|
dependencies.forEach((dep, i) => { |
|
if (dep.rendered_in === render_id) { |
|
deps_to_remove.push(i); |
|
} |
|
}); |
|
deps_to_remove.reverse().forEach((i) => { |
|
dependencies.splice(i, 1); |
|
}); |
|
_dependencies.forEach((dep) => { |
|
dependencies.push(dep); |
|
}); |
|
|
|
rerender_layout({ |
|
components: _components, |
|
layout: render_layout, |
|
root: root, |
|
dependencies: dependencies, |
|
render_id: render_id |
|
}); |
|
} |
|
|
|
function handle_log(msg: LogMessage): void { |
|
const { log, fn_index, level, duration, visible } = msg; |
|
messages = [ |
|
new_message(log, fn_index, level, duration, visible), |
|
...messages |
|
]; |
|
} |
|
|
|
function open_stream_events( |
|
status: StatusMessage, |
|
id: number, |
|
dep: Dependency |
|
): void { |
|
if ( |
|
status.original_msg === "process_starts" && |
|
dep.connection === "stream" |
|
) { |
|
modify_stream(id, "open"); |
|
} |
|
} |
|
|
|
|
|
function handle_status_update(message: StatusMessage): void { |
|
const { fn_index, ...status } = message; |
|
if (status.stage === "streaming" && status.time_limit) { |
|
dep.inputs.forEach((id) => { |
|
set_time_limit(id, status.time_limit); |
|
}); |
|
} |
|
dep.inputs.forEach((id) => { |
|
open_stream_events(message, id, dep); |
|
}); |
|
|
|
loading_status.update({ |
|
...status, |
|
time_limit: status.time_limit, |
|
status: status.stage, |
|
progress: status.progress_data, |
|
fn_index |
|
}); |
|
set_status($loading_status); |
|
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" || status.stage === "generating") { |
|
status.changed_state_ids?.forEach((id) => { |
|
dependencies |
|
.filter((dep) => dep.targets.some(([_id, _]) => _id === id)) |
|
.forEach((dep) => { |
|
wait_then_trigger_api_call(dep.id, payload.trigger_id); |
|
}); |
|
}); |
|
} |
|
if (status.stage === "complete") { |
|
dependencies.forEach(async (dep) => { |
|
if (dep.trigger_after === fn_index) { |
|
wait_then_trigger_api_call(dep.id, payload.trigger_id); |
|
} |
|
}); |
|
dep.inputs.forEach((id) => { |
|
modify_stream(id, "closed"); |
|
}); |
|
submit_map.delete(dep_index); |
|
} |
|
if (status.broken && is_mobile_device && user_left_page) { |
|
window.setTimeout(() => { |
|
messages = [ |
|
new_message(MOBILE_RECONNECT_MESSAGE, fn_index, "error"), |
|
...messages |
|
]; |
|
}, 0); |
|
wait_then_trigger_api_call(dep.id, 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", |
|
status.duration, |
|
status.visible |
|
), |
|
...messages |
|
]; |
|
} |
|
dependencies.map(async (dep) => { |
|
if ( |
|
dep.trigger_after === fn_index && |
|
!dep.trigger_only_on_success |
|
) { |
|
wait_then_trigger_api_call(dep.id, payload.trigger_id); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
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;` |
|
); |
|
await 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) => { |
|
if (dep.targets.some((dep) => dep[1] === "load")) { |
|
wait_then_trigger_api_call(dep.id); |
|
} |
|
}); |
|
|
|
if (!target || render_complete) return; |
|
|
|
target.addEventListener("prop_change", (e: Event) => { |
|
if (!isCustomEvent(e)) throw new Error("not a custom event"); |
|
const { id, prop, value } = e.detail; |
|
update_value([{ id, prop, value }]); |
|
if (prop === "input_ready" && value === false) { |
|
inputs_waiting.push(id); |
|
} |
|
if (prop === "input_ready" && value === true) { |
|
inputs_waiting = inputs_waiting.filter((item) => item !== id); |
|
} |
|
}); |
|
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 if (event == "clear_status") { |
|
update_status(id, "complete", data); |
|
} else if (event == "close_stream") { |
|
const deps = $targets[id]?.[data]; |
|
deps?.forEach((dep_id) => { |
|
if (submit_map.has(dep_id)) { |
|
// @ts-ignore |
|
const url = `${app.config.root + app.config.api_prefix}/stream/${submit_map.get(dep_id).event_id()}`; |
|
app.post_data(`${url}/close`, {}); |
|
app.close_ws(url); |
|
} |
|
}); |
|
} else { |
|
const deps = $targets[id]?.[event]; |
|
|
|
deps?.forEach((dep_id) => { |
|
requestAnimationFrame(() => { |
|
wait_then_trigger_api_call(dep_id, id, data); |
|
}); |
|
}); |
|
} |
|
}); |
|
|
|
render_complete = true; |
|
} |
|
|
|
$: set_status($loading_status); |
|
|
|
function update_status( |
|
id: number, |
|
status: "error" | "complete" | "pending", |
|
data: LoadingStatus |
|
): void { |
|
data.status = status; |
|
update_value([ |
|
{ |
|
id, |
|
prop: "loading_status", |
|
value: data |
|
} |
|
]); |
|
} |
|
|
|
function set_status(statuses: LoadingStatusCollection): void { |
|
let updates: { |
|
id: number; |
|
prop: string; |
|
value: LoadingStatus; |
|
}[] = []; |
|
Object.entries(statuses).forEach(([id, loading_status]) => { |
|
let dependency = dependencies.find( |
|
(dep) => dep.id == loading_status.fn_index |
|
); |
|
if (dependency === undefined) { |
|
return; |
|
} |
|
loading_status.scroll_to_output = dependency.scroll_to_output; |
|
loading_status.show_progress = dependency.show_progress; |
|
updates.push({ |
|
id: parseInt(id), |
|
prop: "loading_status", |
|
value: loading_status |
|
}); |
|
}); |
|
|
|
const inputs_to_update = loading_status.get_inputs_to_update(); |
|
const additional_updates = Array.from(inputs_to_update).map( |
|
([id, pending_status]) => { |
|
return { |
|
id, |
|
prop: "pending", |
|
value: pending_status === "pending" |
|
}; |
|
} |
|
); |
|
|
|
update_value([...updates, ...additional_updates]); |
|
} |
|
|
|
function isCustomEvent(event: Event): event is CustomEvent { |
|
return "detail" in event; |
|
} |
|
|
|
onMount(() => { |
|
document.addEventListener("visibilitychange", function () { |
|
if (document.visibilityState === "hidden") { |
|
user_left_page = true; |
|
} |
|
}); |
|
|
|
is_mobile_device = |
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( |
|
navigator.userAgent |
|
); |
|
}); |
|
</script> |
|
|
|
<svelte:head> |
|
{#if control_page_title} |
|
<title>{title}</title> |
|
{/if} |
|
{#if css} |
|
{@html `\<style\>${prefix_css(css, version)}</style>`} |
|
{/if} |
|
</svelte:head> |
|
|
|
<div class="wrap" style:min-height={app_mode ? "100%" : "auto"}> |
|
<div class="contain" style:flex-grow={app_mode ? "1" : "auto"}> |
|
{#if $_layout && app.config} |
|
<MountComponents |
|
rootNode={$_layout} |
|
{root} |
|
{target} |
|
{theme_mode} |
|
on:mount={handle_mount} |
|
{version} |
|
{autoscroll} |
|
{max_file_size} |
|
client={app} |
|
/> |
|
{/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_recorder_visible} |
|
|
|
|
|
|
|
<div |
|
id="api-recorder-container" |
|
on:click={() => { |
|
set_api_docs_visible(true); |
|
api_recorder_visible = false; |
|
}} |
|
> |
|
<ApiRecorder {api_calls} {dependencies} /> |
|
</div> |
|
{/if} |
|
|
|
{#if api_docs_visible && $_layout} |
|
<div class="api-docs"> |
|
|
|
|
|
|
|
<div |
|
class="backdrop" |
|
on:click={() => { |
|
set_api_docs_visible(false); |
|
}} |
|
/> |
|
<div class="api-docs-wrap"> |
|
<ApiDocs |
|
root_node={$_layout} |
|
on:close={(event) => { |
|
set_api_docs_visible(false); |
|
api_calls = []; |
|
api_recorder_visible = event.detail.api_recorder_visible; |
|
}} |
|
{dependencies} |
|
{root} |
|
{app} |
|
{space_id} |
|
{api_calls} |
|
{username} |
|
/> |
|
</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); |
|
} |
|
|
|
.contain { |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
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-top); |
|
background: rgba(0, 0, 0, 0.5); |
|
width: var(--size-screen); |
|
height: var(--size-screen-h); |
|
} |
|
|
|
.backdrop { |
|
flex: 1 1 0%; |
|
-webkit-backdrop-filter: blur(4px); |
|
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; |
|
} |
|
} |
|
|
|
#api-recorder-container { |
|
position: fixed; |
|
left: 10px; |
|
bottom: 10px; |
|
z-index: 1000; |
|
} |
|
</style> |
|
|