Recall turns a photo of notes or a topic into an editable flashcard deck. During review, the learner reveals an answer and chooses Again, Hard, Good, or Easy.
Again changes the card's stored schedule and sends it to the back of the current queue. The button says “10m,” but a card can reappear sooner during the session. Here's why.
The stored schedule is a pure calculation
I use an SM-2-style scheduler written in TypeScript. It takes the card's interval, ease, repetition count, lapse count, a rating and the current time. It returns the next state without reading the database or calling a model.
For Again, the rule is short:
return {
interval_days: 0,
ease: Math.max(1.3, ease - 0.2),
reps: 0,
lapses: lapses + 1,
due_at: new Date(now + 10 * 60 * 1000).toISOString(),
};A failed recall resets repetitions, records another lapse and lowers ease without taking it below 1.3. The stored due date is ten minutes later.
For a new card, Good starts with a one-day interval and Easy starts with four days. Later intervals depend on the card's history. The function accepts time as an argument, so its output can be checked against an exact date.
The rating labels use this same scheduling calculation to preview day-based intervals. Again has a fixed “10m” label. Keeping the calculation outside the screen avoids maintaining separate scheduling rules in the button and the save path.
The active queue doesn't wait ten minutes
After a rating, the screen removes the current card from the front of the queue. With Again, it appends that card to the back, carrying the updated state:
setQueue((queue) => {
const remaining = queue.slice(1);
return rating === "again"
? [...remaining, { ...card, ...nextSchedule }]
: remaining;
});Suppose the queue contains A, B and C. Rating A Again leaves B, C and A. If the learner answers B and C quickly, A can return well before its stored due date. With just one card in the queue, it can return immediately.
The stored schedule determines when the card is due when fetching a later review session. The active queue gives the learner another attempt during the session already underway.
That means “10m” does not fully describe what the learner will see. I would improve this by making the two consequences explicit in the interface: repeat during this session, with a stored review time ten minutes out. Another option is to enforce the delay in the active queue, but that introduces waiting and a different rule for ending a session.
Scheduling and saving are separate failure points
The calculation can be correct even if persistence fails. The screen currently advances its queue while the review update and review log are sent asynchronously. Their errors are caught without a visible recovery step.
That is a limitation I want to address. A learner can continue reviewing while a saved schedule fails to change. A pending-write state and a retry path would make that failure visible and recoverable; a correct scheduling function alone doesn't provide either.
Where the model fits
OpenAI generates draft cards from the supplied material. The generation endpoint asks for a structured title and card list, with basic or cloze card types and front/back text. Before saving, the learner can rename the deck and remove cards. Individual card content can be edited after saving.
Once a card exists, its next review is calculated by ordinary application code. I can change that scheduling rule without changing how cards are generated.