File size: 17,580 Bytes
9c34cbf
 
 
29e37f2
9c34cbf
29e37f2
 
 
 
 
9c34cbf
 
29e37f2
 
 
 
 
 
 
9c34cbf
 
29e37f2
9c34cbf
 
 
29e37f2
9c34cbf
 
29e37f2
 
 
 
 
 
 
 
 
 
9c34cbf
 
29e37f2
9c34cbf
 
 
 
 
29e37f2
9c34cbf
 
 
 
29e37f2
9c34cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29e37f2
9c34cbf
 
 
 
 
 
 
 
 
29e37f2
9c34cbf
 
 
29e37f2
 
9c34cbf
 
29e37f2
9c34cbf
 
 
 
 
 
 
 
 
 
 
 
 
29e37f2
9c34cbf
 
 
 
 
 
 
29e37f2
 
9c34cbf
 
 
29e37f2
 
 
 
 
 
 
 
 
 
9c34cbf
29e37f2
 
 
 
 
 
 
9c34cbf
29e37f2
 
 
 
 
 
 
 
9c34cbf
 
29e37f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c34cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29e37f2
9c34cbf
29e37f2
 
9c34cbf
 
 
29e37f2
9c34cbf
 
 
 
 
 
 
 
 
 
29e37f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c34cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29e37f2
 
 
 
 
 
 
 
 
9c34cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29e37f2
9c34cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
/**
 * KeySetupModal.tsx
 *
 * WHAT: A first-visit chooser that asks the visitor how they want to use Multiverse AI Studio.
 *
 * WHY: Visitors arrive with different needs:
 *   1. Some want to run the FULL local pipeline on their own GPU (no cloud, no keys).
 *   2. Some want to use the free Groq cloud LLM (BYOK β€” their quota, not the server's).
 *   3. Some just want to see the demo right away (no key β†’ real Pollinations images + local
 *      depth + mock audio/video, served by the Space's default groq_cloud profile).
 *
 * HOW:
 *   - On mount it checks sessionStorage for an existing key. If found, skip straight to the app.
 *   - Otherwise it shows three choice cards. Selecting one reveals a contextual panel:
 *       β€’ "Run locally" β†’ informational setup steps (the heavy models run on THEIR machine).
 *       β€’ "Use Groq"    β†’ the existing key input (validated as gsk_…) or a demo skip.
 *       β€’ "Just demo"   β†’ proceeds immediately.
 *   - The chosen Groq key is stored in sessionStorage and sent by the API client as a header.
 *   - A persistent top-banner reminds the visitor that an active key is session-only.
 */

import React, { useState, useEffect } from "react";
import { saveApiKey, clearApiKey, getStoredApiKey } from "../lib/keyStore";

interface Props {
  onReady: () => void; // Called when the visitor is ready to use the Studio
}

type Mode = "local" | "groq" | "demo" | null;

const LOCAL_STEPS: { title: string; detail: string }[] = [
  { title: "Requirements", detail: "Python 3.10+, an NVIDIA GPU (β‰ˆ8GB+ VRAM). CPU-only is impractical for video/audio." },
  { title: "Install CUDA PyTorch", detail: "pip install torch --index-url https://download.pytorch.org/whl/cu121" },
  { title: "Install dependencies", detail: "pip install -r backend/requirements.txt" },
  { title: "Select local models", detail: "Set INFERENCE_PROFILE=local_gpu (depth runs on CPU; image/audio on GPU; video needs more VRAM)." },
  { title: "Run the server", detail: "python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000" },
];

