gradio

Форк
0
/
Blocks.svelte 
894 строки · 21.9 Кб
1
<script lang="ts">
2
	import { load_component } from "virtual:component-loader";
3

4
	import { tick } from "svelte";
5
	import { _ } from "svelte-i18n";
6
	import type { client } from "@gradio/client";
7

8
	import { create_loading_status_store } from "./stores";
9
	import type { LoadingStatusCollection } from "./stores";
10

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";
19

20
	import logo from "./images/logo.svg";
21
	import api_logo from "./api_docs/img/api-logo.svg";
22

23
	setupi18n();
24

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;
43

44
	let loading_status = create_loading_status_store();
45

46
	let rootNode: ComponentMeta = {
47
		id: layout.id,
48
		type: "column",
49
		props: { interactive: false, scale: fill_height ? 1 : null },
50
		has_modes: false,
51
		instance: null as unknown as ComponentMeta["instance"],
52
		component: null as unknown as ComponentMeta["component"],
53
		component_class_id: ""
54
	};
55

56
	const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
57
	dependencies.forEach((d) => {
58
		if (d.js) {
59
			const wrap = d.backend_fn
60
				? d.inputs.length === 1
61
				: d.outputs.length === 1;
62
			try {
63
				d.frontend_fn = new AsyncFunction(
64
					"__fn_args",
65
					`let result = await (${d.js})(...__fn_args);
66
					return (${wrap} && !Array.isArray(result)) ? [result] : result;`
67
				);
68
			} catch (e) {
69
				console.error("Could not parse custom js method.");
70
				console.error(e);
71
			}
72
		}
73
	});
74

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);
80
		if (visible) {
81
			params.set("view", "api");
82
		} else {
83
			params.delete("view");
84
		}
85
		history.replaceState(null, "", "?" + params.toString());
86
	}
87

88
	function is_dep(
89
		id: number,
90
		type: "inputs" | "outputs",
91
		deps: Dependency[]
92
	): boolean {
93
		for (const dep of deps) {
94
			for (const dep_item of dep[type]) {
95
				if (dep_item === id) return true;
96
			}
97
		}
98
		return false;
99
	}
100

101
	let dynamic_ids: Set<number> = new Set();
102

103
	function has_no_default_value(value: any): boolean {
104
		return (
105
			(Array.isArray(value) && value.length === 0) ||
106
			value === "" ||
107
			value === 0 ||
108
			!value
109
		);
110
	}
111

112
	let instance_map: { [id: number]: ComponentMeta };
113

114
	type LoadedComponent = {
115
		default: ComponentMeta["component"];
116
	};
117

118
	let component_set = new Set<
119
		Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
120
	>();
121

122
	let _component_map = new Map<
123
		`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
124
		Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
125
	>();
126

127
	async function walk_layout(
128
		node: LayoutNode,
129
		type_map: Map<number, ComponentMeta["props"]["interactive"]>,
130
		instance_map: { [id: number]: ComponentMeta },
131
		component_map: Map<
132
			`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
133
			Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
134
		>
135
	): Promise<void> {
136
		ready = false;
137
		let instance = instance_map[node.id];
138

139
		const _component = (await component_map.get(
140
			`${instance.type}_${type_map.get(node.id) || "false"}`
141
		))!.component;
142
		instance.component = _component.default;
143

144
		if (node.children) {
145
			instance.children = node.children.map((v) => instance_map[v.id]);
146
			await Promise.all(
147
				node.children.map((v) =>
148
					walk_layout(v, type_map, instance_map, component_map)
149
				)
150
			);
151
		}
152
	}
153

154
	export let ready = false;
155
	export let render_complete = false;
156

157
	$: components, layout, prepare_components();
158

159
	let target_map: Record<number, Record<string, number[]>> = {};
160

161
	function prepare_components(): void {
162
		target_map = dependencies.reduce(
163
			(acc, dep, i) => {
164
				dep.targets.forEach(([id, trigger]) => {
165
					if (!acc[id]) {
166
						acc[id] = {};
167
					}
168
					if (acc[id]?.[trigger]) {
169
						acc[id][trigger].push(i);
170
					} else {
171
						acc[id][trigger] = [i];
172
					}
173
				});
174

175
				return acc;
176
			},
177
			{} as Record<number, Record<string, number[]>>
178
		);
179
		loading_status = create_loading_status_store();
180

181
		dependencies.forEach((v, i) => {
182
			loading_status.register(i, v.inputs, v.outputs);
183
		});
184

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);
189
			if (
190
				is_input ||
191
				(!is_dep(id, "outputs", dependencies) &&
192
					has_no_default_value(props?.value))
193
			) {
194
				_dynamic_ids.add(id);
195
			}
