mirror of
https://github.com/gradio-app/gradio.git
synced 2025-04-12 12:40:29 +08:00
Allow functions that solely update component properties to run in the frontend by setting js=True
(#10500)
* changes * add changeset * revert * changes * add changeset * changes * fe changse * notebook * changes * fix fe * changes * add changeset * change * add changeset * notebook * add tests * changes * changes * changes * changes * changes * type * changes * changes * changes * notebooks * fix * fix * changes * changes * changes * changes * changes * notebooks * changes * revert * changes * changes * changes * notebook * changes * done * changes * add changeset * update * changes * add changeset * added a guide --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
parent
9b6a29253f
commit
16d419b9f1
7
.changeset/fuzzy-bananas-sneeze.md
Normal file
7
.changeset/fuzzy-bananas-sneeze.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
"@gradio/client": minor
|
||||
"@gradio/core": minor
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:Allow functions that solely update component properties to run in the frontend by setting `js=True`
|
@ -271,6 +271,7 @@ export interface Dependency {
|
||||
stream_every: number;
|
||||
like_user_message: boolean;
|
||||
event_specific_args: string[];
|
||||
js_implementation: string | null;
|
||||
}
|
||||
|
||||
export interface DependencyTypes {
|
||||
|
1
demo/todo_list_js/run.ipynb
Normal file
1
demo/todo_list_js/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: todo_list_js"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["\"\"\"\n", "This is a simple todo list app that allows you to edit tasks and mark tasks as complete.\n", "All actions are performed on the client side.\n", "\"\"\"\n", "import gradio as gr\n", "\n", "tasks = [\"Get a job\", \"Marry rich\", \"\", \"\", \"\", \"\"]\n", "textboxes = []\n", "buttons = []\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column(scale=3):\n", " gr.Markdown(\"# A Simple Interactive Todo List\")\n", " with gr.Column(scale=2):\n", " with gr.Row():\n", " freeze_button = gr.Button(\"Freeze tasks\", variant=\"stop\")\n", " edit_button = gr.Button(\"Edit tasks\")\n", " for i in range(6):\n", " with gr.Row() as r:\n", " t = gr.Textbox(tasks[i], placeholder=\"Enter a task\", show_label=False, container=False, scale=7, interactive=True)\n", " b = gr.Button(\"\u2714\ufe0f\", interactive=bool(tasks[i]), variant=\"primary\" if tasks[i] else \"secondary\")\n", " textboxes.append(t)\n", " buttons.append(b)\n", " t.change(lambda : gr.Button(interactive=True, variant=\"primary\"), None, b, js=True)\n", " b.click(lambda : gr.Row(visible=False), None, r, js=True)\n", " freeze_button.click(lambda : [gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False)], None, textboxes, js=True)\n", " edit_button.click(lambda : [gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True)], None, textboxes, js=True)\n", " freeze_button.click(lambda : [gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False)], None, buttons, js=True)\n", " edit_button.click(lambda : [gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True)], None, buttons, js=True)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
32
demo/todo_list_js/run.py
Normal file
32
demo/todo_list_js/run.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
This is a simple todo list app that allows you to edit tasks and mark tasks as complete.
|
||||
All actions are performed on the client side.
|
||||
"""
|
||||
import gradio as gr
|
||||
|
||||
tasks = ["Get a job", "Marry rich", "", "", "", ""]
|
||||
textboxes = []
|
||||
buttons = []
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
with gr.Column(scale=3):
|
||||
gr.Markdown("# A Simple Interactive Todo List")
|
||||
with gr.Column(scale=2):
|
||||
with gr.Row():
|
||||
freeze_button = gr.Button("Freeze tasks", variant="stop")
|
||||
edit_button = gr.Button("Edit tasks")
|
||||
for i in range(6):
|
||||
with gr.Row() as r:
|
||||
t = gr.Textbox(tasks[i], placeholder="Enter a task", show_label=False, container=False, scale=7, interactive=True)
|
||||
b = gr.Button("✔️", interactive=bool(tasks[i]), variant="primary" if tasks[i] else "secondary")
|
||||
textboxes.append(t)
|
||||
buttons.append(b)
|
||||
t.change(lambda : gr.Button(interactive=True, variant="primary"), None, b, js=True)
|
||||
b.click(lambda : gr.Row(visible=False), None, r, js=True)
|
||||
freeze_button.click(lambda : [gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False)], None, textboxes, js=True)
|
||||
edit_button.click(lambda : [gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True)], None, textboxes, js=True)
|
||||
freeze_button.click(lambda : [gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False)], None, buttons, js=True)
|
||||
edit_button.click(lambda : [gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True)], None, buttons, js=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -29,6 +29,7 @@ import httpx
|
||||
from anyio import CapacityLimiter
|
||||
from gradio_client import utils as client_utils
|
||||
from gradio_client.documentation import document
|
||||
from groovy import transpile
|
||||
|
||||
from gradio import (
|
||||
analytics,
|
||||
@ -507,7 +508,7 @@ class BlockFunction:
|
||||
concurrency_id: str | None = None,
|
||||
tracks_progress: bool = False,
|
||||
api_name: str | Literal[False] = False,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
show_progress: Literal["full", "minimal", "hidden"] = "full",
|
||||
show_progress_on: Sequence[Component] | None = None,
|
||||
cancels: list[int] | None = None,
|
||||
@ -527,6 +528,7 @@ class BlockFunction:
|
||||
like_user_message: bool = False,
|
||||
event_specific_args: list[str] | None = None,
|
||||
page: str = "",
|
||||
js_implementation: str | None = None,
|
||||
):
|
||||
self.fn = fn
|
||||
self._id = _id
|
||||
@ -562,6 +564,8 @@ class BlockFunction:
|
||||
self.renderable = renderable
|
||||
self.rendered_in = rendered_in
|
||||
self.page = page
|
||||
if js_implementation:
|
||||
self.fn.__js_implementation__ = js_implementation # type: ignore
|
||||
|
||||
# We need to keep track of which events are cancel events
|
||||
# so that the client can call the /cancel route directly
|
||||
@ -626,6 +630,7 @@ class BlockFunction:
|
||||
"stream_every": self.stream_every,
|
||||
"like_user_message": self.like_user_message,
|
||||
"event_specific_args": self.event_specific_args,
|
||||
"js_implementation": getattr(self.fn, "__js_implementation__", None),
|
||||
}
|
||||
|
||||
|
||||
@ -713,7 +718,7 @@ class BlocksConfig:
|
||||
show_progress: Literal["full", "minimal", "hidden"] = "full",
|
||||
show_progress_on: Component | Sequence[Component] | None = None,
|
||||
api_name: str | None | Literal[False] = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
no_target: bool = False,
|
||||
queue: bool = True,
|
||||
batch: bool = False,
|
||||
@ -733,6 +738,7 @@ class BlocksConfig:
|
||||
stream_every: float = 0.5,
|
||||
like_user_message: bool = False,
|
||||
event_specific_args: list[str] | None = None,
|
||||
js_implementation: str | None = None,
|
||||
) -> tuple[BlockFunction, int]:
|
||||
"""
|
||||
Adds an event to the component's dependencies.
|
||||
@ -853,6 +859,11 @@ class BlocksConfig:
|
||||
|
||||
rendered_in = LocalContext.renderable.get()
|
||||
|
||||
if js is True and inputs:
|
||||
raise ValueError(
|
||||
"Cannot create event: events with js=True cannot have inputs."
|
||||
)
|
||||
|
||||
block_fn = BlockFunction(
|
||||
fn,
|
||||
inputs,
|
||||
@ -888,6 +899,7 @@ class BlocksConfig:
|
||||
like_user_message=like_user_message,
|
||||
event_specific_args=event_specific_args,
|
||||
page=self.root_block.current_page,
|
||||
js_implementation=js_implementation,
|
||||
)
|
||||
|
||||
self.fns[self.fn_id] = block_fn
|
||||
@ -1060,7 +1072,7 @@ class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta):
|
||||
title: str = "Gradio",
|
||||
css: str | None = None,
|
||||
css_paths: str | Path | Sequence[str | Path] | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
head: str | None = None,
|
||||
head_paths: str | Path | Sequence[str | Path] | None = None,
|
||||
fill_height: bool = False,
|
||||
@ -2196,7 +2208,7 @@ Received inputs:
|
||||
"components": [],
|
||||
"css": self.css,
|
||||
"connect_heartbeat": False,
|
||||
"js": self.js,
|
||||
"js": cast(str | Literal[True] | None, self.js),
|
||||
"head": self.head,
|
||||
"title": self.title or "Gradio",
|
||||
"space_id": self.space_id,
|
||||
@ -2233,6 +2245,30 @@ Received inputs:
|
||||
)
|
||||
return config
|
||||
|
||||
def transpile_to_js(self, quiet: bool = False):
|
||||
fns_to_transpile = [
|
||||
fn.fn for fn in self.fns.values() if fn.fn and fn.js is True
|
||||
]
|
||||
num_to_transpile = len(fns_to_transpile)
|
||||
if not quiet and num_to_transpile > 0:
|
||||
print("********************************************")
|
||||
print("* Trying to transpile functions from Python -> JS for performance\n")
|
||||
for index, fn in enumerate(fns_to_transpile):
|
||||
if not quiet:
|
||||
print(f"* ({index + 1}/{num_to_transpile}) {fn.__name__}: ", end="")
|
||||
if getattr(fn, "__js_implementation__", None) is None: # type: ignore
|
||||
try:
|
||||
fn.__js_implementation__ = transpile(fn, validate=True) # type: ignore
|
||||
if not quiet:
|
||||
print("✅")
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print("❌", e, end="\n\n")
|
||||
elif not quiet:
|
||||
print("✅")
|
||||
if not quiet and num_to_transpile > 0:
|
||||
print("********************************************\n")
|
||||
|
||||
def __enter__(self):
|
||||
render_context = get_render_context()
|
||||
if render_context is None:
|
||||
@ -2508,6 +2544,7 @@ Received inputs:
|
||||
self.pwa = utils.get_space() is not None if pwa is None else pwa
|
||||
self.max_threads = max_threads
|
||||
self._queue.max_thread_count = max_threads
|
||||
self.transpile_to_js(quiet=quiet)
|
||||
self.config = self.get_config_file()
|
||||
|
||||
self.ssr_mode = (
|
||||
|
@ -96,7 +96,7 @@ class ChatInterface(Blocks):
|
||||
flagging_dir: str = ".gradio/flagged",
|
||||
css: str | None = None,
|
||||
css_paths: str | Path | Sequence[str | Path] | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
head: str | None = None,
|
||||
head_paths: str | Path | Sequence[str | Path] | None = None,
|
||||
analytics_enabled: bool | None = None,
|
||||
|
@ -36,7 +36,7 @@ INTERFACE_TEMPLATE = '''
|
||||
cancels: dict[str, Any] | list[dict[str, Any]] | None = None,
|
||||
every: Timer | float | None = None,
|
||||
trigger_mode: Literal["once", "multiple", "always_last"] | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
concurrency_limit: int | None | Literal["default"] = "default",
|
||||
concurrency_id: str | None = None,
|
||||
show_api: bool = True,
|
||||
|
@ -370,7 +370,7 @@ class BlocksConfigDict(TypedDict):
|
||||
components: list[dict[str, Any]]
|
||||
css: str | None
|
||||
connect_heartbeat: bool
|
||||
js: str | None
|
||||
js: str | Literal[True] | None
|
||||
head: str | None
|
||||
title: str
|
||||
space_id: str | None
|
||||
|
@ -587,7 +587,7 @@ class EventListener(str):
|
||||
postprocess: bool = True,
|
||||
cancels: dict[str, Any] | list[dict[str, Any]] | None = None,
|
||||
trigger_mode: Literal["once", "multiple", "always_last"] | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
concurrency_limit: int | None | Literal["default"] = "default",
|
||||
concurrency_id: str | None = None,
|
||||
show_api: bool = True,
|
||||
@ -751,7 +751,7 @@ def on(
|
||||
postprocess: bool = True,
|
||||
cancels: dict[str, Any] | list[dict[str, Any]] | None = None,
|
||||
trigger_mode: Literal["once", "multiple", "always_last"] | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
concurrency_limit: int | None | Literal["default"] = "default",
|
||||
concurrency_id: str | None = None,
|
||||
show_api: bool = True,
|
||||
|
@ -127,7 +127,7 @@ class Interface(Blocks):
|
||||
concurrency_limit: int | None | Literal["default"] = "default",
|
||||
css: str | None = None,
|
||||
css_paths: str | Path | Sequence[str | Path] | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
head: str | None = None,
|
||||
head_paths: str | Path | Sequence[str | Path] | None = None,
|
||||
additional_inputs: str | Component | Sequence[str | Component] | None = None,
|
||||
@ -947,7 +947,7 @@ class TabbedInterface(Blocks):
|
||||
theme: Theme | str | None = None,
|
||||
analytics_enabled: bool | None = None,
|
||||
css: str | None = None,
|
||||
js: str | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
head: str | None = None,
|
||||
):
|
||||
"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Styling
|
||||
# Gradio Themes
|
||||
|
||||
Gradio themes are the easiest way to customize the look and feel of your app. You can choose from a variety of themes, or create your own. To do so, pass the `theme=` kwarg to the `Interface` constructor. For example:
|
||||
|
71
guides/04_additional-features/13_client-side-functions.md
Normal file
71
guides/04_additional-features/13_client-side-functions.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Client Side Functions
|
||||
|
||||
Gradio allows you to run certain "simple" functions directly in the browser by setting `js=True` in your event listeners. This will **automatically convert your Python code into JavaSCript**, which significantly improves the responsiveness of your app by avoiding a round trip to the server for simple UI updates.
|
||||
|
||||
The difference in responsiveness is most noticeable on hosted applications (like Hugging Face Spaces), when the server is under heavy load, with high-latency connections, or when many users are accessing the app simultaneously.
|
||||
|
||||
## When to Use Client Side Functions
|
||||
|
||||
Client side functions are ideal for updating component properties (like visibility, placeholders, interactive state, or styling).
|
||||
|
||||
Here's a basic example:
|
||||
|
||||
```py
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row() as row:
|
||||
btn = gr.Button("Hide this row")
|
||||
|
||||
# This function runs in the browser without a server roundtrip
|
||||
btn.click(
|
||||
lambda: gr.Row(visible=False),
|
||||
None,
|
||||
row,
|
||||
js=True
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
Client side functions have some important restrictions:
|
||||
* They can only update component properties (not values)
|
||||
* They cannot take any inputs
|
||||
|
||||
Here are some functions that will work with `js=True`:
|
||||
|
||||
```py
|
||||
# Simple property updates
|
||||
lambda: gr.Textbox(lines=4)
|
||||
|
||||
# Multiple component updates
|
||||
lambda: [gr.Textbox(lines=4), gr.Button(interactive=False)]
|
||||
|
||||
# Using gr.update() for property changes
|
||||
lambda: gr.update(visible=True, interactive=False)
|
||||
```
|
||||
|
||||
We are working to increase the space of functions that can be transpiled to JavaScript so that they can be run in the browser. [Follow the Groovy library for more info](https://github.com/abidlabs/groovy-transpiler).
|
||||
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a more complete example showing how client side functions can improve the user experience:
|
||||
|
||||
$code_todo_list_js
|
||||
|
||||
|
||||
## Behind the Scenes
|
||||
|
||||
When you set `js=True`, Gradio:
|
||||
|
||||
1. Transpiles your Python function to JavaScript
|
||||
|
||||
2. Runs the function directly in the browser
|
||||
|
||||
3. Still sends the request to the server (for consistency and to handle any side effects)
|
||||
|
||||
This provides immediate visual feedback while ensuring your application state remains consistent.
|
@ -289,7 +289,7 @@
|
||||
trigger_id: trigger_id
|
||||
};
|
||||
|
||||
if (dep.frontend_fn) {
|
||||
if (dep.frontend_fn && typeof dep.frontend_fn !== "boolean") {
|
||||
dep
|
||||
.frontend_fn(
|
||||
payload.data.concat(
|
||||
@ -314,6 +314,21 @@
|
||||
);
|
||||
} else {
|
||||
if (dep.backend_fn) {
|
||||
if (dep.js_implementation) {
|
||||
let js_fn = new AsyncFunction(
|
||||
`let result = await (${dep.js_implementation})(...arguments);
|
||||
return (!Array.isArray(result)) ? [result] : result;`
|
||||
);
|
||||
js_fn(...payload.data)
|
||||
.then((js_result) => {
|
||||
handle_update(js_result, dep_index);
|
||||
payload.js_implementation = true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
payload.js_implementation = false;
|
||||
});
|
||||
}
|
||||
trigger_prediction(dep, payload);
|
||||
}
|
||||
}
|
||||
@ -391,6 +406,9 @@
|
||||
submit_map.set(dep_index, submission);
|
||||
|
||||
for await (const message of submission) {
|
||||
if (payload.js_implementation) {
|
||||
return;
|
||||
}
|
||||
if (message.type === "data") {
|
||||
handle_data(message);
|
||||
} else if (message.type === "render") {
|
||||
|
@ -478,12 +478,12 @@ export const AsyncFunction: new (
|
||||
* @returns The function, or null if the source code is invalid or missing
|
||||
*/
|
||||
export function process_frontend_fn(
|
||||
source: string | null | undefined | false,
|
||||
source: string | null | undefined | boolean,
|
||||
backend_fn: boolean,
|
||||
input_length: number,
|
||||
output_length: number
|
||||
): ((...args: unknown[]) => Promise<unknown[]>) | null {
|
||||
if (!source) return null;
|
||||
if (!source || source === true) return null;
|
||||
|
||||
const wrap = backend_fn ? input_length === 1 : output_length === 1;
|
||||
try {
|
||||
|
@ -43,6 +43,7 @@ export interface Payload {
|
||||
data: unknown[];
|
||||
event_data?: unknown | null;
|
||||
trigger_id?: number | null;
|
||||
js_implementation?: boolean | null;
|
||||
}
|
||||
|
||||
/** A dependency as received from the backend */
|
||||
@ -75,6 +76,7 @@ export interface Dependency {
|
||||
stream_every: number;
|
||||
like_user_message: boolean;
|
||||
event_specific_args: string[];
|
||||
js_implementation: string | null;
|
||||
}
|
||||
|
||||
interface TypeDescription {
|
||||
|
@ -3,6 +3,7 @@ anyio>=3.0,<5.0
|
||||
audioop-lts<1.0; python_version >= "3.13" #it provides support for 'audioop' module removed in latest python version used by pydub
|
||||
fastapi>=0.115.2,<1.0
|
||||
ffmpy
|
||||
groovy~=0.1
|
||||
gradio_client==1.7.2
|
||||
httpx>=0.24.1
|
||||
huggingface_hub>=0.28.1
|
||||
|
Loading…
x
Reference in New Issue
Block a user