export default function KeySetupModal({ onReady }: Props) {
  const [visible, setVisible] = useState(false);
  const [mode, setMode] = useState<Mode>(null);
  const [input, setInput] = useState("");
  const [error, setError] = useState("");
  const [bannerVisible, setBannerVisible] = useState(false);
  const [keyCleared, setKeyCleared] = useState(false);

  // On first mount: if a key is already stored, skip the modal entirely.
  useEffect(() => {
    const stored = getStoredApiKey();
    if (stored) {
      setBannerVisible(true);
      onReady();
    } else {
      setVisible(true);
    }
  }, []);

  const handleSave = () => {
    const trimmed = input.trim();
    if (!trimmed.startsWith("gsk_")) {
      setError("That doesn't look like a Groq key. It should start with gsk_…");
      return;
    }
    saveApiKey(trimmed);
    setVisible(false);
    setBannerVisible(true);
    setError("");
    onReady();
  };

  const handleSkip = () => {
    // Continue without a personal key β€” the Space's default profile serves the demo.
    setVisible(false);
    onReady();
  };

  const handleClear = () => {
    clearApiKey();
    setBannerVisible(false);
    setKeyCleared(true);
    setInput("");
    setMode(null);
    setVisible(true);
  };

  const choose = (m: Mode) => setMode(m);

  return (
    <>
      {/* ── TOP BANNER (only when a visitor key is active) ─────────────────── */}
      {bannerVisible && !keyCleared && (
        <div style={styles.banner}>
          <span style={styles.bannerIcon}>πŸ”‘</span>
          <span style={styles.bannerText}>
            Your Groq API key is active β€” stored in this session only.
            <strong> It will be automatically deleted when you close this tab.</strong>
          </span>
          <button style={styles.clearBtn} onClick={handleClear} title="Remove your key now">
            Clear Key &amp; Leave
          </button>
        </div>
      )}

      {/* ── MODAL OVERLAY ──────────────────────────────────────────────────── */}
      {visible && (
        <div style={styles.overlay}>
          <div style={styles.modal}>
            <div style={styles.header}>
              <div style={styles.logo}>🌌</div>
              <h2 style={styles.title}>Welcome to Multiverse AI Studio</h2>
              <p style={styles.subtitle}>
                How would you like to use the Studio? You can run the full pipeline on your own
                GPU, use the free Groq cloud LLM, or just explore the demo.
              </p>
            </div>

            {/* Three choice cards */}
            <div style={styles.cards}>
              <button
                style={{ ...styles.card, ...(mode === "local" ? styles.cardActive : {}) }}
                onClick={() => choose("local")}
              >
                <span style={styles.cardIcon}>πŸ–₯️</span>
                <span style={styles.cardTitle}>Run locally</span>
                <span style={styles.cardDesc}>Full models on your own GPU β€” no cloud, no keys.</span>
              </button>

              <button
                style={{ ...styles.card, ...(mode === "groq" ? styles.cardActive : {}) }}
                onClick={() => choose("groq")}
              >
                <span style={styles.cardIcon}>☁️</span>
                <span style={styles.cardTitle}>Use Groq</span>
                <span style={styles.cardDesc}>Free cloud LLM. Bring your own key (BYOK).</span>
              </button>

              <button
                style={{ ...styles.card, ...(mode === "demo" ? styles.cardActive : {}) }}
                onClick={() => choose("demo")}
              >
                <span style={styles.cardIcon}>▢️</span>
                <span style={styles.cardTitle}>Just demo</span>
                <span style={styles.cardDesc}>No key needed β€” real images + mock audio/video.</span>
              </button>
            </div>

            {/* ── LOCAL PANEL ───────────────────────────────────────────────── */}
            {mode === "local" && (
              <div style={styles.localPanel}>
                <p style={styles.localIntro}>
                  The heavy models run on <strong>your</strong> machine, not this Space. Follow the
                  steps below, then open the app on your computer. See the README section{" "}
                  <em>β€œRun locally (full models)”</em> for the complete guide.
                </p>
                <ol style={styles.localList}>
                  {LOCAL_STEPS.map((s, i) => (
                    <li key={i} style={styles.localItem}>
                      <span style={styles.localItemTitle}>{s.title}</span>
                      <span style={styles.localItemDetail}>{s.detail}</span>
                    </li>
                  ))}
                </ol>
                <p style={styles.localNote}>
                  Tip: models download once and cache locally, so subsequent runs work offline.
                </p>
              </div>
            )}

            {/* ── GROQ PANEL ────────────────────────────────────────────────── */}
            {mode === "groq" && (
              <>
                <div style={styles.steps}>
                  <div style={styles.step}>
                    <span style={styles.stepNum}>1</span>
                    <span>
                      Get a free key at{" "}
                      <a href="https://console.groq.com" target="_blank" rel="noopener noreferrer" style={styles.link}>
                        console.groq.com
                      </a>{" "}
                      (no credit card).
                    </span>
                  </div>
                  <div style={styles.step}>
                    <span style={styles.stepNum}>2</span>
                    <span>
                      Paste it below (starts with <code style={styles.code}>gsk_</code>). Your key is
                      used only for this session.
                    </span>
                  </div>
                </div>

                <div style={styles.inputRow}>
                  <input
                    style={{ ...styles.input, ...(error ? styles.inputError : {}) }}
                    type="password"
                    placeholder="gsk_xxxxxxxxxxxxxxxxxxxxxxxx"
                    value={input}
                    onChange={(e) => {
                      setInput(e.target.value);
                      setError("");
                    }}
                    onKeyDown={(e) => e.key === "Enter" && handleSave()}
                    autoFocus
                  />
                </div>
                {error && <p style={styles.errorText}>{error}</p>}

                <p style={styles.privacy}>
                  πŸ”’ Your key is stored only in your browser&apos;s <code style={styles.code}>sessionStorage</code>.
                  It is <strong>never sent to or stored on our servers</strong> β€” only included as a
                  request header to the AI pipeline.
                </p>
              </>
            )}

            {/* ── DEMO PANEL ────────────────────────────────────────────────── */}
            {mode === "demo" && (
              <div style={styles.demoPanel}>
                <p style={styles.demoText}>
                  No setup required. The Studio will generate <strong>real images</strong> (via
                  Pollinations) and <strong>real depth maps</strong> locally, with procedural
                  audio/video β€” no API key needed.
                </p>
              </div>
            )}

            {/* ── ACTIONS ───────────────────────────────────────────────────── */}
            {mode === "groq" && (
              <div style={styles.actions}>
                <button style={styles.skipBtn} onClick={handleSkip}>
                  Continue without key (demo)
                </button>
                <button style={styles.saveBtn} onClick={handleSave}>
                  Save Key &amp; Start β†’
                </button>
              </div>
            )}
            {mode === "demo" && (
              <div style={styles.actions}>
                <button style={styles.saveBtn} onClick={handleSkip}>
                  Start demo β†’
                </button>
              </div>
            )}
            {mode === "local" && (
              <div style={styles.actions}>
                <button style={styles.skipBtn} onClick={() => choose(null)}>
                  Back
                </button>
                <button style={styles.skipBtn} onClick={handleSkip}>
                  Use the demo instead β†’
                </button>
              </div>
            )}
          </div>
        </div>
      )}
    </>
  );
}

