API Docs with support for Blocks (#1397)

* backend

* updated PyPi version to 3.0.10

* updated PyPi version to 3.0.9b10

* fixes

* updated PyPi version to 3.0.9b11

* changes

* changes

* changes git push

* changes

* undo

* fixes

Co-authored-by: Ali Abid <aabid94@gmail.com>
This commit is contained in:
Ali Abdalla 2022-06-06 09:10:42 -07:00 committed by GitHub
parent f6414d3c33
commit 42cee33dcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 300 additions and 814 deletions

View File

@ -38,6 +38,7 @@ With this model you can lorem ipsum
inputs=[disease, xray_scan],
outputs=xray_results,
status_tracker=xray_progress,
api_name="xray_model"
)
with gr.TabItem("CT Scan"):
@ -51,6 +52,7 @@ With this model you can lorem ipsum
inputs=[disease, ct_scan],
outputs=ct_results,
status_tracker=ct_progress,
api_name="ct_model"
)
upload_btn = gr.Button("Upload Results")

View File

@ -8,7 +8,7 @@ import random
import sys
import time
import webbrowser
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Dict, List, Optional, Tuple
import anyio
from anyio import CapacityLimiter
@ -85,6 +85,7 @@ class Block:
outputs: Optional[Component | List[Component]],
preprocess: bool = True,
postprocess: bool = True,
api_name: Optional[AnyStr] = None,
js: Optional[str] = False,
no_target: bool = False,
status_tracker: Optional[StatusTracker] = None,
@ -99,6 +100,7 @@ class Block:
outputs: output list
preprocess: whether to run the preprocess methods of components
postprocess: whether to run the postprocess methods of components
api_name: Defining this parameter exposes the endpoint in the api docs
js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components
no_target: if True, sets "targets" to [], used for Blocks "load" event
status_tracker: StatusTracker to visualize function progress
@ -115,20 +117,25 @@ class Block:
outputs = [outputs]
Context.root_block.fns.append(BlockFunction(fn, preprocess, postprocess))
Context.root_block.dependencies.append(
{
"targets": [self._id] if not no_target else [],
"trigger": event_name,
"inputs": [block._id for block in inputs],
"outputs": [block._id for block in outputs],
"backend_fn": fn is not None,
"js": js,
"status_tracker": status_tracker._id
if status_tracker is not None
else None,
"queue": queue,
}
)
dependency = {
"targets": [self._id] if not no_target else [],
"trigger": event_name,
"inputs": [block._id for block in inputs],
"outputs": [block._id for block in outputs],
"backend_fn": fn is not None,
"js": js,
"status_tracker": status_tracker._id
if status_tracker is not None
else None,
"queue": queue,
"api_name": api_name,
}
if api_name is not None:
dependency["documentation"] = [
[component.document_parameters("input") for component in inputs],
[component.document_parameters("output") for component in outputs],
]
Context.root_block.dependencies.append(dependency)
def get_config(self):
return {

View File

@ -244,6 +244,21 @@ class IOComponent(Component):
self._style["container"] = container
return self
@classmethod
def document_parameters(cls, target):
if target == "input":
doc = inspect.getdoc(cls.preprocess)
if "Parameters:\nx (" in doc:
return doc.split("Parameters:\nx ")[1].split("\n")[0]
return None
elif target == "output":
doc = inspect.getdoc(cls.postprocess)
if "Returns:\n" in doc:
return doc.split("Returns:\n")[1].split("\n")[0]
return None
else:
raise ValueError("Invalid doumentation target.")
class Textbox(Changeable, Submittable, IOComponent):
"""
@ -329,6 +344,10 @@ class Textbox(Changeable, Submittable, IOComponent):
def preprocess(self, x: str | None) -> Any:
"""
Any preprocessing needed to be performed on function input.
Parameters:
x (str): text
Returns:
(str): text
"""
if x is None:
return None
@ -415,6 +434,10 @@ class Textbox(Changeable, Submittable, IOComponent):
def postprocess(self, y: str | None):
"""
Any postprocessing needed to be performed on function output.
Parameters:
y (str | None): text
Returns:
(str | None): text
"""
if y is None:
return None
@ -518,21 +541,21 @@ class Number(Changeable, Submittable, IOComponent):
"__type__": "update",
}
def preprocess(self, x: int | float | None) -> int | float | None:
def preprocess(self, x: float | None) -> float | None:
"""
Parameters:
x (int | float | None): numeric input as a string
x (float | None): numeric input
Returns:
(int | float | None): number representing function input
(float | None): number representing function input
"""
if x is None:
return None
return self.round_to_precision(x, self.precision)
def preprocess_example(self, x: int | float | None) -> int | float | None:
def preprocess_example(self, x: float | None) -> float | None:
"""
Returns:
(int | float | None): Number representing function input
(float | None): Number representing function input
"""
if x is None:
return None
@ -584,18 +607,18 @@ class Number(Changeable, Submittable, IOComponent):
interpretation.insert(int(len(interpretation) / 2), [x, None])
return interpretation
def generate_sample(self) -> int | float:
def generate_sample(self) -> float:
return self.round_to_precision(1, self.precision)
# Output Functionalities
def postprocess(self, y: int | float | None) -> int | float | None:
def postprocess(self, y: float | None) -> float | None:
"""
Any postprocessing needed to be performed on function output.
Parameters:
y (int | float | None): numeric output
y (float | None): numeric output
Returns:
(int | float | None): number representing function output
(float | None): number representing function output
"""
if y is None:
return None
@ -740,9 +763,13 @@ class Slider(Changeable, IOComponent):
# Output Functionalities
def postprocess(self, y: int | float | None):
def postprocess(self, y: float | None):
"""
Any postprocessing needed to be performed on function output.
Parameters:
y (float | None): numeric output
Returns:
(float): numeric output or minimum number if None
"""
return self.minimum if y is None else y
@ -867,6 +894,10 @@ class Checkbox(Changeable, IOComponent):
def postprocess(self, y):
"""
Any postprocessing needed to be performed on function output.
Parameters:
y (bool): boolean output
Returns:
(bool): boolean output
"""
return y
@ -1015,6 +1046,10 @@ class CheckboxGroup(Changeable, IOComponent):
def postprocess(self, y):
"""
Any postprocessing needed to be performed on function output.
Parameters:
y (List[str]): List of selected choices
Returns:
(List[str]): List of selected choices
"""
return [] if y is None else y
@ -1160,6 +1195,10 @@ class Radio(Changeable, IOComponent):
def postprocess(self, y):
"""
Any postprocessing needed to be performed on function output.
Parameters:
y (str): string of choice
Returns:
(str): string of choice
"""
return (
y if y is not None else self.choices[0] if len(self.choices) > 0 else None
@ -3407,9 +3446,7 @@ class Model3D(Changeable, Editable, Clearable, IOComponent):
Parameters:
y (str): path to the model
Returns:
(str): file name
(str): file extension
(str): base64 url data
(Dict[name (str): file name, data (str): base64 url data] | None)
"""
if y is None:
return y
@ -3494,8 +3531,7 @@ class Plot(Changeable, Clearable, IOComponent):
Parameters:
y (str): plot data
Returns:
(str): plot type
(str): plot base64 or json
(Dict[type (str): plot type, plot (str): plot base64 | json] | None)
"""
if y is None:
return None

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Dict, List, Optional, Tuple
from gradio.blocks import Block
@ -15,6 +15,7 @@ class Changeable(Block):
inputs: List[Component],
outputs: List[Component],
status_tracker: Optional[StatusTracker] = None,
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -26,6 +27,7 @@ class Changeable(Block):
inputs: List of inputs
outputs: List of outputs
status_tracker: StatusTracker to visualize function progress
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of input and outputs components, return should be a list of values for output component.
Returns: None
"""
@ -35,6 +37,7 @@ class Changeable(Block):
inputs,
outputs,
status_tracker=status_tracker,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -49,6 +52,7 @@ class Clickable(Block):
inputs: List[Component],
outputs: List[Component],
status_tracker: Optional[StatusTracker] = None,
api_name: AnyStr = None,
queue=None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -60,6 +64,7 @@ class Clickable(Block):
inputs: List of inputs
outputs: List of outputs
status_tracker: StatusTracker to visualize function progress
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
_preprocess: If False, will not run preprocessing of component data before running 'fn'.
_postprocess: If False, will not run postprocessing of component data before returning 'fn' output.
@ -71,6 +76,7 @@ class Clickable(Block):
inputs,
outputs,
status_tracker=status_tracker,
api_name=api_name,
queue=queue,
js=_js,
preprocess=_preprocess,
@ -85,6 +91,7 @@ class Submittable(Block):
inputs: List[Component],
outputs: List[Component],
status_tracker: Optional[StatusTracker] = None,
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -96,6 +103,7 @@ class Submittable(Block):
inputs: List of inputs
outputs: List of outputs
status_tracker: StatusTracker to visualize function progress
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -105,6 +113,7 @@ class Submittable(Block):
inputs,
outputs,
status_tracker=status_tracker,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -118,6 +127,7 @@ class Editable(Block):
fn: Callable,
inputs: List[Component],
outputs: List[Component],
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -128,6 +138,7 @@ class Editable(Block):
fn: Callable function
inputs: List of inputs
outputs: List of outputs
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -136,6 +147,7 @@ class Editable(Block):
fn,
inputs,
outputs,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -149,6 +161,7 @@ class Clearable(Block):
fn: Callable,
inputs: List[Component],
outputs: List[Component],
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -159,6 +172,7 @@ class Clearable(Block):
fn: Callable function
inputs: List of inputs
outputs: List of outputs
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -167,6 +181,7 @@ class Clearable(Block):
fn,
inputs,
outputs,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -180,6 +195,7 @@ class Playable(Block):
fn: Callable,
inputs: List[Component],
outputs: List[Component],
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -190,6 +206,7 @@ class Playable(Block):
fn: Callable function
inputs: List of inputs
outputs: List of outputs
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -198,6 +215,7 @@ class Playable(Block):
fn,
inputs,
outputs,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -209,6 +227,7 @@ class Playable(Block):
fn: Callable,
inputs: List[Component],
outputs: List[Component],
api_name: Optional[AnyStr] = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -219,6 +238,7 @@ class Playable(Block):
fn: Callable function
inputs: List of inputs
outputs: List of outputs
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -227,6 +247,7 @@ class Playable(Block):
fn,
inputs,
outputs,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -238,6 +259,7 @@ class Playable(Block):
fn: Callable,
inputs: List[Component],
outputs: List[Component],
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -248,6 +270,7 @@ class Playable(Block):
fn: Callable function
inputs: List of inputs
outputs: List of outputs
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -256,6 +279,7 @@ class Playable(Block):
fn,
inputs,
outputs,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,
@ -269,6 +293,7 @@ class Streamable(Block):
fn: Callable,
inputs: List[Component],
outputs: List[Component],
api_name: AnyStr = None,
queue: Optional[bool] = None,
_js: Optional[str] = None,
_preprocess: bool = True,
@ -279,6 +304,7 @@ class Streamable(Block):
fn: Callable function
inputs: List of inputs
outputs: List of outputs
api_name: Defining this parameter exposes the endpoint in the api docs
_js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
Returns: None
"""
@ -288,6 +314,7 @@ class Streamable(Block):
fn,
inputs,
outputs,
api_name=api_name,
js=_js,
preprocess=_preprocess,
postprocess=_postprocess,

View File

@ -519,6 +519,7 @@ class Interface(Blocks):
submit_fn,
self.input_components,
self.output_components,
api_name="predict",
status_tracker=status_tracker,
)
clear_btn.click(

View File

@ -226,47 +226,19 @@ class App(FastAPI):
if Path(app.cwd).resolve() in Path(path).resolve().parents:
return FileResponse(Path(path).resolve())
@app.get("/api", response_class=HTMLResponse) # Needed for Spaces
@app.get("/api/", response_class=HTMLResponse)
def api_docs(request: Request):
inputs = [type(inp) for inp in app.blocks.input_components]
outputs = [type(out) for out in app.blocks.output_components]
input_types_doc, input_types = get_types(inputs, "input")
output_types_doc, output_types = get_types(outputs, "output")
input_names = [inp.get_block_name() for inp in app.blocks.input_components]
output_names = [
out.get_block_name() for out in app.blocks.output_components
]
if app.blocks.examples is not None:
sample_inputs = app.blocks.examples[0]
else:
sample_inputs = [
inp.generate_sample() for inp in app.blocks.input_components
]
docs = {
"inputs": input_names,
"outputs": output_names,
"len_inputs": len(inputs),
"len_outputs": len(outputs),
"inputs_lower": [name.lower() for name in input_names],
"outputs_lower": [name.lower() for name in output_names],
"input_types": input_types,
"output_types": output_types,
"input_types_doc": input_types_doc,
"output_types_doc": output_types_doc,
"sample_inputs": sample_inputs,
"auth": app.blocks.auth,
"local_login_url": urllib.parse.urljoin(app.blocks.local_url, "login"),
"local_api_url": urllib.parse.urljoin(
app.blocks.local_url, "api/predict"
),
}
return templates.TemplateResponse(
"api_docs.html", {"request": request, **docs}
)
@app.post("/api/queue/push/", dependencies=[Depends(login_check)])
async def queue_push(body: QueuePushBody):
job_hash, queue_position = queueing.push(body)
return {"hash": job_hash, "queue_position": queue_position}
@app.post("/api/predict/", dependencies=[Depends(login_check)])
async def predict(body: PredictBody, username: str = Depends(get_current_user)):
@app.post("/api/queue/status/", dependencies=[Depends(login_check)])
async def queue_status(body: QueueStatusBody):
status, data = queueing.get_status(body.hash)
return {"status": status, "data": data}
async def run_predict(
body: PredictBody, username: str = Depends(get_current_user)
):
if hasattr(body, "session_hash"):
if body.session_hash not in app.state_holder:
app.state_holder[body.session_hash] = {
@ -291,15 +263,24 @@ class App(FastAPI):
raise error
return output
@app.post("/api/queue/push/", dependencies=[Depends(login_check)])
async def queue_push(body: QueuePushBody):
job_hash, queue_position = queueing.push(body)
return {"hash": job_hash, "queue_position": queue_position}
@app.post("/api/queue/status/", dependencies=[Depends(login_check)])
async def queue_status(body: QueueStatusBody):
status, data = queueing.get_status(body.hash)
return {"status": status, "data": data}
@app.post("/api/{api_name}", dependencies=[Depends(login_check)])
@app.post("/api/{api_name}/", dependencies=[Depends(login_check)])
async def predict(
api_name: str, body: PredictBody, username: str = Depends(get_current_user)
):
if body.fn_index is None:
for i, fn in enumerate(app.blocks.dependencies):
if fn["api_name"] == api_name:
body.fn_index = i
break
if body.fn_index is None:
return JSONResponse(
content={
"error": f"This app has no endpoint /api/{api_name}/."
},
status_code=500,
)
return await run_predict(body=body, username=username)
return app
@ -329,19 +310,14 @@ def safe_join(directory: str, path: str) -> Optional[str]:
return posixpath.join(directory, filename)
def get_types(cls_set: List[Type], component: str):
def get_types(cls_set: List[Type]):
docset = []
types = []
if component == "input":
for cls in cls_set:
doc = inspect.getdoc(cls.preprocess)
doc_lines = doc.split("\n")
docset.append(doc_lines[1].split(":")[-1])
types.append(doc_lines[1].split(")")[0].split("(")[-1])
else:
for cls in cls_set:
doc = inspect.getdoc(cls.postprocess)
doc_lines = doc.split("\n")
docset.append(doc_lines[-1].split(":")[-1])
types.append(doc_lines[-1].split(")")[0].split("(")[-1])
for cls in cls_set:
doc = inspect.getdoc(cls)
doc_lines = doc.split("\n")
for line in doc_lines:
if "value (" in line:
types.append(line.split("value (")[1].split(")")[0])
docset.append(doc_lines[1].split(":")[-1])
return docset, types

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,10 @@ else
echo "Uploading to pypi"
set -e
git pull origin main
old_version=$(grep -Po "(?<=version=\")[^\"]+(?=\")" setup.py)
old_version=$(ggrep -Po "(?<=version=\")[^\"]+(?=\")" setup.py)
echo "Current version is $old_version. New version?"
read new_version
sed -i "s/version=\"$old_version\"/version=\"$new_version\"/g" setup.py
gsed -i "s/version=\"$old_version\"/version=\"$new_version\"/g" setup.py
echo -n $new_version > gradio/version.txt
rm -rf gradio/templates/frontend

View File

@ -0,0 +1,132 @@
<script lang="ts">
interface Component {
id: number;
props: {
name: string;
label?: string;
};
}
interface Dependency {
inputs: Array<number>;
outputs: Array<number>;
api_name: string | null;
documentation?: Array<Array<string | null>>;
}
export let components: Array<Component>;
export let dependencies: Array<Dependency>;
export let root: string;
if (root === "") {
root = window.location.href;
}
let just_copied = -1;
</script>
<div>
<h2 class="text-3xl text-center mb-6">
API Docs for
<span class="italic text-amber-500">
{root}
</span>
</h2>
<div class="flex flex-col gap-6">
{#each dependencies as dependency, d}
{#if dependency.documentation}
<div
class="bg-gray-50 border border-gray-100 dark:bg-gray-800 dark:border-gray-700 p-6 rounded"
>
<h3 class="text-3xl text-amber-500 font-semibold mb-2">
POST /api/{dependency.api_name}
</h3>
<div class="mb-6">
Full URL: <span class="underline"
>{root}api/{dependency.api_name}</span
>
<button
class="ml-1 px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700"
on:click={() => {
navigator.clipboard.writeText(
root + "api/" + dependency.api_name
);
just_copied = d;
setTimeout(() => {
just_copied = -1;
}, 500);
}}
>
{#if just_copied === d}copied!{:else}copy{/if}
</button>
</div>
<h4 class="text-2xl mt-6 mb-4">Input Payload</h4>
<div
class="block mb-4 text-lg bg-gray-100 dark:bg-gray-700 p-4 font-mono"
>
&#123;<br />
&nbsp;&nbsp;"data": [<br />
{#each dependency.documentation[0] as dependency_doc, i}
&nbsp;&nbsp;&nbsp;&nbsp;{dependency_doc?.substring(
0,
dependency_doc.indexOf(")") + 1
)},
<span class="text-pink-400 dark:text-pink-600"
>// represents {dependency_doc?.substring(
dependency_doc.indexOf(": ") + 2
)} of
{((label) => {
return label ? "'" + label + "'" : "the";
})(
components.filter((c) => c.id === dependency.inputs[i])[0]
.props.label
)}
<span class="capitalize"
>{components.filter((c) => c.id === dependency.inputs[i])[0]
.props.name}</span
> component
</span>
<br />
{/each}
&nbsp;&nbsp;]<br />
&#125;
</div>
<h4 class="text-2xl mt-6 mb-4">Response Object</h4>
<div
class="block mb-4 text-lg bg-gray-100 dark:bg-gray-700 p-4 font-mono"
>
&#123;<br />
&nbsp;&nbsp;"data": [<br />
{#each dependency.documentation[1] as dependency_doc, i}
&nbsp;&nbsp;&nbsp;&nbsp;{dependency_doc?.substring(
0,
dependency_doc.indexOf(")") + 1
)},
<span class="text-pink-400 dark:text-pink-600"
>// represents {dependency_doc?.substring(
dependency_doc.indexOf(": ") + 2
)} of
{((label) => {
return label ? "'" + label + "'" : "the";
})(
components.filter((c) => c.id === dependency.outputs[i])[0]
.props.label
)}
<span class="capitalize"
>{components.filter((c) => c.id === dependency.outputs[i])[0]
.props.name}</span
> component
</span>
<br />
{/each}
&nbsp;&nbsp;],<br />
&nbsp;&nbsp;"duration": (float)
<span class="text-pink-400 dark:text-pink-600">
// number of seconds to run function call</span
><br />
&#125;
</div>
</div>
{/if}
{/each}
</div>
</div>

View File

@ -8,6 +8,7 @@
import { _ } from "svelte-i18n";
import { setupi18n } from "./i18n";
import Render from "./Render.svelte";
import ApiDocs from "./ApiDocs.svelte";
import { tick } from "svelte";
setupi18n();
@ -44,6 +45,8 @@
status_tracker: number | null;
status?: string;
queue: boolean | null;
api_name: string | null;
documentation?: Array<Array<string>>;
}
export let root: string;
@ -77,6 +80,8 @@
}
}
});
let show_api_docs = dependencies.some((d) => "documentation" in d);
let api_docs_visible = false;
function is_dep(
id: number,
@ -389,7 +394,9 @@
class="mx-auto container px-4 py-6 dark:bg-gray-950"
class:flex-grow={(window.__gradio_mode__ = "app")}
>
{#if ready}
{#if api_docs_visible}
<ApiDocs {components} {dependencies} {root} />
{:else if ready}
<Render
component={rootNode.component}
id={rootNode.id}
@ -404,12 +411,25 @@
/>
{/if}
</div>
<footer class="flex justify-center pb-6">
<footer
class="flex justify-center pb-6 text-gray-300 dark:text-gray-500 font-semibold"
>
{#if show_api_docs}
<div
class="cursor-pointer hover:text-gray-400 dark:hover:text-gray-400 transition-colors"
on:click={() => {
api_docs_visible = !api_docs_visible;
}}
>
{#if api_docs_visible}hide{:else}view{/if} api
</div>
&nbsp; &bull; &nbsp;
{/if}
<a
href="https://gradio.app"
target="_blank"
rel="noreferrer"
class="group text-gray-300 dark:text-gray-500 hover:text-gray-400 dark:hover:text-gray-400 transition-colors font-semibold text-sm"
class="group hover:text-gray-400 dark:hover:text-gray-400 transition-colors"
>
{$_("interface.built_with_Gradio")}
<img

View File

@ -16,6 +16,7 @@
export let value: Array<string> | null = null;
export let style: Styles = {};
$: console.log(">", value);
let selected_image: number | null = null;
$: previous =