196
		}
197

198
		dynamic_ids = _dynamic_ids;
199

200
		const _rootNode: typeof rootNode = {
201
			id: layout.id,
202
			type: "column",
203
			props: { interactive: false, scale: fill_height ? 1 : null },
204
			has_modes: false,
205
			instance: null as unknown as ComponentMeta["instance"],
206
			component: null as unknown as ComponentMeta["component"],
207
			component_class_id: ""
208
		};
209
		components.push(_rootNode);
210
		const _component_set = new Set<
211
			Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
212
		>();
213
		const __component_map = new Map<
214
			`${ComponentMeta["type"]}_${ComponentMeta["props"]["interactive"]}`,
215
			Promise<{ name: ComponentMeta["type"]; component: LoadedComponent }>
216
		>();
217
		const __type_for_id = new Map<
218
			number,
219
			ComponentMeta["props"]["interactive"]
220
		>();
221
		const _instance_map = components.reduce(
222
			(acc, next) => {
223
				acc[next.id] = next;
224
				return acc;
225
			},
226
			{} as { [id: number]: ComponentMeta }
227
		);
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;
235
			} else {
236
				(c.props as any).interactive = false;
237
			}
238

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) {
244
							args = args[0];
245
						}
246
						const result = await app.component_server(c.id, fn, args);
247
						return result;
248
					};
249
				});
250
				(c.props as any).server = server;
251
			}
252

253
			if (target_map[c.id]) {
254
				c.props.attached_events = Object.keys(target_map[c.id]);
255
			}
256
			__type_for_id.set(c.id, c.props.interactive);
257

258
			if (c.type === "dataset") {
259
				const example_component_map = new Map();
260

261
				(c.props.components as string[]).forEach((name: string) => {
262
					if (example_component_map.has(name)) {
263
						return;
264
					}
265
					let _c;
266

267
					const matching_component = components.find((c) => c.type === name);
268
					if (matching_component) {
269
						_c = load_component({
270
							api_url: root,
271
							name,
272
							id: matching_component.component_class_id,
273
							variant: "example"
274
						});
275
						example_component_map.set(name, _c);
276
					}
277
				});
278

279
				c.props.component_map = example_component_map;
280
			}
281

282
			// maybe load custom
283

284
			const _c = load_component({
285
				api_url: root,
286
				name: c.type,
287
				id: c.component_class_id,
288
				variant: "component"
289
			});
290
			_component_set.add(_c);
291
			__component_map.set(`${c.type}_${c.props.interactive}`, _c);
292
		});
293

294
		Promise.all(Array.from(_component_set)).then(() => {
295
			walk_layout(layout, __type_for_id, _instance_map, __component_map)
296
				.then(async () => {
297
					ready = true;
298
					component_set = _component_set;
299
					_component_map = __component_map;
300
					instance_map = _instance_map;
301
					rootNode = _rootNode;
302
				})
303
				.catch((e) => {
304
					console.error(e);
305
				});
306
		});
307
	}
308

309
	function throttle<T extends (...args: any[]) => any>(
310
		func: T,
311
		limit: number
312
	): (...funcArgs: Parameters<T>) => void {
313
		let lastFunc: ReturnType<typeof setTimeout>;
314
		let lastRan: number;
315
		let lastThis: any;
316
		let lastArgs: IArguments | null;
317

318
		return function (this: any, ...args: Parameters<T>) {
319
			if (!lastRan) {
320
				func.apply(this, args);
321
				lastRan = Date.now();
322
			} else {
323
				clearTimeout(lastFunc);
324
				lastThis = this;
325
				lastArgs = arguments;
326

327
				lastFunc = setTimeout(
328
					() => {
329
						if (Date.now() - lastRan >= limit) {
330
							if (lastArgs) {
331
								func.apply(lastThis, Array.prototype.slice.call(lastArgs));
332
							}
333
							lastRan = Date.now();
334
						}
335
					},
336
					Math.max(limit - (Date.now() - lastRan), 0)
337
				);
338
			}
339
		};
340
	}
341

342
	const refresh = throttle(() => {
343
		rootNode = rootNode;
344
	}, 50);
345

