Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sweet-sloths-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gradio/workflowcanvas": minor
"gradio": minor
---

feat:workflow: allow exporting workflow file when on hf spaces
7 changes: 7 additions & 0 deletions gradio/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1872,6 +1878,7 @@ def save_workflow(
get_token,
get_write_access,
get_oauth_available,
get_space_id,
call_space,
call_model,
fetch_dataset,
Expand Down
93 changes: 92 additions & 1 deletion js/workflowcanvas/workflow/WorkflowCanvas.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -388,6 +402,37 @@
toasts = toasts.filter((t) => t.id !== id);
}

async function saveToSpace(): Promise<void> {
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));

Expand Down Expand Up @@ -2322,7 +2367,18 @@
<CodeIcon />
View API
</button>
{#if saveIndicator}
{#if auth.isHFSpace && auth.canWrite && spaceId && auth.token}
<button
class="tool-btn"
disabled={savingToSpace}
onclick={() => (saveToSpaceConfirm = true)}
title="Commit workflow.json to this Space's repo (will restart the Space)"
>
<UploadIcon />
{savingToSpace ? "Saving…" : "Save to Space"}
</button>
{/if}
{#if saveIndicator && !auth.isHFSpace}
<span
class="save-indicator"
in:fade={{ duration: 120 }}
Expand Down Expand Up @@ -2813,6 +2869,41 @@
</div>
{/if}

{#if saveToSpaceConfirm}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="wf-modal-backdrop"
onclick={() => (saveToSpaceConfirm = false)}
>
<div
class="wf-modal"
role="dialog"
aria-modal="true"
aria-labelledby="wf-save-space-title"
onclick={(e) => e.stopPropagation()}
>
<div class="wf-modal-title" id="wf-save-space-title">
Save to Space?
</div>
<div class="wf-modal-body">
This will commit <strong>workflow.json</strong> to
<strong>{spaceId}</strong>. The Space will
<strong>restart</strong> to pick up the change.
</div>
<div class="wf-modal-actions">
<button
class="wf-modal-btn"
onclick={() => (saveToSpaceConfirm = false)}>Cancel</button
>
<button class="wf-modal-btn wf-modal-btn-danger" onclick={saveToSpace}
>Save & restart</button
>
</div>
</div>
</div>
{/if}

{#if showApiPanel}
<WorkflowApiPanel
{server}
Expand Down
Loading