"use client"; import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { useTheme } from "@/components/ThemeToggle"; import { CheckCircleIcon, ExternalLinkIcon, EyeIcon, EyeOffIcon, MoonIcon, QuestionIcon, SnowflakeIcon, SunIcon, XIcon, } from "@/components/icons"; import { Spinner } from "@/components/ui"; import { verifyOllamaKey } from "@/lib/client-api"; import { saveSettings, settingsStore } from "@/lib/settings"; import { useHealth } from "@/lib/use-health"; import { cn } from "@/lib/utils"; const KEYS_URL = "https://ollama.com/settings/keys"; type GateError = { tone: "invalid" | "offline"; message: string } | null; /** * Full-screen gate shown when this browser has no Ollama API key yet (and the * server runs in `user-key` mode). It replaces the app shell entirely: no * sidebar, no header. The key is validated with a real one-token probe before * it is saved, and it is still stored only in this browser. */ export function ApiKeyGate({ children }: { children: React.ReactNode }) { const storedKey = useSyncExternalStore( settingsStore.subscribe, () => settingsStore.getSnapshot().apiKey, () => "", ); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); const { health } = useHealth(); // Server-key and local-daemon installs never need a browser key. const needsKeyMode = health?.authMode !== "local" && health?.authMode !== "server"; // Until hydration finishes, render the page as usual so a key holder never // sees the gate flash. After that, a missing key replaces the app. if (!mounted || storedKey || !needsKeyMode) return <>{children}; return ; } /** Ghost icon button used for the two top-right controls. */ function CornerButton({ label, title, onClick, children, }: { label: string; title: string; onClick: () => void; children: React.ReactNode; }) { return ( ); } function GateScreen() { const reduceMotion = useReducedMotion(); const { theme, toggle } = useTheme(); const [draft, setDraft] = useState(""); const [reveal, setReveal] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); useEffect(() => { if (!helpOpen) return; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") setHelpOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [helpOpen]); const submit = async (event: React.FormEvent) => { event.preventDefault(); const key = draft.trim(); if (!key || busy) return; setBusy(true); setError(null); const check = await verifyOllamaKey(key); if (check.result === "valid") { // Saving flips the gate off and drops the user into the library. saveSettings({ apiKey: key }); return; } setError({ tone: check.result === "invalid" ? "invalid" : "offline", message: check.message }); setBusy(false); }; return (
{/* corner controls: theme + help ---------------------------------- */}
{theme === "dark" ? : }
setHelpOpen(true)} > How do I get a key?
{/* card ----------------------------------------------------------- */}

Connect your Ollama key

One step to freeze your first deck.

{ setDraft(event.target.value); if (error) setError(null); }} placeholder="Paste your Ollama API key" autoComplete="off" autoCapitalize="off" spellCheck={false} disabled={busy} aria-invalid={error?.tone === "invalid" || undefined} aria-describedby={error ? "gate-api-key-error" : undefined} className="h-12 w-full rounded-[13px] border border-[var(--field-border)] bg-[var(--field)] pl-3.5 pr-12 text-left text-[14px] text-[var(--text)] outline-none transition placeholder:text-[var(--text-subtle)] focus:border-[var(--text-subtle)] disabled:opacity-60" />
{error ? ( ) : null}

Your key stays in this browser. Our server never stores it.

setHelpOpen(false)} />
); } /* ------------------------------------------------------------------ */ /* “How do I get a key?” */ /* ------------------------------------------------------------------ */ function HelpDialog({ open, onClose }: { open: boolean; onClose: () => void }) { const reduceMotion = useReducedMotion(); return ( {open ? ( event.stopPropagation()} className="thin-scroll max-h-[90dvh] w-full max-w-[480px] overflow-y-auto rounded-t-[24px] border border-[var(--gate-card-border)] bg-[var(--gate-card)] p-6 pb-safe shadow-[var(--shadow-gate)] sm:rounded-[24px] sm:pb-6" >

How to get your Ollama API key

You only need the{" "} free cloud tier. No payment and no credit card required.

    Go to{" "} ollama.com and sign in, or create a free account. Open Settings → API keys at{" "} ollama.com/settings/keys. Click{" "} “Create new key”, give it any name, and copy it. Paste it into Freezmo and press Continue.

The free tier is enough for generating decks. You can swap or remove your key anytime in Settings.

) : null}
); } function ExternalLink({ href, children }: { href: string; children: React.ReactNode }) { return ( {children} ); } function Step({ index, children }: { index: number; children: React.ReactNode }) { return (
  • {index} {children}
  • ); }