File size: 3,707 Bytes
fb57ab3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ac416b
fb57ab3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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>
  );
}