| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useState, useEffect } from "react"; |
| import { saveApiKey, clearApiKey, getStoredApiKey } from "../lib/keyStore"; |
|
|
| interface Props { |
| onReady: () => void; |
| } |
|
|
| 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); |
|
|
| |
| 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 = () => { |
| |
| 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 & 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'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 & 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> |
| )} |
| </> |
| ); |
| } |
|
|
| |
| 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", |
| }, |
| }; |
|
|