346
	async function handle_update(data: any, fn_index: number): Promise<void> {
347
		const outputs = dependencies[fn_index].outputs;
348

349
		data?.forEach((value: any, i: number) => {
350
			const output = instance_map[outputs[i]];
351
			output.props.value_is_output = true;
352
		});
353

354
		refresh();
355
		await tick();
356
		data?.forEach((value: any, i: number) => {
357
			const output = instance_map[outputs[i]];
358
			if (
359
				typeof value === "object" &&
360
				value !== null &&
361
				value.__type__ === "update"
362
			) {
363
				for (const [update_key, update_value] of Object.entries(value)) {
364
					if (update_key === "__type__") {
365
						continue;
366
					} else {
367
						output.props[update_key] = update_value;
368
					}
369
				}
370
			} else {
371
				output.props.value = value;
372
			}
373
		});
374
		refresh();
375
	}
376

377
	let submit_map: Map<number, ReturnType<typeof app.submit>> = new Map();
378

379
	function set_prop<T extends ComponentMeta>(
380
		obj: T,
381
		prop: string,
382
		val: any
383
	): void {
384
		if (!obj?.props) {
385
			// @ts-ignore
386
			obj.props = {};
387
		}
388
		obj.props[prop] = val;
389
		refresh();
390
	}
391
	let handled_dependencies: number[][] = [];
392

393
	let messages: (ToastMessage & { fn_index: number })[] = [];
394
	function new_message(
395
		message: string,
396
		fn_index: number,
397
		type: ToastMessage["type"]
398
	): ToastMessage & { fn_index: number } {
399
		return {
400
			message,
401
			fn_index,
402
			type,
403
			id: ++_error_id
404
		};
405
	}
406

407
	let _error_id = -1;
408

409
	let user_left_page = false;
410
	document.addEventListener("visibilitychange", function () {
411
		if (document.visibilityState === "hidden") {
412
			user_left_page = true;
413
		}
414
	});
415

416
	const MESSAGE_QUOTE_RE = /^'([^]+)'$/;
417

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(
425
			navigator.userAgent
426
		);
427
	let showed_duplicate_message = false;
428
	let showed_mobile_warning = false;
429

430
	function get_data(comp: ComponentMeta): any | Promise<any> {
431
		if (comp.instance.get_value) {
432
			return comp.instance.get_value() as Promise<any>;
433
		}
434
		return comp.props.value;
435
	}
436

437
	async function trigger_api_call(
438
		dep_index: number,
439
		trigger_id: number | null = null,
440
		event_data: unknown = null
441
	): Promise<void> {
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);
445
		if (dep.cancels) {
446
			await Promise.all(
447
				dep.cancels.map(async (fn_index) => {
448
					const submission = submit_map.get(fn_index);
449
					submission?.cancel();
450
					return submission;
451
				})
452
			);
453
		}
454
		if (current_status === "pending" || current_status === "generating") {
455
			dep.pending_request = true;
456
		}
457

458
		let payload: Payload = {
459
			fn_index: dep_index,
460
			data: await Promise.all(
461
				dep.inputs.map((id) => get_data(instance_map[id]))
462
			),
463
			event_data: dep.collects_event_data ? event_data : null,
464
			trigger_id: trigger_id
465
		};
466

467
		if (dep.frontend_fn) {
468
			dep
469
				.frontend_fn(
470
					payload.data.concat(
471
						await Promise.all(
472
							dep.inputs.map((id) => get_data(instance_map[id]))
473
						)
474
					)
475
				)
476
				.then((v: unknown[]) => {
477
					if (dep.backend_fn) {
478
						payload.data = v;
479
						make_prediction(payload);
480
					} else {
481
						handle_update(v, dep_index);
482
					}
483
				});
484
		} else {
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);
493
					} else {
494
						dep.final_event = payload;
495
					}
496
				}
497
			}
498
		}
499

500
		function make_prediction(payload: Payload): void {
501
			const submission = app
502
				.submit(
503
					payload.fn_index,
504
					payload.data as unknown[],
505
					payload.event_data,
506
					payload.trigger_id
507
				)
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);
512
					}
513
					dep.pending_request = false;
514
					handle_update(data, fn_index);
515
				})
516
				.on("status", ({ fn_index, ...status }) => {
517
					tick().then(() => {
518
						//@ts-ignore
519
						loading_status.update({
520
							...status,
521
							status: status.stage,
522
							progress: status.progress_data,
523
							fn_index
524
						});
525
						if (
526
							!showed_duplicate_message &&
527
							space_id !== null &&
528
							status.position !== undefined &&
529
							status.position >= 2 &&
530
							status.eta !== undefined &&
531
							status.eta > SHOW_DUPLICATE_MESSAGE_ON_ETA
532
						) {
533
							showed_duplicate_message = true;
534
							messages = [
535
								new_message(DUPLICATE_MESSAGE, fn_index, "warning"),
536
								...messages
537
							];
538
						}
539
						if (
540
							!showed_mobile_warning &&
541
							is_mobile_device &&
542
							status.eta !== undefined &&
543
							status.eta > SHOW_MOBILE_QUEUE_WARNING_ON_ETA
544
						) {
545
							showed_mobile_warning = true;
546
							messages = [
547
								new_message(MOBILE_QUEUE_WARNING, fn_index, "warning"),
548
								...messages
549
							];
550
						}
551

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);
556
								}
557
							});
