Revision: a local-first FSRS-5 desktop app with drag and air gestures

Revision is a local-first desktop app for principal-level interview prep across DSA, system design concepts and use cases, AI concepts and use cases, and behavioral topics, with FSRS-5 scheduling, a keyboard-first review loop, and grading by drag or air gesture.

Source: github.com/xpressabhi/revision · Releases: v0.5.0 (macOS .dmg + Windows .exe/.msi, each under 25 MB) · Stack: Tauri 2 + React 19 + TypeScript + Vite 7 + KaTeX, SQLite revision.db with a localStorage fallback for the browser preview. No cloud, no account.


Update: v0.5.0 visual overhaul

This post was written against v0.4.0 and the screenshots above are now from v0.5.0 (all five refreshed). Two things changed, neither touches the interaction model:


What it is

One SQLite file (revision.db via tauri-plugin-sql, or localStorage keys like revision_cards in the browser) — everything else is derived. A single Revision deck with tag trees (dsa, sd-concepts, sd-use-cases, ai-concepts, ai-use-cases, behavioral, bookmark …) replaces the usual multi-deck model. The shell is a 3-pane glass layout — collapsible sidebar (decks, smart filters, tag graph), central canvas, inspector — with four themes, command bar (⌘K), quick capture (⌘⇧K), cloze deletions and KaTeX, a 53-week heatmap and retention forecasts.

The review queue is opinionated: learning (10 m step) → due → new (new cards capped at 20 per session), scoped by tag group or ⌘K smart filters. Hidden card → flip (Space / Enter / click / flick); shown card → grade 1–4 (or a gesture) with live FSRS intervals on the grading bar.

Dashboard — due today, streak heatmap, smart study queues
Sidebar (tag tree)        Canvas (Dashboard / Review / Browse / Analytics)      Inspector (FSRS + hints)
─────────────             ──────────────────────────────────────────            ───────────────
Due · New · Learning      Review: flip-wrap → grade bar → session stats         Retrievability, interval
                          Browse: search + filters + inline edit                predictions per grade
                          Analytics: streak heatmap · forecast                  cloze/hint controls

Decision 1: FSRS-5 you can see

Anki's SM-2 works until it doesn't — stability collapses silently, difficulty never moves, and intervals feel arbitrary. Revision uses an FSRS-5-inspired scheduler (src/lib/fsrs.ts) — the same family of models behind modern Anki — offline, deterministic, per-card.

The product point is transparency. You never grade blind — the bar tells you what each button costs before you press it.

Analytics — retention forecast, heatmap, grade distribution

Decision 2: drag as the grading language

Keyboard-first was non-negotiable (Space reveal, 1–4 grade, G cloze, H hints, ⇧G undo, ⌃→ skip, ⌘K palette — src/App.tsx:222), but grading 1–4 on a laptop all day is finger gymnastics. The grading gesture makes the card itself the control.

useDragGesture (src/lib/gestures.ts:31) is a pointer-drag layer on .flip-wrap in ReviewView.tsx:58:

ConstantValueWhy
Deadzone6 pxignore micro-jitter
Tap<10 px & <260 msflip on click/tap
Flip threshold64 pxshort flick before reveal
Grade threshold118 pxdeliberate commit once shown
Max dist260 pxclamp so the card never leaves the stage
Fly-out230 ms (170 ms for flip)tilt → fly, then callback

The mapping is fixed and always visible:

DirectionGradeKeyMeaning
← leftAgain (1)reset10 m step, stability collapses
→ rightGood (3)normalFSRS interval
↑ upEasy (4)bonus1.3× interval
↓ downHard (2)penaltyshorter growth

Hover highlights on the map, plus g-swipe arrows on each grade zone (ReviewView.tsx:218), keep the mapping in peripheral vision — you learn it in about three cards.

Review — front of card with the gesture map (← Again · → Good · ↑ Easy · ↓ Hard) Review — answer side with the FSRS prediction grading bar

Decision 3: air gestures, fully on-device

Drag proved the interaction; the camera proves it can leave the pointer behind. Air gestures are opt-in (Settings → Gestures, recall_air_gestures in localStorage — src/App.tsx:194) and run entirely locally — no upload, no API key.

macOS camera permission is wired at the bundle level (src-tauri/Info.plist + Entitlements.plist, referenced in tauri.conf.json: bundle.macOS.entitlements — src/AGENTS.md:37). A noisy classifier is worse than no classifier — the cooldowns and reset-distance logic eliminated the false-grade burst that plagued early builds.


Decision 4: activity-aware sessions (v0.4.0)

Spaced repetition only works if every recorded grade is real recall. Leaving a card answer exposed while you answer Slack breaks that.

src/lib/session.ts and src/App.tsx:478 add a small state machine around the review loop:

reviewing  ──(idle ≥ staleMin)──► stale: hide answer, pause pomo, show banner
   ▲                                    │
   └── any key/click/swipe ─── Resume ──┘
   └────── Restart queue ── re-derive queue from current DB
   └────── End ── back to dashboard
   └────── auto-end after 15 m idle (toggleable)

Fifteen minutes auto-ends the session (AUTO_END_DEFAULT_MIN, src/lib/session.ts:4) because the queue should be re-derived anyway. Burying and suspending still work per card (B/S — src/App.tsx:455), independent of staleness.


Decision 5: shipping small

The app could have been Electron + cloud sync + a design-system landing page. The constraint was the opposite: one file, no account, works on a fresh Mac with no network.

Browse — search, filters, inline edit

The boring truth: the commit discipline (feat:/fix:) and the "no comments unless asked" repo rule matter more for ship cadence than any framework choice.


What I kept out


Honesty as a feature (borrowed from SpendIQ)

The system prompt problem shows up here too: an agent that grades itself should not invent its own difficulty curve. Every interval is derived from predictIntervals before you press, retrievability is shown at due time, and analytics split new/learning/review so you can see when you're flooding the future. The user guide documents step-away handling, the development doc documents the three-manifest release rule, and the changelog is the source of truth for what shipped when.


Stack & links

Python wasn't needed here — just web and Rust glue:

Stack: Tauri 2 · React 19 · TypeScript 5.8 (strict) · Vite 7 · KaTeX · MediaPipe Tasks Vision · tauri-plugin-sql · tauri-plugin-autostart · Swift + XcodeGen (WidgetKit)

Browse the code at github.com/xpressabhi/revision, grab the latest release, or run the browser preview with npm run dev at http://localhost:1420 — it uses localStorage so you can try everything without installing the app. If you're hiring for frontend architecture, app shell design, or local-first tooling, my email is in the profile.

← All posts ← Home