adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
452 lines
11 KiB
Svelte
452 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import CodeIcon from "./icons/CodeIcon.svelte";
|
|
|
|
interface ApiParam {
|
|
label: string;
|
|
parameter_name?: string;
|
|
type: string;
|
|
python_type: string;
|
|
}
|
|
interface ApiEndpoint {
|
|
api_name: string;
|
|
label: string;
|
|
parameters: ApiParam[];
|
|
returns: ApiParam[];
|
|
}
|
|
|
|
let {
|
|
server = {},
|
|
workflowName = "Workflow",
|
|
onClose
|
|
}: {
|
|
server?: Record<string, any>;
|
|
workflowName?: string;
|
|
onClose: () => void;
|
|
} = $props();
|
|
|
|
type Lang = "python" | "javascript" | "bash";
|
|
const LANGS: { key: Lang; label: string }[] = [
|
|
{ key: "python", label: "Python" },
|
|
{ key: "javascript", label: "JavaScript" },
|
|
{ key: "bash", label: "curl" }
|
|
];
|
|
|
|
let endpoints = $state<ApiEndpoint[]>([]);
|
|
let loading = $state(true);
|
|
let error = $state<string | null>(null);
|
|
let lang = $state<Lang>("python");
|
|
let copied = $state<string | null>(null);
|
|
|
|
// The Workflow app is served at the site root, so the API base is the
|
|
// current origin (+ any sub-path the app is mounted under, minus a trailing
|
|
// slash).
|
|
const root =
|
|
typeof window !== "undefined"
|
|
? (window.location.origin + window.location.pathname).replace(/\/$/, "")
|
|
: "";
|
|
|
|
onMount(async () => {
|
|
try {
|
|
if (!server?.get_workflow_api) {
|
|
throw new Error("This server does not expose a workflow API.");
|
|
}
|
|
const raw = await server.get_workflow_api();
|
|
const parsed = JSON.parse(raw);
|
|
endpoints = parsed.endpoints ?? [];
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : String(e);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
});
|
|
|
|
function example(p: ApiParam): string {
|
|
if (
|
|
p.python_type === "filepath" ||
|
|
p.python_type.startsWith("list[filepath")
|
|
)
|
|
return lang === "python"
|
|
? `handle_file('https://example.com/file')`
|
|
: `"https://example.com/file"`;
|
|
if (p.python_type === "float") return "3";
|
|
if (p.python_type === "bool") return lang === "python" ? "True" : "true";
|
|
if (p.python_type === "dict") return "{}";
|
|
return lang === "python" ? '"Hello!!"' : '"Hello!!"';
|
|
}
|
|
|
|
function pySnippet(ep: ApiEndpoint): string {
|
|
const needsHandleFile = ep.parameters.some((p) =>
|
|
p.python_type.includes("filepath")
|
|
);
|
|
const imports = needsHandleFile
|
|
? "from gradio_client import Client, handle_file"
|
|
: "from gradio_client import Client";
|
|
const args = ep.parameters.map((p) => `\t\t${paramName(p)}=${example(p)},`);
|
|
return [
|
|
imports,
|
|
"",
|
|
`client = Client("${root}")`,
|
|
"result = client.predict(",
|
|
...args,
|
|
`\t\tapi_name="${ep.api_name}"`,
|
|
")",
|
|
"print(result)"
|
|
].join("\n");
|
|
}
|
|
|
|
function jsSnippet(ep: ApiEndpoint): string {
|
|
const args = ep.parameters
|
|
.map((p) => `\t${paramName(p)}: ${example(p)}`)
|
|
.join(",\n");
|
|
return [
|
|
'import { Client } from "@gradio/client";',
|
|
"",
|
|
`const client = await Client.connect("${root}");`,
|
|
`const result = await client.predict("${ep.api_name}", {`,
|
|
args,
|
|
"});",
|
|
"console.log(result.data);"
|
|
].join("\n");
|
|
}
|
|
|
|
function bashSnippet(ep: ApiEndpoint): string {
|
|
const data = ep.parameters.map((p) => example(p)).join(", ");
|
|
return [
|
|
`curl -X POST ${root}/gradio_api/call${ep.api_name} \\`,
|
|
`\t-H "Content-Type: application/json" \\`,
|
|
`\t-d '{"data": [${data}]}' \\`,
|
|
`\t| awk -F'"' '{ print $4 }' \\`,
|
|
`\t| xargs -I {} curl -N ${root}/gradio_api/call${ep.api_name}/{}`
|
|
].join("\n");
|
|
}
|
|
|
|
function argName(label: string): string {
|
|
return (
|
|
label
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "_")
|
|
.replace(/^_+|_+$/g, "") || "value"
|
|
);
|
|
}
|
|
|
|
function paramName(param: ApiParam): string {
|
|
return param.parameter_name || argName(param.label);
|
|
}
|
|
|
|
function snippet(ep: ApiEndpoint): string {
|
|
if (lang === "python") return pySnippet(ep);
|
|
if (lang === "javascript") return jsSnippet(ep);
|
|
return bashSnippet(ep);
|
|
}
|
|
|
|
async function copy(ep: ApiEndpoint): Promise<void> {
|
|
try {
|
|
await navigator.clipboard.writeText(snippet(ep));
|
|
copied = ep.api_name;
|
|
setTimeout(() => {
|
|
if (copied === ep.api_name) copied = null;
|
|
}, 1400);
|
|
} catch {
|
|
/* clipboard unavailable */
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
<div class="api-overlay" onclick={onClose}>
|
|
<div class="api-panel" onclick={(e) => e.stopPropagation()}>
|
|
<div class="api-header">
|
|
<div class="api-title">
|
|
<span class="api-glyph"><CodeIcon /></span>
|
|
<div>
|
|
<div class="api-title-main">API</div>
|
|
<div class="api-title-sub">
|
|
Call <strong>{workflowName}</strong> programmatically — one endpoint per
|
|
subgraph
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="api-close" onclick={onClose} title="Close">×</button>
|
|
</div>
|
|
|
|
<div class="api-langs">
|
|
{#each LANGS as l}
|
|
<button
|
|
class="api-lang"
|
|
class:active={lang === l.key}
|
|
onclick={() => (lang = l.key)}>{l.label}</button
|
|
>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="api-body">
|
|
{#if loading}
|
|
<div class="api-empty">Loading API…</div>
|
|
{:else if error}
|
|
<div class="api-empty api-error">{error}</div>
|
|
{:else if endpoints.length === 0}
|
|
<div class="api-empty">
|
|
No API endpoints yet — add an output node to expose one.
|
|
</div>
|
|
{:else}
|
|
{#each endpoints as ep}
|
|
<div class="api-endpoint">
|
|
<div class="api-endpoint-head">
|
|
<span class="api-method">POST</span>
|
|
<span class="api-name">{ep.api_name}</span>
|
|
<button class="api-copy" onclick={() => copy(ep)}>
|
|
{copied === ep.api_name ? "Copied!" : "Copy"}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="api-io">
|
|
<div class="api-io-col">
|
|
<div class="api-io-label">
|
|
Accepts {ep.parameters.length} parameter{ep.parameters
|
|
.length === 1
|
|
? ""
|
|
: "s"}
|
|
</div>
|
|
{#each ep.parameters as p}
|
|
<div class="api-port">
|
|
<span class="api-port-name">{paramName(p)}</span>
|
|
<span class="api-port-type">{p.python_type}</span>
|
|
</div>
|
|
{:else}
|
|
<div class="api-port api-port-empty">no inputs</div>
|
|
{/each}
|
|
</div>
|
|
<div class="api-io-col">
|
|
<div class="api-io-label">Returns</div>
|
|
{#each ep.returns as r}
|
|
<div class="api-port">
|
|
<span class="api-port-name">{paramName(r)}</span>
|
|
<span class="api-port-type">{r.python_type}</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<pre class="api-code"><code>{snippet(ep)}</code></pre>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.api-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(8, 9, 13, 0.72);
|
|
backdrop-filter: blur(2px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
padding: var(--size-6);
|
|
}
|
|
.api-panel {
|
|
width: min(720px, 100%);
|
|
max-height: 86vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #101118;
|
|
border: 1px solid #2a2b38;
|
|
border-radius: var(--radius-xl);
|
|
box-shadow: 0 var(--size-6) 60px rgba(0, 0, 0, 0.5);
|
|
overflow: hidden;
|
|
}
|
|
.api-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
padding: var(--size-4) 18px;
|
|
border-bottom: 1px solid #1e1f2a;
|
|
}
|
|
.api-title {
|
|
display: flex;
|
|
gap: var(--size-3);
|
|
align-items: center;
|
|
}
|
|
.api-glyph {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--color-accent, #f97316);
|
|
background: #1a1b25;
|
|
border: 1px solid #2a2b38;
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--size-2) var(--size-2-5);
|
|
}
|
|
.api-title-main {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #e6e7ec;
|
|
}
|
|
.api-title-sub {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: var(--size-3);
|
|
color: #8a8c98;
|
|
margin-top: 2px;
|
|
}
|
|
.api-title-sub strong {
|
|
color: #a0a2ae;
|
|
font-weight: 600;
|
|
}
|
|
.api-close {
|
|
background: transparent;
|
|
border: none;
|
|
color: #6b6e78;
|
|
font-size: 22px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
padding: 0 var(--size-1);
|
|
}
|
|
.api-close:hover {
|
|
color: #e6e7ec;
|
|
}
|
|
.api-langs {
|
|
display: flex;
|
|
gap: var(--size-1);
|
|
padding: var(--size-2-5) 18px;
|
|
border-bottom: 1px solid #1e1f2a;
|
|
}
|
|
.api-lang {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: var(--size-3);
|
|
font-weight: 500;
|
|
padding: var(--size-1) var(--size-3);
|
|
border-radius: var(--radius-full);
|
|
border: 1px solid transparent;
|
|
background: transparent;
|
|
color: #6b6e78;
|
|
cursor: pointer;
|
|
}
|
|
.api-lang:hover {
|
|
color: #a0a2ae;
|
|
}
|
|
.api-lang.active {
|
|
background: #1a1b25;
|
|
border-color: #2a2b38;
|
|
color: #e6e7ec;
|
|
}
|
|
.api-body {
|
|
overflow-y: auto;
|
|
padding: var(--size-4) 18px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--size-4);
|
|
}
|
|
.api-empty {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 13px;
|
|
color: #8a8c98;
|
|
text-align: center;
|
|
padding: var(--size-8) 0;
|
|
}
|
|
.api-error {
|
|
color: #f87171;
|
|
}
|
|
.api-endpoint {
|
|
border: 1px solid #1e1f2a;
|
|
border-radius: var(--size-2-5);
|
|
overflow: hidden;
|
|
}
|
|
.api-endpoint-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--size-2-5);
|
|
padding: var(--size-2-5) var(--size-3);
|
|
background: #16171f;
|
|
border-bottom: 1px solid #1e1f2a;
|
|
}
|
|
.api-method {
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.05em;
|
|
color: #1a1b25;
|
|
background: var(--color-accent, #f97316);
|
|
border-radius: var(--radius-sm);
|
|
padding: 2px var(--size-1-5);
|
|
}
|
|
.api-name {
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 13px;
|
|
color: #e6e7ec;
|
|
flex: 1;
|
|
}
|
|
.api-copy {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: var(--size-1) var(--size-2-5);
|
|
border: 1px solid #2a2b38;
|
|
border-radius: var(--radius-md);
|
|
background: transparent;
|
|
color: #a0a2ae;
|
|
cursor: pointer;
|
|
}
|
|
.api-copy:hover {
|
|
background: #1a1b25;
|
|
color: #e6e7ec;
|
|
}
|
|
.api-io {
|
|
display: flex;
|
|
gap: var(--size-6);
|
|
padding: var(--size-3);
|
|
border-bottom: 1px solid #1e1f2a;
|
|
}
|
|
.api-io-col {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.api-io-label {
|
|
font-family: "Manrope", sans-serif;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: #6b6e78;
|
|
margin-bottom: var(--size-1-5);
|
|
}
|
|
.api-port {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: var(--size-2);
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: var(--size-3);
|
|
padding: 3px 0;
|
|
}
|
|
.api-port-name {
|
|
color: #c5c7d0;
|
|
}
|
|
.api-port-type {
|
|
color: var(--color-accent, #f97316);
|
|
}
|
|
.api-port-empty {
|
|
color: #5a5d68;
|
|
font-style: italic;
|
|
}
|
|
.api-code {
|
|
margin: 0;
|
|
padding: var(--size-3) 14px;
|
|
background: #0b0c12;
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: var(--size-3);
|
|
line-height: 1.55;
|
|
color: #c5c7d0;
|
|
overflow-x: auto;
|
|
white-space: pre;
|
|
tab-size: 2;
|
|
}
|
|
.api-code code {
|
|
font-family: inherit;
|
|
}
|
|
</style>
|