558

559
							submission.destroy();
560
						}
561
						if (status.broken && is_mobile_device && user_left_page) {
562
							window.setTimeout(() => {
563
								messages = [
564
									new_message(MOBILE_RECONNECT_MESSAGE, fn_index, "error"),
565
									...messages
566
								];
567
							}, 0);
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(
573
									MESSAGE_QUOTE_RE,
574
									(_, b) => b
575
								);
576
								messages = [
577
									new_message(_message, fn_index, "error"),
578
									...messages
579
								];
580
							}
581
							dependencies.map(async (dep, i) => {
582
								if (
583
									dep.trigger_after === fn_index &&
584
									!dep.trigger_only_on_success
585
								) {
586
									trigger_api_call(i, payload.trigger_id);
587
								}
588
							});
589

590
							submission.destroy();
591
						}
592
					});
593
				})
594
				.on("log", ({ log, fn_index, level }) => {
595
					messages = [new_message(log, fn_index, level), ...messages];
596
				});
597

598
			submit_map.set(dep_index, submission);
599
		}
600
	}
601

602
	function trigger_share(title: string | undefined, description: string): void {
603
		if (space_id === null) {
604
			return;
605
		}
606
		const discussion_url = new URL(
607
			`https://huggingface.co/spaces/${space_id}/discussions/new`
608
		);
609
		if (title !== undefined && title.length > 0) {
610
			discussion_url.searchParams.set("title", title);
611
		}
612
		discussion_url.searchParams.set("description", description);
613
		window.open(discussion_url.toString(), "_blank");
614
	}
615

616
	function handle_error_close(e: Event & { detail: number }): void {
617
		const _id = e.detail;
618
		messages = messages.filter((m) => m.id !== _id);
619
	}
620

621
	const is_external_url = (link: string | null): boolean =>
622
		!!(link && new URL(link, location.href).origin !== location.origin);
623

624
	async function handle_mount(): Promise<void> {
625
		if (js) {
626
			let blocks_frontend_fn = new AsyncFunction(
627
				`let result = await (${js})();
628
					return (!Array.isArray(result)) ? [result] : result;`
629
			);
630
			blocks_frontend_fn();
631
		}
632

633
		await tick();
634

635
		var a = target.getElementsByTagName("a");
636

637
		for (var i = 0; i < a.length; i++) {
638
			const _target = a[i].getAttribute("target");
639
			const _link = a[i].getAttribute("href");
640

641
			// only target anchor tags with external links
642
			if (is_external_url(_link) && _target !== "_blank")
643
				a[i].setAttribute("target", "_blank");
644
		}
645

646
		// handle load triggers
647
		dependencies.forEach((dep, i) => {
648
			if (dep.targets[0][1] === "load") {
649
				trigger_api_call(i);
650
			}
651
		});
652

653
		if (render_complete) return;
654
		target.addEventListener("gradio", (e: Event) => {
655
			if (!isCustomEvent(e)) throw new Error("not a custom event");
656

657
			const { id, event, data } = e.detail;
658

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];
664
			} else {
665
				const deps = target_map[id]?.[event];
666
				deps?.forEach((dep_id) => {
667
					trigger_api_call(dep_id, id, data);
668
				});
669
			}
670
		});
671

672
		render_complete = true;
673
	}
674

675
	function handle_destroy(id: number): void {
676
		handled_dependencies = handled_dependencies.map((dep) => {
677
			return dep.filter((_id) => _id !== id);
678
		});
679
	}
680

681
	$: set_status($loading_status);
682

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;
689

690
			set_prop(instance_map[id], "loading_status", loading_status);
691
		}
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");
695
		}
696
	}
697

698
	function isCustomEvent(event: Event): event is CustomEvent {
699
		return "detail" in event;
700
	}
701
</script>
702

