/** * 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(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 && (
🔑 Your Groq API key is active — stored in this session only. It will be automatically deleted when you close this tab.
)} {/* ── MODAL OVERLAY ──────────────────────────────────────────────────── */} {visible && (
🌌

Welcome to Multiverse AI Studio

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.

{/* Three choice cards */}
{/* ── LOCAL PANEL ───────────────────────────────────────────────── */} {mode === "local" && (

The heavy models run on your machine, not this Space. Follow the steps below, then open the app on your computer. See the README section{" "} “Run locally (full models)” for the complete guide.

    {LOCAL_STEPS.map((s, i) => (
  1. {s.title} {s.detail}
  2. ))}

Tip: models download once and cache locally, so subsequent runs work offline.

)} {/* ── GROQ PANEL ────────────────────────────────────────────────── */} {mode === "groq" && ( <>
1 Get a free key at{" "} console.groq.com {" "} (no credit card).
2 Paste it below (starts with gsk_). Your key is used only for this session.
{ setInput(e.target.value); setError(""); }} onKeyDown={(e) => e.key === "Enter" && handleSave()} autoFocus />
{error &&

{error}

}

🔒 Your key is stored only in your browser's sessionStorage. It is never sent to or stored on our servers — only included as a request header to the AI pipeline.

)} {/* ── DEMO PANEL ────────────────────────────────────────────────── */} {mode === "demo" && (

No setup required. The Studio will generate real images (via Pollinations) and real depth maps locally, with procedural audio/video — no API key needed.

)} {/* ── ACTIONS ───────────────────────────────────────────────────── */} {mode === "groq" && (
)} {mode === "demo" && (
)} {mode === "local" && (
)}
)} ); } // ── STYLES ───────────────────────────────────────────────────────────────────── const styles: Record = { 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", }, };