File size: 5,158 Bytes
0ac416b
 
10a1123
0ac416b
aab2748
 
86da9e3
aab2748
0ac416b
 
 
 
 
aab2748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ac416b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86da9e3
 
 
0ac416b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d3ee50
0ac416b
 
 
 
 
 
 
 
 
10a1123
0ac416b
2d3ee50
0ac416b
10a1123
aab2748
0ac416b
 
 
 
 
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { Button } from "@cloudflare/kumo";
import { ArrowSquareOutIcon, ArrowClockwiseIcon, SpinnerGapIcon } from "@phosphor-icons/react";
import flowMark from "../assets/flow-mark.svg";
import hfLogo from "../assets/hf-logo.svg";
import { useEffect, useState } from "react";
import { pendingShare } from "../lib/deepLink";
import { isEmbedded, newTabUrl } from "../lib/hfAuth";
import { fetchShare } from "../lib/share";
import { useAuthStore } from "../store/useAuthStore";
import { useSyncStore } from "../store/useSyncStore";

const LOGO = <img src={hfLogo} alt="" aria-hidden className="h-[18px] w-[18px]" draggable={false} />;

/** Someone opened a share link while signed out: say what it is before asking them to sign in. */
function SharePreview() {
  const [info, setInfo] = useState<{ owner: string; title: string } | null>(null);
  useEffect(() => {
    const id = pendingShare();
    if (!id) return;
    let live = true;
    void fetchShare(id)
      .then((s) => live && setInfo({ owner: s.owner, title: s.title }))
      .catch(() => undefined);
    return () => {
      live = false;
    };
  }, []);
  if (!info) return null;
  return (
    <p className="gate__msg gate__share">
      <strong>@{info.owner}</strong> shared “{info.title}” with you. Sign in to open your own copy.
    </p>
  );
}

/**
 * Everything before the editor: signed out, signing in (embedded: in a new tab), and loading the
 * account's storage. Flow keeps workflows only in the user's Hugging Face bucket, so there is no
 * signed-out editor.
 */
export function SignInGate() {
  const user = useAuthStore((s) => s.user);
  const busy = useAuthStore((s) => s.busy);
  const configured = useAuthStore((s) => s.configured);
  const pending = useAuthStore((s) => s.pending);
  const error = useAuthStore((s) => s.error);
  const bootError = useSyncStore((s) => s.bootError);
  const auth = useAuthStore.getState();

  let body: React.ReactNode;
  if (user && bootError) {
    body = (
      <>
        <p className="gate__msg" role="alert">
          {bootError}
        </p>
        <div className="gate__actions">
          <Button variant="primary" icon={ArrowClockwiseIcon} onClick={() => void useSyncStore.getState().boot()}>
            Retry
          </Button>
          <Button variant="secondary" onClick={() => auth.login()}>
            Sign in again
          </Button>
        </div>
        <button type="button" className="gate__link" onClick={() => auth.logout()}>
          Sign out
        </button>
      </>
    );
  } else if (user || busy) {
    body = (
      <p className="gate__msg gate__loading" aria-live="polite">
        <SpinnerGapIcon size={16} className="animate-spin" />
        {user ? "Loading your workflows…" : "Signing in…"}
      </p>
    );
  } else if (!configured) {
    body = <p className="gate__msg">Sign-in isn't configured for this deployment (no OAuth client id).</p>;
  } else if (pending === "tab") {
    body = (
      <>
        <p className="gate__msg">
          <strong>Signed in in the new tab?</strong> Continue there: it's the full app, and your work saves to your account either way.
        </p>
        <div className="gate__actions">
          <Button variant="primary" icon={ArrowClockwiseIcon} onClick={() => void auth.finishLogin()}>
            Use it here too
          </Button>
          <Button variant="secondary" icon={ArrowSquareOutIcon} onClick={() => auth.openApp()}>
            Open Flow in a new tab
          </Button>
        </div>
        {error && (
          <p className="gate__err" role="alert">
            {error}
          </p>
        )}
        <p className="gate__fine">
          Didn't see a tab?{" "}
          <a className="gate__link" href={newTabUrl()} target="_blank" rel="noopener">
            Open the sign-in tab
          </a>{" "}
          ·{" "}
          <button type="button" className="gate__link" onClick={() => auth.cancelLogin()}>
            Cancel
          </button>
        </p>
      </>
    );
  } else {
    body = (
      <>
        <Button variant="primary" className="gate__cta" icon={LOGO} onClick={() => auth.login()}>
          Sign in with Hugging Face
        </Button>
        {error && (
          <p className="gate__err" role="alert">
            {error}
          </p>
        )}
        <p className="gate__fine">
          {isEmbedded ? "Opens Flow in a new tab to sign in. " : ""}Flow is still in beta and hasn't launched yet. It's an independent tool, not made by Hugging Face. Runs use your own quota; workflows save to a private bucket on your account.
        </p>
      </>
    );
  }

  return (
    <div className="gate">
      <div className="gate__card">
        <div className="gate__brand">
          <img src={flowMark} alt="" aria-hidden className="h-7 w-7" draggable={false} />
          <h1 className="gate__title">Flow</h1>
          <span className="beta-tag">Beta</span>
        </div>
        <p className="gate__lede">A visual editor for chaining Hugging Face Spaces into workflows and running them from one canvas.</p>
        {!user && <SharePreview />}
        {body}
      </div>
    </div>
  );
}