From 3594c8b40dc721d21dbad03f3f90d1287bf0a6e8 Mon Sep 17 00:00:00 2001 From: hannahblair Date: Thu, 6 Aug 2026 15:01:15 +0100 Subject: [PATCH 1/6] gate behind hf spaces --- .../workflow/WorkflowCanvas.svelte | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.svelte b/js/workflowcanvas/workflow/WorkflowCanvas.svelte index d4d05220ed..592c61743f 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.svelte +++ b/js/workflowcanvas/workflow/WorkflowCanvas.svelte @@ -12,6 +12,7 @@ import LayoutIcon from "./icons/LayoutIcon.svelte"; import InfoIcon from "./icons/InfoIcon.svelte"; import CodeIcon from "./icons/CodeIcon.svelte"; + import DownloadIcon from "./icons/DownloadIcon.svelte"; import { MODALITIES, @@ -388,6 +389,38 @@ toasts = toasts.filter((t) => t.id !== id); } + function serializedWorkflow(): string { + return JSON.stringify(sanitize_for_save($workflow), null, 2); + } + + async function copyWorkflowJson(): Promise { + try { + await navigator.clipboard.writeText(serializedWorkflow()); + showToast("Copied workflow JSON to clipboard", 2000, "success"); + } catch { + showToast( + "Couldn't copy to clipboard — use Download instead", + 3500, + "warning" + ); + } + } + + function downloadWorkflowJson(): void { + const blob = new Blob([serializedWorkflow()], { + type: "application/json" + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const safeName = ($workflow.name || "workflow").replace(/[^\w.-]+/g, "_"); + a.download = `${safeName}.json`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + // v1 shape for read paths; writes go through v2 store actions. const legacyView = $derived(toLegacyShape($workflow)); @@ -2322,6 +2355,24 @@ View API + {#if auth.isHFSpace} + + + {/if} {#if saveIndicator} Date: Thu, 6 Aug 2026 15:24:38 +0100 Subject: [PATCH 2/6] btn --- js/workflowcanvas/workflow/WorkflowCanvas.css | 45 ++++++++++++ .../workflow/WorkflowCanvas.svelte | 68 ++++++++++++++----- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.css b/js/workflowcanvas/workflow/WorkflowCanvas.css index 793506b9ba..a619e24618 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.css +++ b/js/workflowcanvas/workflow/WorkflowCanvas.css @@ -337,6 +337,51 @@ button.access-badge.access-readonly.open { border-color: #3a3b48; } +.toolbar-export-wrap { + position: relative; + display: flex; + align-items: center; +} + +.toolbar-export-menu { + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 180px; + background: #16171f; + border: 1px solid #2a2b36; + border-radius: 10px; + padding: 6px; + display: flex; + flex-direction: column; + gap: 2px; + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.45), + 0 0 0 1px rgba(255, 255, 255, 0.03); + z-index: 40; +} + +.toolbar-export-menu-btn { + display: flex; + align-items: center; + gap: 8px; + font-family: "Manrope", sans-serif; + font-size: 12px; + font-weight: 600; + padding: 6px 10px; + border: none; + border-radius: 6px; + background: transparent; + color: #e0e1e6; + cursor: pointer; + text-align: left; + transition: background 0.15s; +} + +.toolbar-export-menu-btn:hover { + background: #1e1f2a; +} + .toolbar-token-input { font-family: "JetBrains Mono", monospace; font-size: 11px; diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.svelte b/js/workflowcanvas/workflow/WorkflowCanvas.svelte index 592c61743f..c3e29136ff 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.svelte +++ b/js/workflowcanvas/workflow/WorkflowCanvas.svelte @@ -13,6 +13,7 @@ import InfoIcon from "./icons/InfoIcon.svelte"; import CodeIcon from "./icons/CodeIcon.svelte"; import DownloadIcon from "./icons/DownloadIcon.svelte"; + import ChevronDownIcon from "./icons/ChevronDownIcon.svelte"; import { MODALITIES, @@ -337,6 +338,7 @@ ); let showShortcuts = $state(false); let showUserMenu = $state(false); + let showExportMenu = $state(false); let showApiPanel = $state(false); // Popover shown when the "Run only" badge is clicked, explaining why editing // is disabled and how to enable it. @@ -1818,6 +1820,9 @@ if (showUserMenu && !target?.closest(".toolbar-user-wrap")) { showUserMenu = false; } + if (showExportMenu && !target?.closest(".toolbar-export-wrap")) { + showExportMenu = false; + } if (showAccessInfo && !target?.closest(".access-info-wrap")) { showAccessInfo = false; } @@ -2356,22 +2361,42 @@ View API {#if auth.isHFSpace} - - +
+ + {#if showExportMenu} +
+ + +
+ {/if} +
{/if} {#if saveIndicator} Date: Thu, 6 Aug 2026 15:56:00 +0100 Subject: [PATCH 3/6] tweak --- js/workflowcanvas/workflow/WorkflowCanvas.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.css b/js/workflowcanvas/workflow/WorkflowCanvas.css index a619e24618..50fdcf78a8 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.css +++ b/js/workflowcanvas/workflow/WorkflowCanvas.css @@ -367,7 +367,7 @@ button.access-badge.access-readonly.open { gap: 8px; font-family: "Manrope", sans-serif; font-size: 12px; - font-weight: 600; + font-weight: 400; padding: 6px 10px; border: none; border-radius: 6px; From 254ad993c32210ee214d9fced6d617e5b1e04f77 Mon Sep 17 00:00:00 2001 From: hannahblair Date: Fri, 7 Aug 2026 11:30:21 +0100 Subject: [PATCH 4/6] tweak --- js/workflowcanvas/workflow/WorkflowCanvas.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.svelte b/js/workflowcanvas/workflow/WorkflowCanvas.svelte index c3e29136ff..bccfaceac8 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.svelte +++ b/js/workflowcanvas/workflow/WorkflowCanvas.svelte @@ -2398,7 +2398,7 @@ {/if} {/if} - {#if saveIndicator} + {#if saveIndicator && !auth.isHFSpace} Date: Fri, 14 Aug 2026 19:57:13 +0100 Subject: [PATCH 5/6] refactor to use save to space --- gradio/workflow.py | 7 + js/workflowcanvas/workflow/WorkflowCanvas.css | 45 ----- .../workflow/WorkflowCanvas.svelte | 164 +++++++++--------- 3 files changed, 91 insertions(+), 125 deletions(-) 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.css b/js/workflowcanvas/workflow/WorkflowCanvas.css index 50fdcf78a8..793506b9ba 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.css +++ b/js/workflowcanvas/workflow/WorkflowCanvas.css @@ -337,51 +337,6 @@ button.access-badge.access-readonly.open { border-color: #3a3b48; } -.toolbar-export-wrap { - position: relative; - display: flex; - align-items: center; -} - -.toolbar-export-menu { - position: absolute; - top: calc(100% + 8px); - left: 0; - min-width: 180px; - background: #16171f; - border: 1px solid #2a2b36; - border-radius: 10px; - padding: 6px; - display: flex; - flex-direction: column; - gap: 2px; - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.45), - 0 0 0 1px rgba(255, 255, 255, 0.03); - z-index: 40; -} - -.toolbar-export-menu-btn { - display: flex; - align-items: center; - gap: 8px; - font-family: "Manrope", sans-serif; - font-size: 12px; - font-weight: 400; - padding: 6px 10px; - border: none; - border-radius: 6px; - background: transparent; - color: #e0e1e6; - cursor: pointer; - text-align: left; - transition: background 0.15s; -} - -.toolbar-export-menu-btn:hover { - background: #1e1f2a; -} - .toolbar-token-input { font-family: "JetBrains Mono", monospace; font-size: 11px; diff --git a/js/workflowcanvas/workflow/WorkflowCanvas.svelte b/js/workflowcanvas/workflow/WorkflowCanvas.svelte index bccfaceac8..24acc466de 100644 --- a/js/workflowcanvas/workflow/WorkflowCanvas.svelte +++ b/js/workflowcanvas/workflow/WorkflowCanvas.svelte @@ -12,8 +12,8 @@ import LayoutIcon from "./icons/LayoutIcon.svelte"; import InfoIcon from "./icons/InfoIcon.svelte"; import CodeIcon from "./icons/CodeIcon.svelte"; - import DownloadIcon from "./icons/DownloadIcon.svelte"; - import ChevronDownIcon from "./icons/ChevronDownIcon.svelte"; + import UploadIcon from "./icons/UploadIcon.svelte"; + import { uploadFile } from "@huggingface/hub"; import { MODALITIES, @@ -338,8 +338,19 @@ ); let showShortcuts = $state(false); let showUserMenu = $state(false); - let showExportMenu = $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); @@ -391,38 +402,37 @@ toasts = toasts.filter((t) => t.id !== id); } - function serializedWorkflow(): string { - return JSON.stringify(sanitize_for_save($workflow), null, 2); - } - - async function copyWorkflowJson(): Promise { + 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 navigator.clipboard.writeText(serializedWorkflow()); - showToast("Copied workflow JSON to clipboard", 2000, "success"); - } catch { + 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( - "Couldn't copy to clipboard — use Download instead", - 3500, - "warning" + `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; } } - function downloadWorkflowJson(): void { - const blob = new Blob([serializedWorkflow()], { - type: "application/json" - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - const safeName = ($workflow.name || "workflow").replace(/[^\w.-]+/g, "_"); - a.download = `${safeName}.json`; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - } - // v1 shape for read paths; writes go through v2 store actions. const legacyView = $derived(toLegacyShape($workflow)); @@ -1820,9 +1830,6 @@ if (showUserMenu && !target?.closest(".toolbar-user-wrap")) { showUserMenu = false; } - if (showExportMenu && !target?.closest(".toolbar-export-wrap")) { - showExportMenu = false; - } if (showAccessInfo && !target?.closest(".access-info-wrap")) { showAccessInfo = false; } @@ -2360,43 +2367,16 @@ View API - {#if auth.isHFSpace} -
- - {#if showExportMenu} -
- - -
- {/if} -
+ {#if auth.isHFSpace && auth.canWrite && spaceId && auth.token} + {/if} {#if saveIndicator && !auth.isHFSpace} {/if} + {#if saveToSpaceConfirm} + + +
(saveToSpaceConfirm = false)} + > + +
+ {/if} + {#if showApiPanel} Date: Fri, 14 Aug 2026 18:58:10 +0000 Subject: [PATCH 6/6] add changeset --- .changeset/sweet-sloths-beam.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/sweet-sloths-beam.md 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