Add Playground to Lite Custom Element (#7660)

* lite playground changes

* add changeset

* formatting

* index

* fix tests

* fixes

* add changeset

* styling changes

* code parsing

* formatting

* remove tailiwnd

* add shortcut

* formatting

* linting

* formatting

* snake case

* typing

* try fix

* remove import

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
Co-authored-by: Hannah <hannahblair@users.noreply.github.com>
This commit is contained in:
Ali Abdalla 2024-03-19 11:49:21 -07:00 committed by GitHub
parent abb3f3c116
commit f739bef6c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 591 additions and 67 deletions

View File

@ -0,0 +1,8 @@
---
"@gradio/app": minor
"@gradio/lite": minor
"@gradio/wasm": minor
"gradio": minor
---
feat:Add Playground to Lite Custom Element

View File

@ -0,0 +1,2 @@
<!-- Lightning https://iconscout.com/icons/lightning by IconLauk https://iconscout.com/contributors/icon-lauk on IconScout https://iconscout.com" -->
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 32 32" viewBox="0 0 32 32" id="lightning"><path fill="#999b9e" d="M26.9,10.5C26.7,10.2,26.4,10,26,10h-9V2c0-0.4-0.3-0.8-0.7-1c-0.4-0.1-0.9,0-1.1,0.4l-10,14c-0.2,0.3-0.3,0.7-0.1,1 C5.3,16.8,5.6,17,6,17h8.8L13,29.9c-0.1,0.5,0.2,0.9,0.6,1.1c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.7-0.2,0.8-0.5l12-19 C27,11.2,27,10.8,26.9,10.5z"/></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1,2 @@
<!-- Play https://iconscout.com/icons/play by Alexandru Stoica https://iconscout.com/contributors/alexandru-stoica on IconScout https://iconscout.com" -->
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="7" viewBox="0 0 6 7" id="play"><g fill="#999b9e" transform="translate(-347 -3766)"><g transform="translate(56 160)"><path d="M296.495 3608.573l-3.994-2.43c-.669-.408-1.501.107-1.501.926v4.862c0 .82.832 1.333 1.5.927l3.995-2.43c.673-.41.673-1.445 0-1.855"/></g></g></svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@ -0,0 +1,354 @@
<script lang="ts">
import Index from "../Index.svelte";
import type { ThemeMode } from "../types";
import { mount_css as default_mount_css } from "../css";
import type { api_factory } from "@gradio/client";
import type { WorkerProxy } from "@gradio/wasm";
import { SvelteComponent, createEventDispatcher } from "svelte";
import Code from "@gradio/code";
import ErrorDisplay from "./ErrorDisplay.svelte";
import lightning from "../images/lightning.svg";
import play from "../images/play.svg";
import type { LoadingStatus } from "js/statustracker";
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: ReturnType<typeof api_factory>["client"];
export let upload_files: ReturnType<typeof api_factory>["upload_files"];
export let worker_proxy: WorkerProxy | undefined = undefined;
export let fetch_implementation: typeof fetch = fetch;
export let EventSource_factory: (url: URL) => EventSource = (url) =>
new EventSource(url);
export let space: string | null;
export let host: string | null;
export let src: string | null;
export let code: string | undefined;
export let error_display: SvelteComponent | null;
const dispatch = createEventDispatcher();
let dummy_elem: any = { classList: { contains: () => false } };
let dummy_gradio: any = { dispatch: (_: any) => {} };
let dummy_loading_status: LoadingStatus = {
eta: 0,
queue_position: 0,
queue_size: 0,
status: "complete",
show_progress: "hidden",
scroll_to_output: false,
visible: false,
fn_index: 0
};
let loading_text = "";
export let loaded = false;
worker_proxy?.addEventListener("progress-update", (event) => {
loading_text = (event as CustomEvent).detail + "...";
});
worker_proxy?.addEventListener("initialization-completed", (_) => {
loaded = true;
});
function shortcut_run(e: KeyboardEvent): void {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
dispatch("code", { code });
}
}
$: loading_text;
$: loaded;
</script>
<svelte:window on:keydown={shortcut_run} />
<div class="parent-container">
<div class="child-container">
<div class:code-editor-border={loaded} class="code-editor">
<div class="loading-panel">
<div class="code-header">app.py</div>
{#if !loaded}
<div style="display: flex;"></div>
<div class="loading-section">
<div class="loading-dot"></div>
{loading_text}
</div>
{:else}
<div style="display: flex;"></div>
<div class="loading-section">
<img src={lightning} alt="lightning icon" class="lightning-logo" />
Interactive
</div>
{/if}
</div>
<div style="flex-grow: 1;">
{#if loaded}
<Code
bind:value={code}
label=""
language="python"
target={dummy_elem}
gradio={dummy_gradio}
lines={10}
interactive={true}
loading_status={dummy_loading_status}
/>
{:else}
<Code
bind:value={code}
label=""
language="python"
target={dummy_elem}
gradio={dummy_gradio}
lines={10}
interactive={false}
loading_status={dummy_loading_status}
/>
{/if}
</div>
</div>
{#if loaded}
<div class="preview">
<div class="buttons">
<div class="run">
<button
class="button"
on:click={() => {
dispatch("code", { code });
}}
>
Run
<img src={play} alt="play icon" class="play-logo" />
</button>
<div class="shortcut">⌘+↵</div>
</div>
<div style="display: flex; float: right;"></div>
</div>
<div>
{#if !error_display}
<Index
{autoscroll}
{version}
{initial_height}
{app_mode}
{is_embed}
{theme_mode}
{control_page_title}
{container}
{info}
{eager}
{mount_css}
{client}
{upload_files}
bind:worker_proxy
{fetch_implementation}
{EventSource_factory}
{space}
{host}
{src}
/>
{:else}
<ErrorDisplay
is_embed={error_display.is_embed}
error={error_display.error}
/>
{/if}
</div>
</div>
{/if}
</div>
</div>
<style>
.parent-container {
width: 100%;
height: 100%;
}
.child-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
border: 1px solid rgb(229 231 235);
border-radius: 0.375rem;
}
@media (min-width: 768px) {
.child-container {
flex-direction: row;
}
.code-editor-border {
border-right: 1px solid rgb(229 231 235);
}
}
.code-editor {
flex-grow: 1;
flex: 1 1 0%;
display: flex;
flex-direction: column;
}
.loading-panel {
display: flex;
justify-content: space-between;
vertical-align: middle;
height: 2rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
border-bottom: 1px solid rgb(229 231 235);
}
.code-header {
padding-top: 0.25rem;
flex-grow: 1;
font-family: monospace;
margin-top: 4px;
}
.loading-section {
align-items: center;
display: flex;
margin-left: 0.5rem;
margin-right: 0.5rem;
color: #999b9e;
font-family: sans-serif;
}
.lightning-logo {
width: 1rem;
height: 1rem;
margin: 0.125rem;
}
.preview {
flex: 1 1 0%;
display: flex;
flex-direction: column;
}
.buttons {
display: flex;
justify-content: space-between;
align-items: middle;
height: 2rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
border-bottom: 1px solid rgb(229 231 235);
}
.run {
display: flex;
align-items: center;
color: #999b9e;
}
.button {
display: flex;
align-items: center;
font-weight: 500;
padding-left: 0.5rem;
padding-right: 0.25rem;
border-radius: 0.375rem;
float: right;
margin: 0.25rem;
color: rgb(107 114 128);
background: #eff1f3;
border: none;
font-size: 100%;
cursor: pointer;
font-family: sans-serif;
}
.play-logo {
width: 0.75rem;
height: 0.75rem;
margin: 0.125rem;
}
:global(div.code-editor div.block) {
border-radius: 0;
border: none;
}
:global(div.code-editor div.block .cm-gutters) {
background-color: white;
}
:global(div.code-editor div.block .cm-content) {
width: 0;
}
:global(div.lite-demo div.gradio-container) {
height: 100%;
overflow-y: scroll;
margin: 0 !important;
}
:global(.gradio-container) {
max-width: none !important;
}
.code-editor :global(label) {
display: none;
}
.code-editor :global(.codemirror-wrappper) {
border-radius: var(--block-radius);
}
.code-editor :global(> .block) {
border: none !important;
}
.code-editor :global(.cm-scroller) {
height: 100% !important;
}
:global(.code-editor .block) {
border-style: none !important;
height: 100%;
}
:global(.code-editor .container) {
display: none;
}
:global(.code-editor button) {
display: none;
}
.loading-dot {
position: relative;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #fd7b00;
color: #fd7b00;
box-shadow: 9999px 0 0 -1px;
animation: loading-dot 2s infinite linear;
animation-delay: 0.25s;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
@keyframes loading-dot {
0% {
box-shadow: 9999px 0 0 -1px;
}
50% {
box-shadow: 9999px 0 0 2px;
}
100% {
box-shadow: 9999px 0 0 -1px;
}
}
</style>

View File

@ -62,6 +62,7 @@ export function bootstrap_custom_element(
requirements: gradioLiteAppOptions.requirements,
files: gradioLiteAppOptions.files,
entrypoint: gradioLiteAppOptions.entrypoint,
playground: this.hasAttribute("playground"),
...gradioComponentOptions
});
});
@ -174,7 +175,6 @@ export function bootstrap_custom_element(
const firstRequirementsElement = requirementsElements[0];
const requirementsTxt = firstRequirementsElement?.textContent ?? "";
options.requirements = parseRequirementsTxt(requirementsTxt);
return options;
}
}