703
<svelte:head>
704
	{#if control_page_title}
705
		<title>{title}</title>
706
	{/if}
707
	{#if analytics_enabled}
708
		<script
709
			async
710
			defer
711
			src="https://www.googletagmanager.com/gtag/js?id=UA-156449732-1"
712
		></script>
713
		<script>
714
			window.dataLayer = window.dataLayer || [];
715
			function gtag() {
716
				dataLayer.push(arguments);
717
			}
718
			gtag("js", new Date());
719
			gtag("config", "UA-156449732-1", {
720
				cookie_flags: "samesite=none;secure"
721
			});
722
		</script>
723
	{/if}
724
</svelte:head>
725

726
<div class="wrap" style:min-height={app_mode ? "100%" : "auto"}>
727
	<div class="contain" style:flex-grow={app_mode ? "1" : "auto"}>
728
		{#if ready}
729
			<MountComponents
730
				{rootNode}
731
				{dynamic_ids}
732
				{instance_map}
733
				{root}
734
				{target}
735
				{theme_mode}
736
				on:mount={handle_mount}
737
				on:destroy={({ detail }) => handle_destroy(detail)}
738
				{version}
739
				{autoscroll}
740
			/>
741
		{/if}
742
	</div>
743

744
	{#if show_footer}
745
		<footer>
746
			{#if show_api}
747
				<button
748
					on:click={() => {
749
						set_api_docs_visible(!api_docs_visible);
750
					}}
751
					class="show-api"
752
				>
753
					{$_("errors.use_via_api")}
754
					<img src={api_logo} alt={$_("common.logo")} />
755
				</button>
756
				<div>·</div>
757
			{/if}
758
			<a
759
				href="https://gradio.app"
760
				class="built-with"
761
				target="_blank"
762
				rel="noreferrer"
763
			>
764
				{$_("common.built_with_gradio")}
765
				<img src={logo} alt={$_("common.logo")} />
766
			</a>
767
		</footer>
768
	{/if}
769
</div>
770

771
{#if api_docs_visible && ready}
772
	<div class="api-docs">
773
		<!-- TODO: fix -->
774
		<!-- svelte-ignore a11y-click-events-have-key-events-->
775
		<!-- svelte-ignore a11y-no-static-element-interactions-->
776
		<div
777
			class="backdrop"
778
			on:click={() => {
779
				set_api_docs_visible(false);
780
			}}
781
		/>
782
		<div class="api-docs-wrap">
783
			<ApiDocs
784
				on:close={() => {
785
					set_api_docs_visible(false);
786
				}}
787
				{instance_map}
788
				{dependencies}
789
				{root}
790
				{app}
791
				{space_id}
792
			/>
793
		</div>
794
	</div>
795
{/if}
796

797
{#if messages}
798
	<Toast {messages} on:close={handle_error_close} />
799
{/if}
800

801
<style>
802
	.wrap {
803
		display: flex;
804
		flex-grow: 1;
805
		flex-direction: column;
806
		width: var(--size-full);
807
		font-weight: var(--body-text-weight);
808
		font-size: var(--body-text-size);
809
	}
810

811
	.contain {
812
		display: flex;
813
		flex-direction: column;
814
	}
815

816
	footer {
817
		display: flex;
818
		justify-content: center;
819
		margin-top: var(--size-4);
820
		color: var(--body-text-color-subdued);
821
	}
822

823
	footer > * + * {
824
		margin-left: var(--size-2);
825
	}
826

827
	.show-api {
828
		display: flex;
829
		align-items: center;
830
	}
831
	.show-api:hover {
832
		color: var(--body-text-color);
833
	}
834

835
	.show-api img {
836
		margin-right: var(--size-1);
837
		margin-left: var(--size-2);
838
		width: var(--size-3);
839
	}
840

841
	.built-with {
842
		display: flex;
843
		align-items: center;
844
	}
845

846
	.built-with:hover {
847
		color: var(--body-text-color);
848
	}
849

850
	.built-with img {
851
		margin-right: var(--size-1);
852
		margin-left: var(--size-1);
853
		margin-bottom: 1px;
854
		width: var(--size-4);
855
	}
856

857
	.api-docs {
858
		display: flex;
859
		position: fixed;
860
		top: 0;
861
		right: 0;
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);
866
	}
867

868
	.backdrop {
869
		flex: 1 1 0%;
870
		-webkit-backdrop-filter: blur(4px);
871
		backdrop-filter: blur(4px);
872
	}
873

874
	.api-docs-wrap {
875
		box-shadow: var(--shadow-drop-lg);
876
		background: var(--background-fill-primary);
877
		overflow-x: hidden;
878
		overflow-y: auto;
879
	}
880

881
	@media (--screen-md) {
882
		.api-docs-wrap {
883
			border-top-left-radius: var(--radius-lg);
884
			border-bottom-left-radius: var(--radius-lg);
885
			width: 950px;
886
		}
887
	}
888

889
	@media (--screen-xxl) {
890
		.api-docs-wrap {
891
			width: 1150px;
892
		}
893
	}
894
</style>
895

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.