Skip to content

Commit c05610c

Browse files
freddyaboultongradio-pr-botabidlabsaliabid94Ali Abid
authored
Add Deep Links (#10834)
* WIP * Fix * roughdraft * Workinig * query params * add changeset * modify * revert * lint * Code * Fix * lint * Add code * Fix * Fix python unit tests * Update `markupsafe` dependency version (#10820) * changes * add changeset * type * add changeset --------- Co-authored-by: gradio-pr-bot <[email protected]> * Adds a watermark parameter to `gr.Chatbot` that is added to copied text (#10814) * changes * add changeset * format' * test * copy * changes * doc * format --------- Co-authored-by: gradio-pr-bot <[email protected]> * Fix gr.load_chat (#10829) * changes * add changeset --------- Co-authored-by: Ali Abid <[email protected]> Co-authored-by: gradio-pr-bot <[email protected]> * Fix typo in docstring of Request class in route_utils.py (#10833) * Fix cell menu not showing in non-editable dataframes (#10819) * remove editable condition * - add test - improve html semantics * add changeset * fix test * fix test * - fix test - fix column widths changing on sort * swap e2e for story --------- Co-authored-by: gradio-pr-bot <[email protected]> * Sketch code generator (#10824) * changes * changes * add changeset * changes * changes * changes * changes * changes * changes * changes --------- Co-authored-by: Ali Abid <[email protected]> Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Abubakar Abid <[email protected]> * chore: update versions (#10811) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * minor fixes * fix * Add guide * Minor tweaks * Address comments --------- Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Abubakar Abid <[email protected]> Co-authored-by: aliabid94 <[email protected]> Co-authored-by: Ali Abid <[email protected]> Co-authored-by: Abdesselam Benameur <[email protected]> Co-authored-by: Hannah <[email protected]> Co-authored-by: Gradio PR Bot <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8ff0a5e commit c05610c

File tree

20 files changed

+405
-47
lines changed

20 files changed

+405
-47
lines changed

.changeset/cuddly-cycles-vanish.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@gradio/client": minor
3+
"@self/app": minor
4+
"@self/spa": minor
5+
"gradio": minor
6+
---
7+
8+
feat:Add Deep Links

client/js/src/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
export class Client {
4343
app_reference: string;
4444
options: ClientOptions;
45+
deep_link: string | null = null;
4546

4647
config: Config | undefined;
4748
api_prefix = "";
@@ -183,6 +184,7 @@ export class Client {
183184
options: ClientOptions = { events: ["data"] }
184185
) {
185186
this.app_reference = app_reference;
187+
this.deep_link = options.query_params?.deep_link || null;
186188
if (!options.events) {
187189
options.events = ["data"];
188190
}
@@ -307,7 +309,9 @@ export class Client {
307309
let config: Config | undefined;
308310

309311
try {
310-
config = await this.resolve_config(`${http_protocol}//${host}`);
312+
// Create base URL
313+
let configUrl = `${http_protocol}//${host}`;
314+
config = await this.resolve_config(configUrl);
311315

312316
if (!config) {
313317
throw new Error(CONFIG_ERROR_MSG);

client/js/src/helpers/init_helpers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ export async function resolve_config(
8686
config.root = config_root;
8787
return { ...config, path } as Config;
8888
} else if (endpoint) {
89-
const config_url = join_urls(endpoint, CONFIG_URL);
89+
let config_url = join_urls(
90+
endpoint,
91+
this.deep_link ? CONFIG_URL + "?deep_link=" + this.deep_link : CONFIG_URL
92+
);
9093

9194
const response = await this.fetch(config_url, {
9295
headers,

client/js/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export type SpaceStatusCallback = (a: SpaceStatus) => void;
154154
// Configuration and Response Types
155155
// --------------------------------
156156
export interface Config {
157+
deep_link_state?: "none" | "valid" | "invalid";
157158
auth_required?: true;
158159
analytics_enabled: boolean;
159160
connect_heartbeat: boolean;
@@ -313,6 +314,7 @@ export interface ClientOptions {
313314
with_null_state?: boolean;
314315
events?: EventType[];
315316
headers?: Record<string, string>;
317+
query_params?: Record<string, string>;
316318
}
317319

318320
export interface FileData {

demo/deep_link/run.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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}

demo/deep_link/run.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import gradio as gr
2+
import random
3+
4+
def random_response(message, history):
5+
return random.choice(["Hi!", "Hello!", "Greetings!"])
6+
7+
with gr.Blocks() as demo:
8+
gr.ChatInterface(
9+
random_response,
10+
title="Greeting Bot",
11+
description="Ask anything and receive a nice greeting!",
12+
)
13+
gr.DeepLinkButton()
14+
15+
if __name__ == "__main__":
16+
demo.launch(share=True)

gradio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
Dataframe,
3131
Dataset,
3232
DateTime,
33+
DeepLinkButton,
3334
DownloadButton,
3435
Dropdown,
3536
DuplicateButton,
@@ -235,4 +236,5 @@
235236
"set_static_paths",
236237
"skip",
237238
"update",
239+
"DeepLinkButton",
238240
]

gradio/blocks.py

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,43 @@ def set_event_trigger(
907907
self.fn_id += 1
908908
return block_fn, block_fn._id
909909

910+
@staticmethod
911+
def config_for_block(
912+
_id: int,
913+
rendered_ids: list[int],
914+
block: Block | Component,
915+
renderable: Renderable | None = None,
916+
) -> dict:
917+
if renderable:
918+
if _id not in rendered_ids:
919+
return {}
920+
if block.key:
921+
block.key = f"{renderable._id}-{block.key}"
922+
props = block.get_config() if hasattr(block, "get_config") else {}
923+
block_config = {
924+
"id": _id,
925+
"type": block.get_block_name(),
926+
"props": utils.delete_none(props),
927+
"skip_api": block.skip_api,
928+
"component_class_id": getattr(block, "component_class_id", None),
929+
"key": block.key,
930+
}
931+
if renderable:
932+
block_config["renderable"] = renderable._id
933+
if not block.skip_api:
934+
block_config["api_info"] = block.api_info() # type: ignore
935+
if hasattr(block, "api_info_as_input"):
936+
block_config["api_info_as_input"] = block.api_info_as_input() # type: ignore
937+
else:
938+
block_config["api_info_as_input"] = block.api_info() # type: ignore
939+
if hasattr(block, "api_info_as_output"):
940+
block_config["api_info_as_output"] = block.api_info_as_output() # type: ignore
941+
else:
942+
block_config["api_info_as_output"] = block.api_info() # type: ignore
943+
block_config["example_inputs"] = block.example_inputs() # type: ignore
944+
945+
return block_config
946+
910947
def get_config(self, renderable: Renderable | None = None):
911948
config = {
912949
"page": {},
@@ -950,33 +987,9 @@ def get_layout(block: Block) -> Layout:
950987
self.blocks.items()
951988
) # freeze as list to prevent concurrent re-renders from changing the dict during loop, see https://github.com/gradio-app/gradio/issues/9991
952989
for _id, block in blocks_items:
953-
if renderable:
954-
if _id not in rendered_ids:
955-
continue
956-
if block.key:
957-
block.key = f"{renderable._id}-{block.key}"
958-
props = block.get_config() if hasattr(block, "get_config") else {}
959-
block_config = {
960-
"id": _id,
961-
"type": block.get_block_name(),
962-
"props": utils.delete_none(props),
963-
"skip_api": block.skip_api,
964-
"component_class_id": getattr(block, "component_class_id", None),
965-
"key": block.key,
966-
}
967-
if renderable:
968-
block_config["renderable"] = renderable._id
969-
if not block.skip_api:
970-
block_config["api_info"] = block.api_info() # type: ignore
971-
if hasattr(block, "api_info_as_input"):
972-
block_config["api_info_as_input"] = block.api_info_as_input() # type: ignore
973-
else:
974-
block_config["api_info_as_input"] = block.api_info() # type: ignore
975-
if hasattr(block, "api_info_as_output"):
976-
block_config["api_info_as_output"] = block.api_info_as_output() # type: ignore
977-
else:
978-
block_config["api_info_as_output"] = block.api_info() # type: ignore
979-
block_config["example_inputs"] = block.example_inputs() # type: ignore
990+
block_config = self.config_for_block(_id, rendered_ids, block, renderable)
991+
if not block_config:
992+
continue
980993
config["components"].append(block_config)
981994
config["page"][block.page]["components"].append(block._id)
982995

@@ -1791,6 +1804,13 @@ async def preprocess_data(
17911804
inputs_cached = data_model.model_validate(
17921805
inputs_cached, context={"validate_meta": True}
17931806
)
1807+
if isinstance(inputs_cached, (GradioModel, GradioRootModel)):
1808+
inputs_serialized = inputs_cached.model_dump()
1809+
else:
1810+
inputs_serialized = inputs_cached
1811+
if block._id not in state:
1812+
state[block._id] = block
1813+
state[block._id].value = inputs_serialized
17941814
processed_input.append(block.preprocess(inputs_cached))
17951815
else:
17961816
processed_input = inputs
@@ -1917,6 +1937,20 @@ async def postprocess_data(
19171937
if block._id in state:
19181938
block = state[block._id]
19191939
prediction_value = block.postprocess(prediction_value)
1940+
if isinstance(prediction_value, (GradioModel, GradioRootModel)):
1941+
prediction_value_serialized = prediction_value.model_dump()
1942+
else:
1943+
prediction_value_serialized = prediction_value
1944+
prediction_value_serialized = (
1945+
await processing_utils.async_move_files_to_cache(
1946+
prediction_value_serialized,
1947+
block,
1948+
postprocess=True,
1949+
)
1950+
)
1951+
if block._id not in state:
1952+
state[block._id] = block
1953+
state[block._id].value = prediction_value_serialized
19201954

19211955
outputs_cached = await processing_utils.async_move_files_to_cache(
19221956
prediction_value,

gradio/components/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from gradio.components.dataframe import Dataframe
2121
from gradio.components.dataset import Dataset
2222
from gradio.components.datetime import DateTime
23+
from gradio.components.deep_link_button import DeepLinkButton
2324
from gradio.components.download_button import DownloadButton
2425
from gradio.components.dropdown import Dropdown
2526
from gradio.components.duplicate_button import DuplicateButton
@@ -121,4 +122,5 @@
121122
"ParamViewer",
122123
"MultimodalTextbox",
123124
"NativePlot",
125+
"DeepLinkButton",
124126
]

gradio/components/deep_link_button.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Predefined button to copy a shareable link to the current Gradio Space."""
2+
3+
from __future__ import annotations
4+
5+
import textwrap
6+
import time
7+
from collections.abc import Sequence
8+
from pathlib import Path
9+
from typing import TYPE_CHECKING, Literal
10+
11+
from gradio_client.documentation import document
12+
13+
from gradio import utils
14+
from gradio.components.base import Component
15+
from gradio.components.button import Button
16+
from gradio.context import get_blocks_context
17+
18+
if TYPE_CHECKING:
19+
from gradio.components import Timer
20+
21+
22+
@document()
23+
class DeepLinkButton(Button):
24+
"""
25+
Creates a button that copies a shareable link to the current Gradio Space.
26+
The link includes the current session hash as a query parameter.
27+
"""
28+
29+
is_template = True
30+
n_created = 0
31+
32+
def __init__(
33+
self,
34+
value: str = "Share via Link",
35+
copied_value: str = "Link Copied!",
36+
*,
37+
inputs: Component | Sequence[Component] | set[Component] | None = None,
38+
variant: Literal["primary", "secondary"] = "primary",
39+
size: Literal["sm", "md", "lg"] = "lg",
40+
icon: str | Path | None = utils.get_icon_path("link.svg"),
41+
link: str | None = None,
42+
visible: bool = True,
43+
interactive: bool = True,
44+
elem_id: str | None = None, # noqa: ARG002
45+
elem_classes: list[str] | str | None = None,
46+
render: bool = True,
47+
key: int | str | None = None,
48+
scale: int | None = None,
49+
min_width: int | None = None,
50+
every: Timer | float | None = None,
51+
):
52+
"""
53+
Parameters:
54+
value: The text to display on the button.
55+
copied_value: The text to display on the button after the link has been copied.
56+
"""
57+
self.copied_value = copied_value
58+
super().__init__(
59+
value,
60+
inputs=inputs,
61+
variant=variant,
62+
size=size,
63+
icon=icon,
64+
link=link,
65+
visible=visible,
66+
interactive=interactive,
67+
elem_id=f"gradio-share-link-button-{self.n_created}",
68+
elem_classes=elem_classes,
69+
render=render,
70+
key=key,
71+
scale=scale,
72+
min_width=min_width,
73+
every=every,
74+
)
75+
self.elem_id: str
76+
self.n_created += 1
77+
if get_blocks_context():
78+
self.activate()
79+
80+
def activate(self):
81+
"""Attach the click event to copy the share link."""
82+
_js = self.get_share_link(self.value, self.copied_value)
83+
# Need to separate events because can't run .then in a pure js
84+
# function.
85+
self.click(fn=None, inputs=[], outputs=[self], js=_js)
86+
self.click(
87+
fn=lambda: time.sleep(1) or self.value,
88+
inputs=[],
89+
outputs=[self],
90+
queue=False,
91+
)
92+
93+
def get_share_link(
94+
self, value: str = "Share via Link", copied_value: str = "Link Copied!"
95+
):
96+
return textwrap.dedent(
97+
"""
98+
() => {
99+
const sessionHash = window.__gradio_session_hash__;
100+
fetch(`/gradio_api/deep_link?session_hash=${sessionHash}`)
101+
.then(response => {
102+
if (!response.ok) {
103+
throw new Error('Network response was not ok');
104+
}
105+
return response.text();
106+
})
107+
.then(data => {
108+
const currentUrl = new URL(window.location.href);
109+
const cleanData = data.replace(/^"|"$/g, '');
110+
if (cleanData) {
111+
currentUrl.searchParams.set('deep_link', cleanData);
112+
}
113+
navigator.clipboard.writeText(currentUrl.toString());
114+
})
115+
.catch(error => {
116+
console.error('Error fetching deep link:', error);
117+
return "Error";
118+
});
119+
120+
return "BUTTON_COPIED_VALUE";
121+
}
122+
""".replace("BUTTON_DEFAULT_VALUE", value).replace(
123+
"BUTTON_COPIED_VALUE", copied_value
124+
)
125+
).replace("ID", self.elem_id)

gradio/data_classes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ class Page(TypedDict):
366366

367367
class BlocksConfigDict(TypedDict):
368368
version: str
369+
deep_link_state: NotRequired[Literal["valid", "invalid", "none"]]
369370
mode: str
370371
app_id: int
371372
dev_mode: bool

gradio/icons/link.svg

Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)