"use client"; import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { EyeIcon, EyeOffIcon, QuestionIcon, SnowflakeIcon, 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 ; } function GateScreen() { const reduceMotion = useReducedMotion(); 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 (
{/* help -------------------------------------------------------- */}
How do I get a key?
{/* card -------------------------------------------------------- */}

Connect your Ollama key

One step and you’re ready 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-[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. Freezmo’s 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-md overflow-y-auto rounded-t-[24px] border border-[var(--hairline)] bg-[var(--surface)] 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 Step({ index, children }: { index: number; children: React.ReactNode }) { return (
  • {index} {children}
  • ); }