Lite: Support the custom HTML element syntax <gradio-lite> (#5953)

* Create a custom element `<gradio-app`> for Gradio-lite

* Parse `<gradio-app`> attributes to configure the top level component

* Move custom-element code to lite/custom-element module

* add changeset

* Apply formatter

* Rename the custom element `<gradio-app>` to `<gradio-lite>`

* Fix the .gradio-lite to be minimum

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
Yuichiro Tachibana (Tsuchiya) 2023-10-18 15:07:13 +09:00 committed by GitHub
parent 6780d660bb
commit 921334526f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 1 deletions

View File

@ -0,0 +1,7 @@
---
"@gradio/app": minor
"@gradio/theme": minor
"gradio": minor
---
feat:Lite: Support the custom HTML element syntax `<gradio-lite>`

View File

@ -0,0 +1,163 @@
import { create, type Options } from "..";
interface GradioComponentOptions {
info: Options["info"];
container: Options["container"];
isEmbed: Options["isEmbed"];
initialHeight?: Options["initialHeight"];
eager: Options["eager"];
themeMode: Options["themeMode"];
autoScroll: Options["autoScroll"];
controlPageTitle: Options["controlPageTitle"];
appMode: Options["appMode"];
}
interface GradioLiteAppOptions {
files?: Options["files"];
requirements?: Options["requirements"];
code?: Options["code"];
entrypoint?: Options["entrypoint"];
}
function parseRequirementsTxt(content: string): string[] {
return content
.split("\n")
.filter((r) => !r.startsWith("#"))
.map((r) => r.trim())
.filter((r) => r !== "");
}
export function bootstrap_custom_element(): void {
const CUSTOM_ELEMENT_NAME = "gradio-lite";
if (customElements.get(CUSTOM_ELEMENT_NAME)) {
return;
}
class GradioLiteAppElement extends HTMLElement {
constructor() {
super();
const gradioComponentOptions = this.parseGradioComponentOptions();
const gradioLiteAppOptions = this.parseGradioLiteAppOptions();
this.innerHTML = "";
create({
target: this, // Same as `js/app/src/main.ts`
code: gradioLiteAppOptions.code,
requirements: gradioLiteAppOptions.requirements,
files: gradioLiteAppOptions.files,
entrypoint: gradioLiteAppOptions.entrypoint,
...gradioComponentOptions
});
}
parseGradioComponentOptions(): GradioComponentOptions {
// Parse the options from the attributes of the <gradio-lite> element.
// The following attributes are supported:
// * info: boolean
// * container: boolean
// * embed: boolean
// * initial-height: string
// * eager: boolean
// * theme: "light" | "dark" | null
// * auto-scroll: boolean
// * control-page-title: boolean
// * app-mode: boolean
const info = this.hasAttribute("info");
const container = this.hasAttribute("container");
const isEmbed = this.hasAttribute("embed");
const initialHeight = this.getAttribute("initial-height");
const eager = this.hasAttribute("eager");
const themeMode = this.getAttribute("theme");
const autoScroll = this.hasAttribute("auto-scroll");
const controlPageTitle = this.hasAttribute("control-page-title");
const appMode = this.hasAttribute("app-mode");
return {
info,
container,
isEmbed,
initialHeight: initialHeight ?? undefined,
eager,
themeMode:
themeMode != null && ["light", "dark"].includes(themeMode)
? (themeMode as GradioComponentOptions["themeMode"])
: null,
autoScroll,
controlPageTitle,
appMode
};
}
parseGradioLiteAppOptions(): GradioLiteAppOptions {
// When gradioLiteAppElement only contains text content, it is treated as the Python code.
if (this.childElementCount === 0) {
return { code: this.textContent ?? "" };
}
// When it contains child elements, parse them as options. Available child elements are:
// * <gradio-file />
// Represents a file to be mounted in the virtual file system of the Wasm worker.
// At least 1 <gradio-file> element must have the `entrypoint` attribute.
// The following 2 forms are supported:
// * <gradio-file name="{file name}" >{file content}</gradio-file>
// * <gradio-file name="{file name}" url="{remote URL}" />
// * <gradio-requirements>{requirements.txt}</gradio-requirements>
// * <gradio-code>{Python code}</gradio-code>
const options: GradioLiteAppOptions = {};
const fileElements = this.getElementsByTagName("gradio-file");
for (const fileElement of fileElements) {
const name = fileElement.getAttribute("name");
if (name == null) {
throw new Error("<gradio-file> must have the name attribute.");
}
const entrypoint = fileElement.hasAttribute("entrypoint");
const url = fileElement.getAttribute("url");
options.files ??= {};
if (url != null) {
options.files[name] = { url };
} else {
options.files[name] = { data: fileElement.textContent ?? "" };
}
if (entrypoint) {
if (options.entrypoint != null) {
throw new Error("Multiple entrypoints are not allowed.");
}
options.entrypoint = name;
}
}
const codeElements = this.getElementsByTagName("gradio-code");
if (codeElements.length > 1) {
console.warn(
"Multiple <gradio-code> elements are found. Only the first one will be used."
);
}
const firstCodeElement = codeElements[0];
options.code = firstCodeElement?.textContent ?? undefined;
const requirementsElements = this.getElementsByTagName(
"gradio-requirements"
);
if (requirementsElements.length > 1) {
console.warn(
"Multiple <gradio-requirements> elements are found. Only the first one will be used."
);
}
const firstRequirementsElement = requirementsElements[0];
const requirementsTxt = firstRequirementsElement?.textContent ?? "";
options.requirements = parseRequirementsTxt(requirementsTxt);
return options;
}
}
customElements.define(CUSTOM_ELEMENT_NAME, GradioLiteAppElement);
}

View File

@ -7,6 +7,7 @@ import { wasm_proxied_mount_css, mount_prebuilt_css } from "./css";
import type { mount_css } from "../css";
import Index from "../Index.svelte";
import type { ThemeMode } from "../components/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";
@ -43,7 +44,7 @@ interface GradioAppController {
unmount: () => void;
}
interface Options {
export interface Options {
target: HTMLElement;
files?: WorkerProxyOptions["files"];
requirements?: WorkerProxyOptions["requirements"];
@ -191,6 +192,8 @@ export function create(options: Options): GradioAppController {
// @ts-ignore
globalThis.createGradioApp = create;
bootstrap_custom_element();
declare let BUILD_MODE: string;
if (BUILD_MODE === "dev") {
(async function () {

View File

@ -108,6 +108,11 @@ export default defineConfig(({ mode }) => {
fileName.indexOf(".svelte") > -1
) {
return selector;
} else if (
// For the custom element <gradio-lite>. See theme/src/global.css for the details.
/^gradio-lite(\:[^\:]+)?/.test(selector)
) {
return selector;
}
return prefixedSelector;
}

View File

@ -1,3 +1,20 @@
/*
Custom element styles
*/
gradio-lite {
display: flex;
}
/*
To avoid FOUC of custom elements
Refs:
https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/#the-%3Adefined-selector
https://github.com/pyscript/pypercard/blob/66d3550abd8e478f9389e6b87790d5985e55ef7f/static/pyscript.css#L6-L8
*/
gradio-lite:not(:defined) {
display: none;
}
.scroll-hide {
-ms-overflow-style: none;
scrollbar-width: none;