Skip to content

Add Deep Links #10834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
081ca97
WIP
freddyaboulton Mar 13, 2025
d871c02
Fix
freddyaboulton Mar 17, 2025
ff19606
roughdraft
freddyaboulton Mar 18, 2025
234d815
Workinig
freddyaboulton Mar 18, 2025
80b5a1b
query params
freddyaboulton Mar 18, 2025
b6ae4cd
add changeset
gradio-pr-bot Mar 18, 2025
b02be2a
modify
freddyaboulton Mar 18, 2025
8f4c808
revert
freddyaboulton Mar 18, 2025
a7ea851
lint
freddyaboulton Mar 18, 2025
f0c64ec
Code
freddyaboulton Mar 18, 2025
c12787b
Fix
freddyaboulton Mar 18, 2025
e6803ae
lint
freddyaboulton Mar 18, 2025
2fb0d43
Add code
freddyaboulton Mar 18, 2025
e5bc12d
Fix
freddyaboulton Mar 18, 2025
279846e
Fix python unit tests
freddyaboulton Mar 18, 2025
5aaf05e
Update `markupsafe` dependency version (#10820)
abidlabs Mar 17, 2025
688e398
Adds a watermark parameter to `gr.Chatbot` that is added to copied te…
abidlabs Mar 17, 2025
746c493
Fix gr.load_chat (#10829)
aliabid94 Mar 17, 2025
11b14c2
Fix typo in docstring of Request class in route_utils.py (#10833)
abdesselam-benameur Mar 18, 2025
6d53abc
Fix cell menu not showing in non-editable dataframes (#10819)
hannahblair Mar 18, 2025
73cff1a
Sketch code generator (#10824)
aliabid94 Mar 18, 2025
7703142
chore: update versions (#10811)
gradio-pr-bot Mar 18, 2025
67910b1
minor fixes
freddyaboulton Mar 20, 2025
c91a08a
fix
freddyaboulton Mar 20, 2025
e96c56c
Add guide
freddyaboulton Mar 20, 2025
26173af
Minor tweaks
freddyaboulton Mar 20, 2025
9c788e7
fix conflicts
abidlabs Mar 21, 2025
25a2f47
Address comments
freddyaboulton Mar 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/cuddly-cycles-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@gradio/client": minor
"@self/app": minor
"@self/spa": minor
"gradio": minor
---

feat:Add Deep Links
6 changes: 5 additions & 1 deletion client/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
export class Client {
app_reference: string;
options: ClientOptions;
deep_link: string | null = null;

config: Config | undefined;
api_prefix = "";
Expand Down Expand Up @@ -183,6 +184,7 @@ export class Client {
options: ClientOptions = { events: ["data"] }
) {
this.app_reference = app_reference;
this.deep_link = options.query_params?.deep_link || null;
if (!options.events) {
options.events = ["data"];
}
Expand Down Expand Up @@ -307,7 +309,9 @@ export class Client {
let config: Config | undefined;

try {
config = await this.resolve_config(`${http_protocol}//${host}`);
// Create base URL
let configUrl = `${http_protocol}//${host}`;
config = await this.resolve_config(configUrl);

if (!config) {
throw new Error(CONFIG_ERROR_MSG);
Expand Down
5 changes: 4 additions & 1 deletion client/js/src/helpers/init_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ export async function resolve_config(
config.root = config_root;
return { ...config, path } as Config;
} else if (endpoint) {
const config_url = join_urls(endpoint, CONFIG_URL);
let config_url = join_urls(
endpoint,
this.deep_link ? CONFIG_URL + "?deep_link=" + this.deep_link : CONFIG_URL
);

const response = await this.fetch(config_url, {
headers,
Expand Down
2 changes: 2 additions & 0 deletions client/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export type SpaceStatusCallback = (a: SpaceStatus) => void;
// Configuration and Response Types
// --------------------------------
export interface Config {
deep_link_state?: "none" | "valid" | "invalid";
auth_required?: true;
analytics_enabled: boolean;
connect_heartbeat: boolean;
Expand Down Expand Up @@ -313,6 +314,7 @@ export interface ClientOptions {
with_null_state?: boolean;
events?: EventType[];
headers?: Record<string, string>;
query_params?: Record<string, string>;
}

export interface FileData {
Expand Down
1 change: 1 addition & 0 deletions demo/deep_link/run.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: deep_link"]}, {"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": ["import gradio as gr\n", "import random\n", "\n", "def random_response(message, history):\n", " return random.choice([\"Hi!\", \"Hello!\", \"Greetings!\"])\n", "\n", "with gr.Blocks() as demo:\n", " gr.ChatInterface(\n", " random_response,\n", " title=\"Greeting Bot\",\n", " description=\"Ask anything and receive a nice greeting!\",\n", " )\n", " gr.DeepLinkButton()\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch(share=True)\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
16 changes: 16 additions & 0 deletions demo/deep_link/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import gradio as gr
import random

def random_response(message, history):
return random.choice(["Hi!", "Hello!", "Greetings!"])

with gr.Blocks() as demo:
gr.ChatInterface(
random_response,
title="Greeting Bot",
description="Ask anything and receive a nice greeting!",
)
gr.DeepLinkButton()

if __name__ == "__main__":
demo.launch(share=True)
2 changes: 2 additions & 0 deletions gradio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
Dataframe,
Dataset,
DateTime,
DeepLinkButton,
DownloadButton,
Dropdown,
DuplicateButton,
Expand Down Expand Up @@ -235,4 +236,5 @@
"set_static_paths",
"skip",
"update",
"DeepLinkButton",
]
88 changes: 61 additions & 27 deletions gradio/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,43 @@ def set_event_trigger(
self.fn_id += 1
return block_fn, block_fn._id

@staticmethod
def config_for_block(
_id: int,
rendered_ids: list[int],
block: Block | Component,
renderable: Renderable | None = None,
) -> dict:
if renderable:
if _id not in rendered_ids:
return {}
if block.key:
block.key = f"{renderable._id}-{block.key}"
props = block.get_config() if hasattr(block, "get_config") else {}
block_config = {
"id": _id,
"type": block.get_block_name(),
"props": utils.delete_none(props),
"skip_api": block.skip_api,
"component_class_id": getattr(block, "component_class_id", None),
"key": block.key,
}
if renderable:
block_config["renderable"] = renderable._id
if not block.skip_api:
block_config["api_info"] = block.api_info() # type: ignore
if hasattr(block, "api_info_as_input"):
block_config["api_info_as_input"] = block.api_info_as_input() # type: ignore
else:
block_config["api_info_as_input"] = block.api_info() # type: ignore
if hasattr(block, "api_info_as_output"):
block_config["api_info_as_output"] = block.api_info_as_output() # type: ignore
else:
block_config["api_info_as_output"] = block.api_info() # type: ignore
block_config["example_inputs"] = block.example_inputs() # type: ignore

return block_config

def get_config(self, renderable: Renderable | None = None):
config = {
"page": {},
Expand Down Expand Up @@ -950,33 +987,9 @@ def get_layout(block: Block) -> Layout:
self.blocks.items()
) # freeze as list to prevent concurrent re-renders from changing the dict during loop, see https://github.com/gradio-app/gradio/issues/9991
for _id, block in blocks_items:
if renderable:
if _id not in rendered_ids:
continue
if block.key:
block.key = f"{renderable._id}-{block.key}"
props = block.get_config() if hasattr(block, "get_config") else {}
block_config = {
"id": _id,
"type": block.get_block_name(),
"props": utils.delete_none(props),
"skip_api": block.skip_api,
"component_class_id": getattr(block, "component_class_id", None),
"key": block.key,
}
if renderable:
block_config["renderable"] = renderable._id
if not block.skip_api:
block_config["api_info"] = block.api_info() # type: ignore
if hasattr(block, "api_info_as_input"):
block_config["api_info_as_input"] = block.api_info_as_input() # type: ignore
else:
block_config["api_info_as_input"] = block.api_info() # type: ignore
if hasattr(block, "api_info_as_output"):
block_config["api_info_as_output"] = block.api_info_as_output() # type: ignore
else:
block_config["api_info_as_output"] = block.api_info() # type: ignore
block_config["example_inputs"] = block.example_inputs() # type: ignore
block_config = self.config_for_block(_id, rendered_ids, block, renderable)
if not block_config:
continue
config["components"].append(block_config)
config["page"][block.page]["components"].append(block._id)

Expand Down Expand Up @@ -1791,6 +1804,13 @@ async def preprocess_data(
inputs_cached = data_model.model_validate(
inputs_cached, context={"validate_meta": True}
)
if isinstance(inputs_cached, (GradioModel, GradioRootModel)):
inputs_serialized = inputs_cached.model_dump()
else:
inputs_serialized = inputs_cached
if block._id not in state:
state[block._id] = block
state[block._id].value = inputs_serialized
processed_input.append(block.preprocess(inputs_cached))
else:
processed_input = inputs
Expand Down Expand Up @@ -1917,6 +1937,20 @@ async def postprocess_data(
if block._id in state:
block = state[block._id]
prediction_value = block.postprocess(prediction_value)
if isinstance(prediction_value, (GradioModel, GradioRootModel)):
prediction_value_serialized = prediction_value.model_dump()
else:
prediction_value_serialized = prediction_value
prediction_value_serialized = (
await processing_utils.async_move_files_to_cache(
prediction_value_serialized,
block,
postprocess=True,
)
)
if block._id not in state:
state[block._id] = block
state[block._id].value = prediction_value_serialized

outputs_cached = await processing_utils.async_move_files_to_cache(
prediction_value,
Expand Down
2 changes: 2 additions & 0 deletions gradio/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from gradio.components.dataframe import Dataframe
from gradio.components.dataset import Dataset
from gradio.components.datetime import DateTime
from gradio.components.deep_link_button import DeepLinkButton
from gradio.components.download_button import DownloadButton
from gradio.components.dropdown import Dropdown
from gradio.components.duplicate_button import DuplicateButton
Expand Down Expand Up @@ -121,4 +122,5 @@
"ParamViewer",
"MultimodalTextbox",
"NativePlot",
"DeepLinkButton",
]
125 changes: 125 additions & 0 deletions gradio/components/deep_link_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Predefined button to copy a shareable link to the current Gradio Space."""

from __future__ import annotations

import textwrap
import time
from collections.abc import Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Literal

from gradio_client.documentation import document

from gradio import utils
from gradio.components.base import Component
from gradio.components.button import Button
from gradio.context import get_blocks_context

if TYPE_CHECKING:
from gradio.components import Timer


@document()
class DeepLinkButton(Button):
"""
Creates a button that copies a shareable link to the current Gradio Space.
The link includes the current session hash as a query parameter.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add it to a few demos and link here?


is_template = True
n_created = 0

def __init__(
self,
value: str = "Share via Link",
copied_value: str = "Link Copied!",
*,
inputs: Component | Sequence[Component] | set[Component] | None = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of inputs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just following the convention for all components. They should take inputs in case value is a callable but it doesn't really make sense in this case.

variant: Literal["primary", "secondary"] = "primary",
size: Literal["sm", "md", "lg"] = "lg",
icon: str | Path | None = utils.get_icon_path("link.svg"),
link: str | None = None,
visible: bool = True,
interactive: bool = True,
elem_id: str | None = None, # noqa: ARG002
elem_classes: list[str] | str | None = None,
render: bool = True,
key: int | str | None = None,
scale: int | None = None,
min_width: int | None = None,
every: Timer | float | None = None,
):
"""
Parameters:
value: The text to display on the button.
copied_value: The text to display on the button after the link has been copied.
"""
self.copied_value = copied_value
super().__init__(
value,
inputs=inputs,
variant=variant,
size=size,
icon=icon,
link=link,
visible=visible,
interactive=interactive,
elem_id=f"gradio-share-link-button-{self.n_created}",
elem_classes=elem_classes,
render=render,
key=key,
scale=scale,
min_width=min_width,
every=every,
)
self.elem_id: str
self.n_created += 1
if get_blocks_context():
self.activate()

def activate(self):
"""Attach the click event to copy the share link."""
_js = self.get_share_link(self.value, self.copied_value)
# Need to separate events because can't run .then in a pure js
# function.
self.click(fn=None, inputs=[], outputs=[self], js=_js)
self.click(
fn=lambda: time.sleep(1) or self.value,
inputs=[],
outputs=[self],
queue=False,
)

def get_share_link(
self, value: str = "Share via Link", copied_value: str = "Link Copied!"
):
return textwrap.dedent(
"""
() => {
const sessionHash = window.__gradio_session_hash__;
fetch(`/gradio_api/deep_link?session_hash=${sessionHash}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
const currentUrl = new URL(window.location.href);
const cleanData = data.replace(/^"|"$/g, '');
if (cleanData) {
currentUrl.searchParams.set('deep_link', cleanData);
}
navigator.clipboard.writeText(currentUrl.toString());
})
.catch(error => {
console.error('Error fetching deep link:', error);
return "Error";
});

return "BUTTON_COPIED_VALUE";
}
""".replace("BUTTON_DEFAULT_VALUE", value).replace(
"BUTTON_COPIED_VALUE", copied_value
)
).replace("ID", self.elem_id)
1 change: 1 addition & 0 deletions gradio/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ class Page(TypedDict):

class BlocksConfigDict(TypedDict):
version: str
deep_link_state: NotRequired[Literal["valid", "invalid", "none"]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this parameter do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the URL is not valid, we return deep_link_state of invalid so that the browser can display an appropriate error message as opposed to just failing silently.

mode: str
app_id: int
dev_mode: bool
Expand Down
4 changes: 4 additions & 0 deletions gradio/icons/link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading