Turn your notes into a study feed
Snap or drop photos of your notes. Freezmo reads them with your own Ollama vision model
and builds a swipeable deck of mini study games.
{error ? (
{error.kind === "needs_key"
? "API key required"
: error.kind === "rate_limited"
? "Slow down a moment"
: error.kind === "auth"
? "Ollama rejected your key"
: "Generation failed"}
{error.message}
{error.kind === "needs_key" || error.kind === "auth" ? (
Open settings
) : null}
void generate()}>
Retry
) : null}
{/* ---------------------------------------------------------- */}
{
event.preventDefault();
setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={(event) => {
event.preventDefault();
setDragging(false);
void addFiles(imageFilesFromDataTransfer(event.dataTransfer));
}}
className={cn(
"card relative overflow-hidden p-4 transition-all sm:p-5",
dragging && "border-frost-400 bg-frost-50 ring-4 ring-frost-200 dark:bg-frost-500/10 dark:ring-frost-500/30",
)}
>
1. Add your notes
Drag & drop, paste a screenshot (Ctrl/⌘+V), or use the camera. Photos, PDF, Word,
PowerPoint, OpenDocument and text files. Up to {MAX_IMAGES} photos and {MAX_DOCUMENTS}{" "}
documents.
fileInputRef.current?.click()}>
Choose files
cameraInputRef.current?.click()}>
Camera
{
void addFiles(Array.from(event.target.files ?? []));
event.target.value = "";
}}
/>
{
void addFiles(Array.from(event.target.files ?? []));
event.target.value = "";
}}
/>
{images.length === 0 ? (
fileInputRef.current?.click()}
className="mt-4 flex w-full flex-col items-center gap-2 rounded-2xl border-2 border-dashed border-white/70 bg-white/40 px-4 py-10 text-center backdrop-blur-xl transition hover:border-accent-300 hover:bg-white/65 dark:border-white/15 dark:bg-white/[0.05] dark:hover:bg-white/[0.09]"
>
Drop your notes here
Photos, PDF, DOCX, PPTX, ODT/ODP, TXT, MD · images resized to 1600px before sending
) : (
{images.map((image) => (
{/* eslint-disable-next-line @next/next/no-img-element */}
removeImage(image.id)}
aria-label={`Remove ${image.name}`}
className="absolute right-1 top-1 grid size-7 place-items-center rounded-full bg-frost-950/70 text-xs font-bold text-white transition hover:bg-rose-600"
>
✕
{formatBytes(image.bytes)}
))}
{images.length < MAX_IMAGES ? (
fileInputRef.current?.click()}
className="grid aspect-square w-full place-items-center rounded-2xl border-2 border-dashed border-white/70 text-2xl text-accent-500 backdrop-blur-xl transition hover:border-accent-300 hover:bg-white/60 dark:border-white/15 dark:text-accent-200"
aria-label="Add more photos"
>
+
) : null}
)}
{documents.length > 0 ? (
{documents.map((entry) => (
{KIND_LABELS[entry.kind]}
{entry.name}
{entry.status === "extracting" ? (
{entry.progress ?? "Reading…"}
) : null}
{entry.status === "error" ? (
{entry.error}
) : null}
{entry.status === "ready" && entry.extracted ? (
<>
{describeExtraction(entry.extracted)}
{entry.extracted.words === 0 && entry.extracted.images.length === 0 ? (
No readable text — is this a scan without a text layer?
) : null}
{entry.extracted.warnings.map((warning) => (
ⓘ {warning}
))}
>
) : null}
removeDocument(entry.id)}
aria-label={`Remove ${entry.name}`}
className="grid size-7 shrink-0 place-items-center rounded-full bg-frost-950/10 text-xs font-bold text-frost-900 transition hover:bg-rose-500 hover:text-white dark:bg-white/10 dark:text-frost-50"
>
✕
))}
) : null}
{images.length > 0 || documents.length > 0 ? (
{images.length > 0
? `${images.length} photo${images.length === 1 ? "" : "s"} · ${formatBytes(totalBytes)} after compression`
: ""}
{images.length > 0 && documents.length > 0 ? " · " : ""}
{documents.length > 0
? `${documents.length} document${documents.length === 1 ? "" : "s"}${
documentImages.length ? ` (${documentImages.length} page image${documentImages.length === 1 ? "" : "s"})` : ""
}`
: ""}
{imagesSkipped > 0
? ` · ${imagesSkipped} image${imagesSkipped === 1 ? "" : "s"} will be skipped (max ${MAX_IMAGES_TO_SEND} per deck)`
: ""}
) : null}
{phase === "preparing" ? (
Compressing photos…
) : null}
{fileError ? (
{fileError}
) : null}
{/* ---------------------------------------------------------- */}
2. Optional extras
Paste extra notes or give the AI a hint about what to focus on.
Pasted notes or text
Topic hint (optional)
setFocus(event.target.value)}
placeholder="e.g. focus on the formulas, or on dates"
className="w-full rounded-2xl border border-white/60 bg-white/55 px-4 py-3 text-sm text-frost-950 outline-none backdrop-blur-xl transition placeholder:text-frost-400 focus:border-accent-400 dark:border-white/12 dark:bg-white/[0.07] dark:text-frost-50"
/>
{/* ---------------------------------------------------------- */}
3. Difficulty & size
Freeze into folder
{[{ id: UNORGANIZED_ID, name: "Unorganized" }, ...sortedFolders].map((folder) => {
const active = folderId === folder.id;
return (
setFolderId(folder.id)}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-[13px] font-medium transition",
active
? "border-accent-500 bg-accent-50 text-accent-800 dark:bg-accent-500/20 dark:text-accent-100"
: "border-[var(--hairline)] text-frost-600 hover:text-frost-900 dark:text-frost-300 dark:hover:text-white",
)}
>
{folder.name}
);
})}
{creatingFolder ? (
setFolderDraft(event.target.value)}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Escape") {
setCreatingFolder(false);
setFolderDraft("");
}
if (event.key === "Enter") {
const created = createFolder(folderDraft);
if ("folder" in created) {
setFolderId(created.folder.id);
setFolderDraft("");
setCreatingFolder(false);
} else {
setFileError(created.error);
}
}
}}
aria-label="New folder name"
placeholder="e.g. History"
maxLength={60}
className="w-32 rounded-xl border border-[var(--hairline)] bg-[var(--surface-strong)] px-3 py-1.5 text-sm outline-none"
/>
{
const created = createFolder(folderDraft);
if ("folder" in created) {
setFolderId(created.folder.id);
setFolderDraft("");
setCreatingFolder(false);
} else {
setFileError(created.error);
}
}}
>
Add
) : (
setCreatingFolder(true)}
className="inline-flex items-center gap-1.5 rounded-xl border border-dashed border-[var(--hairline)] px-3 py-1.5 text-sm font-semibold text-frost-500 transition-colors hover:text-accent-700 dark:text-frost-400 dark:hover:text-accent-200"
>
New folder
)}
{DIFFICULTIES.map((option) => {
const selected = difficulty === option.value;
return (
setDifficulty(option.value)}
className={cn(
"flex flex-col items-center gap-1 rounded-2xl border px-2 py-3 text-center backdrop-blur-xl transition-colors",
selected
? "border-accent-400 bg-accent-50/80 shadow-lift dark:bg-accent-500/20"
: "border-white/60 bg-white/45 backdrop-blur-xl hover:bg-white/70 dark:border-white/12 dark:bg-white/[0.05]",
)}
>
{option.icon}
{option.label}
{option.blurb}
);
})}
Activities
{COUNT_OPTIONS.map((option) => (
setCount(option)}
className={cn(
"min-w-12 rounded-xl border-2 px-3 py-1.5 text-sm font-bold transition",
count === option
? "border-transparent bg-[var(--text)] text-white dark:bg-white dark:text-[#0d0d0d]"
: "border-[var(--hairline)] text-[var(--text-muted)] hover:bg-[var(--hover)]",
)}
>
{option}
))}
{documentsBusy
? "Reading your documents…"
: hasContent
? "Ready when you are — generation can take a minute."
: "Add a photo or a document (or some text) to start."}
void generate()} disabled={!canGenerate}>
Freeze my deck