diff --git a/.changeset/sweet-sloths-beam.md b/.changeset/sweet-sloths-beam.md new file mode 100644 index 0000000000..6315f73747 --- /dev/null +++ b/.changeset/sweet-sloths-beam.md @@ -0,0 +1,6 @@ +--- +"@gradio/workflowcanvas": minor +"gradio": minor +--- + +feat:workflow: allow exporting workflow file when on hf spaces diff --git a/gradio/workflow.py b/gradio/workflow.py index 65b8b48bfd..75e1062a50 100644 --- a/gradio/workflow.py +++ b/gradio/workflow.py @@ -561,6 +561,12 @@ def get_write_access( return "true" if has_write_access(request, token) else "false" +def get_space_id(_data=None) -> str: + """Return this Space's repo id (`owner/name`) for the "Save to Space" button. + Empty locally — the button is hidden there anyway.""" + return os.getenv("SPACE_ID") or "" + + def get_oauth_available(_data=None) -> str: """Whether OAuth sign-in is actually wired up. On a Space this requires `hf_oauth: true` in the README metadata, which provisions OAUTH_CLIENT_ID @@ -1872,6 +1878,7 @@ def save_workflow( get_token, get_write_access, get_oauth_available, + get_space_id, call_space, call_model, fetch_dataset, diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.svelte b/js/workflowcanvas/workflow/WorkflowCanvas.svelte index d4d05220ed..24acc466de 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.svelte +++ b/js/workflowcanvas/workflow/WorkflowCanvas.svelte @@ -12,6 +12,8 @@ import LayoutIcon from "./icons/LayoutIcon.svelte"; import InfoIcon from "./icons/InfoIcon.svelte"; import CodeIcon from "./icons/CodeIcon.svelte"; + import UploadIcon from "./icons/UploadIcon.svelte"; + import { uploadFile } from "@huggingface/hub"; import { MODALITIES, @@ -337,6 +339,18 @@ let showShortcuts = $state(false); let showUserMenu = $state(false); let showApiPanel = $state(false); + let saveToSpaceConfirm = $state(false); + let savingToSpace = $state(false); + let spaceId = $state(""); + $effect(() => { + if (!server?.get_space_id) return; + void server + .get_space_id() + .then((id: string) => { + spaceId = id || ""; + }) + .catch(() => {}); + }); // Popover shown when the "Run only" badge is clicked, explaining why editing // is disabled and how to enable it. let showAccessInfo = $state(false); @@ -388,6 +402,37 @@ toasts = toasts.filter((t) => t.id !== id); } + async function saveToSpace(): Promise { + saveToSpaceConfirm = false; + if (!spaceId || !auth.token || savingToSpace) return; + savingToSpace = true; + const serialized = JSON.stringify(sanitize_for_save($workflow), null, 2); + try { + await uploadFile({ + repo: { type: "space", name: spaceId }, + accessToken: auth.token, + file: { + path: "workflow.json", + content: new Blob([serialized], { type: "application/json" }) + }, + commitTitle: "Update workflow.json from canvas" + }); + showToast( + `Committed workflow.json to ${spaceId} — the Space will restart.`, + 4000, + "success" + ); + } catch (e: any) { + const msg = + e?.message?.includes("403") || e?.statusCode === 403 + ? "Your token doesn't have write access to this Space repo." + : `Save failed: ${e?.message ?? e}`; + showToast(msg, 5000, "warning"); + } finally { + savingToSpace = false; + } + } + // v1 shape for read paths; writes go through v2 store actions. const legacyView = $derived(toLegacyShape($workflow)); @@ -2322,7 +2367,18 @@ View API - {#if saveIndicator} + {#if auth.isHFSpace && auth.canWrite && spaceId && auth.token} + + {/if} + {#if saveIndicator && !auth.isHFSpace} {/if} + {#if saveToSpaceConfirm} + + +
(saveToSpaceConfirm = false)} + > + +
+ {/if} + {#if showApiPanel}