- Word import (multi-format: word, word-def, word:def, word|def) - Flashcard filter UI (swipe + keyboard arrows + Space to flip) - SM-2 spaced repetition review queue - Stimulus flashcard controller with 3D flip animation
79 lines
1.8 KiB
Ruby
79 lines
1.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Review < ApplicationRecord
|
|
belongs_to :word
|
|
|
|
EASE_MIN = 1.3
|
|
EASE_START = 2.5
|
|
|
|
# status: new | known | learning | skipped
|
|
|
|
scope :due, -> { where(status: "learning").where("due_at <= ?", Time.current).order(:due_at) }
|
|
scope :upcoming, -> { where(status: "learning").order(:due_at) }
|
|
|
|
def mark_known!
|
|
update!(status: "known", last_reviewed_at: Time.current, due_at: nil)
|
|
end
|
|
|
|
def mark_learning!
|
|
update!(
|
|
status: "learning",
|
|
interval: 1,
|
|
repetitions: 0,
|
|
ease_factor: EASE_START,
|
|
due_at: 1.day.from_now,
|
|
last_reviewed_at: Time.current
|
|
)
|
|
end
|
|
|
|
def mark_skipped!
|
|
update!(status: "skipped", last_reviewed_at: Time.current)
|
|
end
|
|
|
|
# SM-2 algorithm: quality 0-5
|
|
# 0-2 = forgot/hard 3-5 = remembered
|
|
def sm2_update!(quality)
|
|
quality = quality.to_i.clamp(0, 5)
|
|
|
|
new_ease = [EASE_MIN, ease_factor + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)].max.round(3)
|
|
|
|
if quality < 3
|
|
update!(
|
|
status: "learning",
|
|
repetitions: 0,
|
|
interval: 1,
|
|
ease_factor: new_ease,
|
|
due_at: 1.day.from_now,
|
|
last_reviewed_at: Time.current
|
|
)
|
|
else
|
|
new_reps = repetitions + 1
|
|
new_interval = case repetitions
|
|
when 0 then 1
|
|
when 1 then 6
|
|
else [ (interval * new_ease).ceil, 1 ].max
|
|
end
|
|
update!(
|
|
status: "learning",
|
|
repetitions: new_reps,
|
|
interval: new_interval,
|
|
ease_factor: new_ease,
|
|
due_at: new_interval.days.from_now,
|
|
last_reviewed_at: Time.current
|
|
)
|
|
end
|
|
end
|
|
|
|
def due_label
|
|
return "Now" unless due_at
|
|
return "Now" if due_at <= Time.current
|
|
|
|
days = ((due_at - Time.current) / 1.day).ceil
|
|
case days
|
|
when 0 then "Today"
|
|
when 1 then "Tomorrow"
|
|
else "#{days} days"
|
|
end
|
|
end
|
|
end
|