flow / src /components /NodeError.tsx
mrfakename's picture
Require sign-in, bucket-only storage, sign in via new tab when embedded
0ac416b verified
Raw History Blame Contribute Delete
3.71 kB
import { useCanvasStore } from "../store/useCanvasStore";
import { askCopilot } from "../store/useCopilotStore";
import { useAuthStore } from "../store/useAuthStore";
import { hubUrl, isUsable } from "../lib/hub";
import type { CanvasNodeData, NodeDef, SpaceEntry } from "../lib/types";
interface Action {
label: string;
run?: () => void;
href?: string;
}
const BILLING = "https://huggingface.co/settings/billing";
/** The one tap that could fix a Space's problem: wake it, sign in, grant access, or go see it on the Hub. */
export function spaceAction(spaceId: string | null, entry: SpaceEntry | undefined, message: string): Action | null {
const s = useCanvasStore.getState();
const auth = useAuthStore.getState();
const msg = message.toLowerCase();
const stage = entry?.summary.stage;
if (/private|not found|sign in|sign-in|grant access/.test(msg)) {
if (!auth.token()) return { label: "Sign in", run: () => void auth.login() };
if (!auth.hasScope("read-repos")) return { label: "Grant access", run: () => void auth.reauth() };
return spaceId ? { label: "Open on Hub", href: hubUrl(spaceId) } : null;
}
if (/401|403|unauthori[sz]ed|forbidden|gated/.test(msg)) {
if (!auth.token()) return { label: "Sign in", run: () => void auth.login() };
return { label: "Usage & billing", href: BILLING };
}
if (/quota|zerogpu|gpu.*(limit|exceed|exhaust)|(limit|exceed|exhaust).*gpu|too many requests|429/.test(msg)) return { label: "See usage", href: BILLING };
if (spaceId && (stage === "SLEEPING" || stage === "APP_STARTING" || stage === "BUILDING" || /asleep|sleep|starting up|not running|wak(e|ing)|did not wake/.test(msg))) {
return { label: stage === "SLEEPING" || !stage ? "Wake it" : "Wait for it", run: () => void s.wakeSpace(spaceId) };
}
if (spaceId && stage && !isUsable(stage) && stage !== "UNKNOWN") return { label: "Open on Hub", href: hubUrl(spaceId) };
if (spaceId && /crashed|paused|stopped|broken|owner/.test(msg)) return { label: "Open on Hub", href: hubUrl(spaceId) };
if (spaceId && (/not loaded|could not reach|network|cors|disconnected|closed the stream|result stream|timed out/.test(msg) || entry?.status === "error")) {
return { label: "Retry", run: () => void s.refreshSpace(spaceId) };
}
return null;
}
/** One tap that could fix the error, derived from the message and the Space's state. */
export function errorAction(data: CanvasNodeData, def: NodeDef): Action | null {
const spaceId = def.space?.id ?? (def.type.startsWith("space:") ? def.type.slice(6).split("/").slice(0, 2).join("/") : null);
const entry = spaceId ? useCanvasStore.getState().spaces[spaceId] : undefined;
return spaceAction(spaceId, entry, data.error ?? "");
}
/** The red error box under a node: the message, the best next action, and "Ask copilot why". */
export function NodeError({ data, def, className = "" }: { data: CanvasNodeData; def: NodeDef; className?: string }) {
if (data.status !== "error" || !data.error) return null;
const action = errorAction(data, def);
return (
<div className={`node-error ${className}`}>
{data.error}
<div className="node-error__actions">
{action &&
(action.href ? (
<a className="node-error__act" href={action.href} target="_blank" rel="noreferrer">
{action.label}
</a>
) : (
<button type="button" className="node-error__act" onClick={action.run}>
{action.label}
</button>
))}
<button type="button" className="node-error__ask" onClick={() => askCopilot(data.title ?? def.title)}>
Ask copilot why
</button>
</div>
</div>
);
}