Refactoring <gradio-lite /> component making the code simpler and fixing a Playground mode bug (#8693) (#8818)

* Create <LiteIndex /> component that contains the initialization logic and manages <Index /> and <Playground />

* add changeset

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Yuichiro Tachibana (Tsuchiya) 2024-07-18 04:17:11 +09:00 committed by GitHub
parent 7b049e0357
commit 2de9a97ae9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 228 additions and 256 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/app": patch
"gradio": patch
---
fix:Refactoring <gradio-lite /> component making the code simpler and fixing a Playground mode bug

View File

@ -0,0 +1,201 @@
<script lang="ts">
import "@gradio/theme/src/reset.css";
import "@gradio/theme/src/global.css";
import "@gradio/theme/src/pollen.css";
import "@gradio/theme/src/typography.css";
import { onDestroy, SvelteComponent } from "svelte";
import Index from "../Index.svelte";
import Playground from "./Playground.svelte";
import ErrorDisplay from "./ErrorDisplay.svelte";
import type { ThemeMode } from "../types";
import { WorkerProxy, type WorkerProxyOptions } from "@gradio/wasm";
import { Client } from "@gradio/client";
import { wasm_proxied_fetch } from "./fetch";
import { wasm_proxied_stream_factory } from "./sse";
import { wasm_proxied_mount_css, mount_prebuilt_css } from "./css";
import type { mount_css } from "../css";
// These imports are aliased at built time with Vite. See the `resolve.alias` config in `vite.config.ts`.
import gradioWheel from "gradio.whl";
import gradioClientWheel from "gradio_client.whl";
export let info: boolean;
export let container: boolean;
export let is_embed: boolean;
export let initial_height: string;
export let eager: boolean;
export let version: string;
export let theme_mode: ThemeMode | null;
export let autoscroll: boolean;
export let control_page_title: boolean;
export let app_mode: boolean;
// For Wasm mode
export let files: WorkerProxyOptions["files"] | undefined;
export let requirements: WorkerProxyOptions["requirements"] | undefined;
export let code: string | undefined;
export let entrypoint: string | undefined;
export let sharedWorkerMode: boolean | undefined;
// For playground
export let playground = false;
export let layout: string | null;
const worker_proxy = new WorkerProxy({
gradioWheelUrl: new URL(gradioWheel, import.meta.url).href,
gradioClientWheelUrl: new URL(gradioClientWheel, import.meta.url).href,
files: files ?? {},
requirements: requirements ?? [],
sharedWorkerMode: sharedWorkerMode ?? false
});
onDestroy(() => {
worker_proxy.terminate();
});
let error: Error | null = null;
const wrapFunctionWithAppLogic = <TArgs extends any[], TRet extends any>(
func: (...args: TArgs) => Promise<TRet>
): ((...args: TArgs) => Promise<TRet>) => {
return async (...args: TArgs) => {
try {
error = null;
const retval = await func(...args);
refresh_index_component();
return retval;
} catch (err) {
error = err as Error;
throw err;
}
};
};
worker_proxy.runPythonCode = wrapFunctionWithAppLogic(
worker_proxy.runPythonCode.bind(worker_proxy)
);
worker_proxy.runPythonFile = wrapFunctionWithAppLogic(
worker_proxy.runPythonFile.bind(worker_proxy)
);
worker_proxy.writeFile = wrapFunctionWithAppLogic(
worker_proxy.writeFile.bind(worker_proxy)
);
worker_proxy.renameFile = wrapFunctionWithAppLogic(
worker_proxy.renameFile.bind(worker_proxy)
);
worker_proxy.unlink = wrapFunctionWithAppLogic(
worker_proxy.unlink.bind(worker_proxy)
);
worker_proxy.install = wrapFunctionWithAppLogic(
worker_proxy.install.bind(worker_proxy)
);
worker_proxy.addEventListener("initialization-error", (event) => {
error = (event as CustomEvent).detail;
});
// Internally, the execution of `runPythonCode()` or `runPythonFile()` is queued
// and its promise will be resolved after the Pyodide is loaded and the worker initialization is done
// (see the await in the `onmessage` callback in the webworker code)
// So we don't await this promise because we want to mount the `Index` immediately and start the app initialization asynchronously.
if (code != null) {
worker_proxy.runPythonCode(code);
} else if (entrypoint != null) {
worker_proxy.runPythonFile(entrypoint);
} else {
throw new Error("Either code or entrypoint must be provided.");
}
mount_prebuilt_css(document.head);
class LiteClient extends Client {
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return wasm_proxied_fetch(worker_proxy, input, init);
}
stream(url: URL): EventSource {
return wasm_proxied_stream_factory(worker_proxy, url);
}
}
const overridden_mount_css: typeof mount_css = async (url, target) => {
return wasm_proxied_mount_css(worker_proxy, url, target);
};
let index_component_key = 0;
function refresh_index_component(): void {
index_component_key += 1;
}
let playground_component: SvelteComponent | null = null;
$: playground_component?.$on("code", (event) => {
const { code } = event.detail;
worker_proxy.runPythonCode(code);
});
export const run_code = worker_proxy.runPythonCode;
export const run_file = worker_proxy.runPythonFile;
export const write = worker_proxy.writeFile;
export const rename = worker_proxy.renameFile;
export const unlink = worker_proxy.unlink;
export const install = worker_proxy.install;
</script>
{#if playground}
<Playground
bind:this={playground_component}
{worker_proxy}
{layout}
{code}
{is_embed}
>
{#key index_component_key}
{#if error}
<ErrorDisplay {error} is_embed />
{:else}
<Index
space={null}
src={null}
host={null}
{info}
{container}
{is_embed}
{initial_height}
{eager}
{version}
{theme_mode}
{autoscroll}
{control_page_title}
{app_mode}
{worker_proxy}
Client={LiteClient}
mount_css={overridden_mount_css}
/>
{/if}
{/key}
</Playground>
{:else}
{#key index_component_key}
{#if error}
<ErrorDisplay {error} {is_embed} />
{:else}
<Index
space={null}
src={null}
host={null}
{info}
{container}
{is_embed}
{initial_height}
{eager}
{version}
{theme_mode}
{autoscroll}
{control_page_title}
{app_mode}
{worker_proxy}
Client={LiteClient}
mount_css={overridden_mount_css}
/>
{/if}
{/key}
{/if}

View File

@ -1,35 +1,16 @@
<script lang="ts">
import Index from "../Index.svelte";
import type { ThemeMode } from "../types";
import { mount_css as default_mount_css } from "../css";
import type { Client as ClientType } from "@gradio/client";
import type { WorkerProxy } from "@gradio/wasm";
import { createEventDispatcher, onMount } from "svelte";
import { Block } from "@gradio/atoms";
import { BaseCode as Code } from "@gradio/code";
import ErrorDisplay from "./ErrorDisplay.svelte";
import lightning from "../images/lightning.svg";
export let autoscroll: boolean;
export let version: string;
export let initial_height: string;
export let app_mode: boolean;
export let is_embed: boolean;
export let theme_mode: ThemeMode | null = "system";
export let control_page_title: boolean;
export let container: boolean;
export let info: boolean;
export let eager: boolean;
export let mount_css: typeof default_mount_css = default_mount_css;
export let Client: typeof ClientType;
export let worker_proxy: WorkerProxy | undefined = undefined;
export let space: string | null;
export let host: string | null;
export let src: string | null;
export let code: string | undefined;
export let error_display: { is_embed: boolean; error: Error } | null;
export let layout: string | null = null;
const dispatch = createEventDispatcher();
@ -172,31 +153,7 @@
</div>
{#if loaded}
<div class="preview">
{#if !error_display}
<Index
{autoscroll}
{version}
{initial_height}
{app_mode}
{is_embed}
{theme_mode}
{control_page_title}
{container}
{info}
{eager}
{mount_css}
{Client}
bind:worker_proxy
{space}
{host}
{src}
/>
{:else}
<ErrorDisplay
is_embed={error_display.is_embed}
error={error_display.error}
/>
{/if}
<slot></slot>
</div>
{/if}
</div>

View File

@ -1,26 +1,11 @@
import "@gradio/theme/src/reset.css";
import "@gradio/theme/src/global.css";
import "@gradio/theme/src/pollen.css";
import "@gradio/theme/src/typography.css";
import type { SvelteComponent } from "svelte";
import { WorkerProxy, type WorkerProxyOptions } from "@gradio/wasm";
import { Client } from "@gradio/client";
import { wasm_proxied_fetch } from "./fetch";
import { wasm_proxied_stream_factory } from "./sse";
import { wasm_proxied_mount_css, mount_prebuilt_css } from "./css";
import type { mount_css } from "../css";
import Index from "../Index.svelte";
import Playground from "./Playground.svelte";
import ErrorDisplay from "./ErrorDisplay.svelte";
import { type WorkerProxyOptions } from "@gradio/wasm";
import type { ThemeMode } from "../types";
import { bootstrap_custom_element } from "./custom-element";
// These imports are aliased at built time with Vite. See the `resolve.alias` config in `vite.config.ts`.
import gradioWheel from "gradio.whl";
import gradioClientWheel from "gradio_client.whl";
declare let GRADIO_VERSION: string;
import LiteIndex from "./LiteIndex.svelte";
// NOTE: The following line has been copied from `main.ts`.
// In `main.ts`, which is the normal Gradio app entry point,
// the string literal "__ENTRY_CSS__" will be replaced with the actual CSS file path
@ -78,217 +63,40 @@ export function create(options: Options): GradioAppController {
observer.observe(options.target, { childList: true });
const worker_proxy = new WorkerProxy({
gradioWheelUrl: new URL(gradioWheel, import.meta.url).href,
gradioClientWheelUrl: new URL(gradioClientWheel, import.meta.url).href,
files: options.files ?? {},
requirements: options.requirements ?? [],
sharedWorkerMode: options.sharedWorkerMode ?? false
});
worker_proxy.addEventListener("initialization-error", (event) => {
showError((event as CustomEvent).detail);
});
// Internally, the execution of `runPythonCode()` or `runPythonFile()` is queued
// and its promise will be resolved after the Pyodide is loaded and the worker initialization is done
// (see the await in the `onmessage` callback in the webworker code)
// So we don't await this promise because we want to mount the `Index` immediately and start the app initialization asynchronously.
if (options.code != null) {
worker_proxy.runPythonCode(options.code).catch(showError);
} else if (options.entrypoint != null) {
worker_proxy.runPythonFile(options.entrypoint).catch(showError);
} else {
throw new Error("Either code or entrypoint must be provided.");
}
mount_prebuilt_css(document.head);
class LiteClient extends Client {
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return wasm_proxied_fetch(worker_proxy, input, init);
}
stream(url: URL): EventSource {
return wasm_proxied_stream_factory(worker_proxy, url);
}
}
const overridden_mount_css: typeof mount_css = async (url, target) => {
return wasm_proxied_mount_css(worker_proxy, url, target);
};
let app: SvelteComponent;
let app_props: any;
let loaded = false;
function showError(error: Error): void {
if (app != null) {
app.$destroy();
}
if (options.playground) {
app = new Playground({
target: options.target,
props: {
...app_props,
code: options.code,
error_display: {
is_embed: !options.isEmbed,
error
},
loaded: true
}
});
app.$on("code", (event) => {
options.code = event.detail.code;
loaded = true;
worker_proxy
.runPythonCode(event.detail.code)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
});
} else {
app = new ErrorDisplay({
target: options.target,
props: {
is_embed: !options.isEmbed,
error
}
});
}
}
function launchNewApp(): Promise<void> {
if (app != null) {
app.$destroy();
}
app_props = {
// embed source
space: null,
src: null,
host: null,
// embed info
const app = new LiteIndex({
target: options.target,
props: {
info: options.info,
container: options.container,
is_embed: options.isEmbed,
initial_height: options.initialHeight ?? "300px", // default: 300px
initial_height: options.initialHeight ?? "300px",
eager: options.eager,
// gradio meta info
version: GRADIO_VERSION,
theme_mode: options.themeMode,
// misc global behaviour
autoscroll: options.autoScroll,
control_page_title: options.controlPageTitle,
// for gradio docs
// TODO: Remove -- i think this is just for autoscroll behavhiour, app vs embeds
app_mode: options.appMode,
// For Wasm mode
worker_proxy,
Client: LiteClient,
mount_css: overridden_mount_css,
files: options.files,
requirements: options.requirements,
code: options.code,
entrypoint: options.entrypoint,
sharedWorkerMode: options.sharedWorkerMode,
// For playground
playground: options.playground,
layout: options.layout
};
if (options.playground) {
app = new Playground({
target: options.target,
props: {
...app_props,
code: options.code,
error_display: null,
loaded: loaded
}
});
app.$on("code", (event) => {
options.code = event.detail.code;
loaded = true;
worker_proxy
.runPythonCode(event.detail.code)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
});
} else {
app = new Index({
target: options.target,
props: app_props
});
}
return new Promise((resolve) => {
app.$on("loaded", () => {
resolve();
});
});
}
launchNewApp();
});
return {
run_code: (code: string) => {
return worker_proxy
.runPythonCode(code)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
},
run_file: (path: string) => {
return worker_proxy
.runPythonFile(path)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
},
write: (path, data, opts) => {
return worker_proxy
.writeFile(path, data, opts)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
},
rename: (old_path, new_path) => {
return worker_proxy
.renameFile(old_path, new_path)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
},
unlink: (path) => {
return worker_proxy
.unlink(path)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
},
install: (requirements) => {
return worker_proxy
.install(requirements)
.then(launchNewApp)
.catch((e) => {
showError(e);
throw e;
});
},
run_code: app.run_code,
run_file: app.run_file,
write: app.write,
rename: app.rename,
unlink: app.unlink,
install: app.install,
unmount() {
app.$destroy();
worker_proxy.terminate();
}
};
}