View File

@ -77,7 +77,8 @@ def hi(name):
themeMode: null,
autoScroll: false,
controlPageTitle: false,
appMode: true
appMode: true,
playground: false
});
});
onDestroy(() => {

View File

@ -10,6 +10,7 @@ import { wasm_proxied_EventSource_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 { ThemeMode } from "../types";
import { bootstrap_custom_element } from "./custom-element";
@ -65,6 +66,7 @@ export interface Options {
autoScroll: boolean;
controlPageTitle: boolean;
appMode: boolean;
playground: boolean | undefined;
}
export function create(options: Options): GradioAppController {
// TODO: Runtime type validation for options.
@ -87,6 +89,28 @@ export function create(options: Options): GradioAppController {
showError((event as CustomEvent).detail);
});
function clean_indent(code: string): string {
const lines = code.split("\n");
let min_indent: any = null;
lines.forEach((line) => {
const current_indent = line.match(/^(\s*)\S/);
if (current_indent) {
const indent_length = current_indent[1].length;
min_indent =
min_indent !== null
? Math.min(min_indent, indent_length)
: indent_length;
}
});
if (min_indent === null || min_indent === 0) {
return code.trim();
}
const normalized_lines = lines.map((line) => line.substring(min_indent));
return normalized_lines.join("\n").trim();
}
options.code = options.code ? clean_indent(options.code) : options.code;
// 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)
@ -116,55 +140,109 @@ export function create(options: Options): GradioAppController {
};
let app: SvelteComponent;
let app_props: any;
let loaded = false;
function showError(error: Error): void {
if (app != null) {
app.$destroy();
}
app = new ErrorDisplay({
target: options.target,
props: {
is_embed: !options.isEmbed,
error
}
});
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", (code) => {
options.code = clean_indent(code.detail.code);
loaded = true;
worker_proxy
.runPythonCode(options.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 = new Index({
target: options.target,
props: {
// embed source
space: null,
src: null,
host: null,
// embed info
info: options.info,
container: options.container,
is_embed: options.isEmbed,
initial_height: options.initialHeight ?? "300px", // default: 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,
upload_files,
mount_css: overridden_mount_css,
fetch_implementation: overridden_fetch,
EventSource_factory
}
});
app_props = {
// embed source
space: null,
src: null,
host: null,
// embed info
info: options.info,
container: options.container,
is_embed: options.isEmbed,
initial_height: options.initialHeight ?? "300px", // default: 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,
upload_files,
mount_css: overridden_mount_css,
fetch_implementation: overridden_fetch,
EventSource_factory
};
if (options.playground) {
app = new Playground({
target: options.target,
props: {
...app_props,
code: options.code,
error_display: null,
loaded: loaded
}
});
app.$on("code", (code) => {
options.code = clean_indent(code.detail.code);
loaded = true;
worker_proxy
.runPythonCode(options.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", () => {
@ -177,6 +255,7 @@ export function create(options: Options): GradioAppController {
return {
run_code: (code: string) => {
code = clean_indent(code);
return worker_proxy
.runPythonCode(code)
.then(launchNewApp)

View File

@ -1,6 +1,6 @@
<!doctype html>
<!-- A demo HTML file to test the bundled JS and CSS files -->
<html style="margin: 0; padding: 0; height: 100%">
<html>
<head>
<meta charset="utf-8" />
<meta
@ -14,36 +14,109 @@
href="https://fonts.gstatic.com"
crossorigin="anonymous"
/>
<script type="module" crossorigin src="./dist/lite.js"></script>
<link rel="stylesheet" href="./dist/lite.css" />
</head>
<body style="margin: 0; padding: 0; height: 100%">
<div id="gradio-app"></div>
<body style="padding: 10px; height: 100%; width: 100%">
<h1>Lorem Ipsum Dolor</h1>
<script type="module">
// type="module" is necessary to use `createGradioApp()`, which is loaded with <script type="module" /> tag above.
createGradioApp({
target: document.getElementById("gradio-app"),
code: `
import gradio as gr
<p>
<strong>Lorem ipsum</strong> dolor sit amet, consectetur adipiscing elit.
Nullam vitae est maximus,
<a href="https://example.com">link to example</a>, vestibulum lorem quis,
vehicula nunc.
</p>
def greet(name):
return "Hello, " + name + "!"
<h2>Subheading: Curabitur blandit</h2>
gr.Interface(fn=greet, inputs="text", outputs="text").launch()
`,
info: true,
container: true,
isEmbed: false,
initialHeight: "300px",
eager: false,
themeMode: null,
autoScroll: false,
controlPageTitle: false,
appMode: true
});
</script>
<p>
Curabitur blandit tempus porttitor.
<em>Etiam porta sem malesuada</em> magna mollis euismod. Donec ullamcorper
nulla non metus auctor fringilla.
</p>
<h3>Subsection: Vestibulum</h3>
<p>
Vestibulum id ligula porta felis euismod semper. Sed posuere consectetur
est at lobortis.
</p>
<blockquote>Cras mattis consectetur purus sit amet fermentum.</blockquote>
<pre><code>// Sample code block
function helloWorld() {
console.log("Hello, world!");
}
helloWorld();
</code></pre>
<ul>
<li>
Praesent commodo cursus magna, vel scelerisque nisl consectetur et.
</li>
<li>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet.
</li>
</ul>
<p>
<b>Bolded text:</b> Duis mollis, est non commodo luctus, nisi erat
porttitor ligula, eget lacinia odio sem nec elit.
</p>
<h4>Further Information</h4>
<p>
For more details, visit
<a href="https://example.com/moreinfo">our information page</a>.
</p>
<gradio-lite playground>
import gradio as gr gr.Interface(fn=lambda x: x, inputs=gr.Textbox(),
outputs=gr.Textbox()).launch()
</gradio-lite>
<h3>Subsection: Vestibulum</h3>
<p>
Vestibulum id ligula porta felis euismod semper. Sed posuere consectetur
est at lobortis.
</p>
<blockquote>Cras mattis consectetur purus sit amet fermentum.</blockquote>
<pre><code>// Sample code block
function helloWorld() {
console.log("Hello, world!");
}
helloWorld();
</code></pre>
<ul>
<li>
Praesent commodo cursus magna, vel scelerisque nisl consectetur et.
</li>
<li>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet.
</li>
</ul>
<p>
<b>Bolded text:</b> Duis mollis, est non commodo luctus, nisi erat
porttitor ligula, eget lacinia odio sem nec elit.
</p>
<h4>Further Information</h4>
<p>
For more details, visit
<a href="https://example.com/moreinfo">our information page</a>.
</p>
<gradio-lite playground>
import gradio as gr gr.ChatInterface(lambda x,y:x).launch()
</gradio-lite>
</body>
</html>

View File

@ -223,6 +223,7 @@ async function initializeApp(
updateProgress("Installing packages");
await micropip.install.callKwargs(options.requirements, { keep_going: true });
console.debug("Packages are installed.");
updateProgress("App is now loaded");
}
const ctx = self as DedicatedWorkerGlobalScope | SharedWorkerGlobalScope;

View File

@ -92,7 +92,11 @@ export class WorkerProxy extends EventTarget {
}
})
.then(() => {
console.debug("WorkerProxy.constructor(): App initialization is done.");
this.dispatchEvent(
new CustomEvent("initialization-completed", {
detail: null
})
);
})
.catch((error) => {
console.error(