// ── STYLES ─────────────────────────────────────────────────────────────────────
const styles: Record<string, React.CSSProperties> = {
  overlay: {
    position: "fixed",
    inset: 0,
    background: "rgba(0,0,0,0.75)",
    backdropFilter: "blur(8px)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    zIndex: 9999,
    padding: "1rem",
  },
  modal: {
    background: "linear-gradient(135deg, #0f0c29, #1a1a3e, #0f0c29)",
    border: "1px solid rgba(139,92,246,0.4)",
    borderRadius: "1.5rem",
    padding: "2.5rem",
    maxWidth: "640px",
    width: "100%",
    maxHeight: "90vh",
    overflowY: "auto",
    boxShadow: "0 25px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(139,92,246,0.15)",
    color: "#e2e8f0",
  },
  header: { textAlign: "center", marginBottom: "1.5rem" },
  logo: { fontSize: "3rem", marginBottom: "0.75rem" },
  title: {
    fontSize: "1.5rem",
    fontWeight: 700,
    background: "linear-gradient(135deg, #a78bfa, #60a5fa)",
    WebkitBackgroundClip: "text",
    WebkitTextFillColor: "transparent",
    margin: "0 0 0.5rem",
  },
  subtitle: { color: "#94a3b8", fontSize: "0.9rem", margin: 0, lineHeight: 1.6 },

  cards: {
    display: "grid",
    gridTemplateColumns: "repeat(3, 1fr)",
    gap: "0.75rem",
    marginBottom: "1.5rem",
  },
  card: {
    display: "flex",
    flexDirection: "column",
    gap: "0.4rem",
    textAlign: "left",
    background: "rgba(255,255,255,0.04)",
    border: "1px solid rgba(255,255,255,0.08)",
    borderRadius: "1rem",
    padding: "1rem",
    cursor: "pointer",
    color: "#e2e8f0",
    transition: "all 0.2s",
  },
  cardActive: {
    borderColor: "#8b5cf6",
    background: "rgba(139,92,246,0.12)",
    boxShadow: "0 0 0 1px rgba(139,92,246,0.4)",
  },
  cardIcon: { fontSize: "1.5rem" },
  cardTitle: { fontWeight: 700, fontSize: "0.95rem" },
  cardDesc: { fontSize: "0.78rem", color: "#94a3b8", lineHeight: 1.4 },

  localPanel: {
    background: "rgba(255,255,255,0.04)",
    border: "1px solid rgba(255,255,255,0.08)",
    borderRadius: "1rem",
    padding: "1.25rem",
    marginBottom: "1rem",
  },
  localIntro: { fontSize: "0.85rem", color: "#cbd5e1", lineHeight: 1.6, margin: "0 0 1rem" },
  localList: { margin: 0, padding: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: "0.75rem" },
  localItem: { display: "flex", flexDirection: "column", gap: "0.2rem" },
  localItemTitle: { fontWeight: 600, fontSize: "0.85rem", color: "#c4b5fd" },
  localItemDetail: {
    fontSize: "0.8rem",
    color: "#94a3b8",
    fontFamily: "monospace",
    background: "rgba(0,0,0,0.3)",
    borderRadius: "0.5rem",
    padding: "0.4rem 0.6rem",
    lineHeight: 1.4,
    wordBreak: "break-word",
  },
  localNote: { fontSize: "0.78rem", color: "#64748b", marginTop: "1rem", marginBottom: 0 },

  steps: { display: "flex", flexDirection: "column", gap: "0.75rem", marginBottom: "1.25rem" },
  step: {
    display: "flex",
    alignItems: "flex-start",
    gap: "0.75rem",
    background: "rgba(255,255,255,0.04)",
    borderRadius: "0.75rem",
    padding: "0.75rem 1rem",
    fontSize: "0.875rem",
    lineHeight: 1.5,
    color: "#cbd5e1",
  },
  stepNum: {
    background: "linear-gradient(135deg, #7c3aed, #3b82f6)",
    color: "white",
    borderRadius: "50%",
    width: "1.5rem",
    height: "1.5rem",
    minWidth: "1.5rem",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontWeight: 700,
    fontSize: "0.75rem",
  },
  inputRow: { marginBottom: "0.5rem" },
  input: {
    width: "100%",
    padding: "0.875rem 1rem",
    background: "rgba(255,255,255,0.06)",
    border: "1px solid rgba(139,92,246,0.3)",
    borderRadius: "0.75rem",
    color: "#e2e8f0",
    fontSize: "0.9rem",
    outline: "none",
    boxSizing: "border-box",
    transition: "border-color 0.2s",
    fontFamily: "monospace",
  },
  inputError: { borderColor: "#f87171" },
  errorText: { color: "#f87171", fontSize: "0.8rem", margin: "0 0 0.75rem" },
  privacy: {
    background: "rgba(59,130,246,0.1)",
    border: "1px solid rgba(59,130,246,0.2)",
    borderRadius: "0.75rem",
    padding: "0.75rem 1rem",
    fontSize: "0.8rem",
    color: "#93c5fd",
    lineHeight: 1.6,
    margin: "0.75rem 0 1.5rem",
  },
  demoPanel: {
    background: "rgba(16,185,129,0.08)",
    border: "1px solid rgba(16,185,129,0.25)",
    borderRadius: "1rem",
    padding: "1.25rem",
    marginBottom: "1rem",
  },
  demoText: { fontSize: "0.85rem", color: "#a7f3d0", lineHeight: 1.6, margin: 0 },

  actions: { display: "flex", gap: "0.75rem", justifyContent: "flex-end" },
  skipBtn: {
    padding: "0.7rem 1.25rem",
    background: "transparent",
    border: "1px solid rgba(255,255,255,0.15)",
    borderRadius: "0.75rem",
    color: "#94a3b8",
    fontSize: "0.85rem",
    cursor: "pointer",
    transition: "all 0.2s",
  },
  saveBtn: {
    padding: "0.7rem 1.5rem",
    background: "linear-gradient(135deg, #7c3aed, #3b82f6)",
    border: "none",
    borderRadius: "0.75rem",
    color: "white",
    fontWeight: 600,
    fontSize: "0.9rem",
    cursor: "pointer",
    transition: "all 0.2s",
  },
  link: { color: "#818cf8", textDecoration: "underline" },
  code: {
    background: "rgba(255,255,255,0.1)",
    borderRadius: "0.25rem",
    padding: "0.1rem 0.3rem",
    fontFamily: "monospace",
    fontSize: "0.85em",
  },

  banner: {
    position: "fixed",
    top: 0,
    left: 0,
    right: 0,
    zIndex: 9998,
    background: "linear-gradient(90deg, rgba(124,58,237,0.9), rgba(59,130,246,0.9))",
    backdropFilter: "blur(8px)",
    display: "flex",
    alignItems: "center",
    gap: "0.75rem",
    padding: "0.6rem 1.5rem",
    fontSize: "0.8rem",
    color: "white",
    boxShadow: "0 2px 12px rgba(0,0,0,0.3)",
  },
  bannerIcon: { fontSize: "1rem" },
  bannerText: { flex: 1, lineHeight: 1.4 },
  clearBtn: {
    background: "rgba(248,113,113,0.2)",
    border: "1px solid rgba(248,113,113,0.5)",
    borderRadius: "0.5rem",
    color: "#fca5a5",
    padding: "0.3rem 0.75rem",
    cursor: "pointer",
    fontSize: "0.8rem",
    fontWeight: 600,
    whiteSpace: "nowrap",
    transition: "all 0.2s",
  },
};