diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index e0b76d56a..e9e902264 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -346,6 +346,7 @@ deck-config-compute-optimal-weights = Optimize FSRS parameters deck-config-compute-optimal-retention = Compute optimal retention deck-config-optimize-button = Optimize deck-config-compute-button = Compute +deck-config-ignore-before = Ignore reviews before deck-config-optimize-all-tip = You can optimize all presets at once by using the dropdown button next to "Save". deck-config-evaluate-button = Evaluate deck-config-desired-retention = Desired retention @@ -398,6 +399,9 @@ deck-config-reschedule-cards-warning = Use this option sparingly, as it will add a review entry to each of your cards, and increase the size of your collection. +deck-config-ignore-before-tooltip = + If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters. + This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons. deck-config-compute-optimal-weights-tooltip = Once you've done 1000+ reviews in Anki, you can use the Optimize button to analyze your review history, and automatically generate parameters that are optimal for your memory and the content you're studying. diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index 98d27d8f2..137d03dc6 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -154,6 +154,7 @@ message DeckConfig { // for fsrs float desired_retention = 37; + string ignore_revlogs_before_date = 46; // used for fsrs_reschedule in the past reserved 39; float sm2_retention = 40; diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 5def20e82..acdaa0369 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -339,6 +339,7 @@ message ComputeFsrsWeightsRequest { /// The search used to gather cards for training string search = 1; repeated float current_weights = 2; + int64 ignore_revlogs_before_ms = 3; } message ComputeFsrsWeightsResponse { @@ -400,6 +401,7 @@ message GetOptimalRetentionParametersResponse { message EvaluateWeightsRequest { repeated float weights = 1; string search = 2; + int64 ignore_revlogs_before_ms = 3; } message EvaluateWeightsResponse { diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index 6fab45db4..fb30353cb 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -77,6 +77,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner { other: Vec::new(), sm2_retention: 0.9, weight_search: String::new(), + ignore_revlogs_before_date: String::new(), }; impl Default for DeckConfig { diff --git a/rslib/src/deckconfig/schema11.rs b/rslib/src/deckconfig/schema11.rs index daca7d7ac..3e0c55263 100644 --- a/rslib/src/deckconfig/schema11.rs +++ b/rslib/src/deckconfig/schema11.rs @@ -74,6 +74,8 @@ pub struct DeckConfSchema11 { #[serde(default)] desired_retention: f32, #[serde(default)] + ignore_revlogs_before_date: String, + #[serde(default)] stop_timer_on_answer: bool, #[serde(default)] seconds_to_show_question: f32, @@ -294,6 +296,7 @@ impl Default for DeckConfSchema11 { desired_retention: 0.9, sm2_retention: 0.9, weight_search: "".to_string(), + ignore_revlogs_before_date: "".to_string(), } } } @@ -368,6 +371,7 @@ impl From for DeckConfig { bury_reviews: c.rev.bury, bury_interday_learning: c.bury_interday_learning, fsrs_weights: c.fsrs_weights, + ignore_revlogs_before_date: c.ignore_revlogs_before_date, desired_retention: c.desired_retention, sm2_retention: c.sm2_retention, weight_search: c.weight_search, @@ -477,6 +481,7 @@ impl From for DeckConfSchema11 { desired_retention: i.desired_retention, sm2_retention: i.sm2_retention, weight_search: i.weight_search, + ignore_revlogs_before_date: i.ignore_revlogs_before_date, } } } @@ -507,6 +512,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! { "waitForAudio", "sm2Retention", "weightSearch", + "ignoreRevlogsBeforeDate", }; static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! { diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index bc6fe87be..24517aa07 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -17,7 +17,9 @@ use fsrs::DEFAULT_WEIGHTS; use crate::config::StringKey; use crate::decks::NormalDeck; use crate::prelude::*; +use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest; +use crate::scheduler::fsrs::weights::ignore_revlogs_before_ms_from_config; use crate::search::JoinSearches; use crate::search::SearchNode; use crate::storage::comma_separated_ids; @@ -238,29 +240,32 @@ impl Collection { } if !decks_needing_memory_recompute.is_empty() { - let input: Vec<(Option, SearchNode)> = - decks_needing_memory_recompute - .into_iter() - .map(|(conf_id, search)| { - let weights = configs_after_update.get(&conf_id).and_then(|c| { - if req.fsrs { - Some(UpdateMemoryStateRequest { - weights: c.inner.fsrs_weights.clone(), - desired_retention: c.inner.desired_retention, - max_interval: c.inner.maximum_review_interval, - reschedule: req.fsrs_reschedule, - sm2_retention: c.inner.sm2_retention, - }) - } else { - None - } - }); - Ok(( - weights, - SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)), - )) + let input: Vec = decks_needing_memory_recompute + .into_iter() + .map(|(conf_id, search)| { + let config = configs_after_update.get(&conf_id); + let weights = config.and_then(|c| { + if req.fsrs { + Some(UpdateMemoryStateRequest { + weights: c.inner.fsrs_weights.clone(), + desired_retention: c.inner.desired_retention, + max_interval: c.inner.maximum_review_interval, + reschedule: req.fsrs_reschedule, + sm2_retention: c.inner.sm2_retention, + }) + } else { + None + } + }); + Ok(UpdateMemoryStateEntry { + req: weights, + search: SearchNode::DeckIdsWithoutChildren(comma_separated_ids(&search)), + ignore_before: config + .map(ignore_revlogs_before_ms_from_config) + .unwrap_or(Ok(0.into()))?, }) - .collect::>()?; + }) + .collect::>()?; self.update_memory_state(input)?; } @@ -332,8 +337,10 @@ impl Collection { } else { config.inner.weight_search.clone() }; + let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?; match self.compute_weights( &search, + ignore_revlogs_before_ms, idx as u32 + 1, config_len, &config.inner.fsrs_weights, diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 906e0d543..6c4d947d2 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -14,6 +14,7 @@ use rand::prelude::*; use rand::rngs::StdRng; use revlog::RevlogEntryPartial; +use super::fsrs::weights::ignore_revlogs_before_ms_from_config; use super::queue::BuryMode; use super::states::steps::LearningSteps; use super::states::CardState; @@ -382,6 +383,7 @@ impl Collection { revlog, timing.next_day_at, config.inner.sm2_retention, + ignore_revlogs_before_ms_from_config(&config)?, )?; card.set_memory_state(&fsrs, item, config.inner.sm2_retention)?; } diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 55565b06e..e14d3dfda 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -9,6 +9,7 @@ use fsrs::MemoryState; use fsrs::FSRS; use itertools::Itertools; +use super::weights::ignore_revlogs_before_ms_from_config; use crate::card::CardType; use crate::prelude::*; use crate::revlog::RevlogEntry; @@ -35,6 +36,12 @@ pub(crate) struct UpdateMemoryStateRequest { pub reschedule: bool, } +pub(crate) struct UpdateMemoryStateEntry { + pub req: Option, + pub search: SearchNode, + pub ignore_before: TimestampMillis, +} + impl Collection { /// For each provided set of weights, locate cards with the provided search, /// and update their memory state. @@ -43,11 +50,16 @@ impl Collection { /// memory state should be removed. pub(crate) fn update_memory_state( &mut self, - entries: Vec<(Option, SearchNode)>, + entries: Vec, ) -> Result<()> { let timing = self.timing_today()?; let usn = self.usn()?; - for (req, search) in entries { + for UpdateMemoryStateEntry { + req, + search, + ignore_before, + } in entries + { let search = SearchBuilder::all([search.into(), SearchNode::State(StateKind::New).negated()]); let revlog = self.revlog_for_srs(search)?; @@ -64,6 +76,7 @@ impl Collection { revlog, timing.next_day_at, sm2_retention.unwrap_or(0.9), + ignore_before, )?; let desired_retention = req.as_ref().map(|w| w.desired_retention); let mut progress = self.new_progress_handler::(); @@ -148,6 +161,7 @@ impl Collection { revlog, self.timing_today()?.next_day_at, sm2_retention, + ignore_revlogs_before_ms_from_config(&config)?, )?; card.set_memory_state(&fsrs, item, sm2_retention)?; Ok(ComputeMemoryStateResponse { @@ -196,6 +210,7 @@ pub(crate) fn fsrs_items_for_memory_state( revlogs: Vec, next_day_at: TimestampSecs, sm2_retention: f32, + ignore_revlogs_before: TimestampMillis, ) -> Result)>> { revlogs .into_iter() @@ -204,7 +219,13 @@ pub(crate) fn fsrs_items_for_memory_state( .map(|(card_id, group)| { Ok(( card_id, - single_card_revlog_to_item(fsrs, group.collect(), next_day_at, sm2_retention)?, + single_card_revlog_to_item( + fsrs, + group.collect(), + next_day_at, + sm2_retention, + ignore_revlogs_before, + )?, )) }) .collect() @@ -257,6 +278,7 @@ pub(crate) fn single_card_revlog_to_item( entries: Vec, next_day_at: TimestampSecs, sm2_retention: f32, + ignore_revlogs_before: TimestampMillis, ) -> Result> { struct FirstReview { interval: f32, @@ -275,7 +297,7 @@ pub(crate) fn single_card_revlog_to_item( / 1000.0, }); if let Some((mut items, revlogs_complete)) = - single_card_revlog_to_items(entries, next_day_at, false) + single_card_revlog_to_items(entries, next_day_at, false, ignore_revlogs_before) { let mut item = items.pop().unwrap(); if revlogs_complete { @@ -345,6 +367,7 @@ mod tests { ], TimestampSecs::now(), 0.9, + 0.into(), )? .unwrap(); assert_int_eq( @@ -377,6 +400,7 @@ mod tests { }], TimestampSecs::now(), 0.9, + 0.into(), )?; assert!(item.is_none()); card.interval = 123; diff --git a/rslib/src/scheduler/fsrs/weights.rs b/rslib/src/scheduler/fsrs/weights.rs index 78e1dd02b..fcb2c274b 100644 --- a/rslib/src/scheduler/fsrs/weights.rs +++ b/rslib/src/scheduler/fsrs/weights.rs @@ -9,6 +9,8 @@ use anki_io::write_file; use anki_proto::scheduler::ComputeFsrsWeightsResponse; use anki_proto::stats::revlog_entry; use anki_proto::stats::RevlogEntries; +use chrono::NaiveDate; +use chrono::NaiveTime; use fsrs::CombinedProgressState; use fsrs::FSRSItem; use fsrs::FSRSReview; @@ -26,6 +28,23 @@ use crate::search::SortMode; pub(crate) type Weights = Vec; +fn ignore_revlogs_before_date_to_ms( + ignore_revlogs_before_date: &String, +) -> Result { + Ok(match ignore_revlogs_before_date { + s if s.is_empty() => 0, + s => NaiveDate::parse_from_str(s.as_str(), "%Y-%m-%d") + .or_else(|err| invalid_input!(err, "Error parsing date: {s}"))? + .and_time(NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap()) + .timestamp_millis(), + } + .into()) +} + +pub(crate) fn ignore_revlogs_before_ms_from_config(config: &DeckConfig) -> Result { + ignore_revlogs_before_date_to_ms(&config.inner.ignore_revlogs_before_date) +} + impl Collection { /// Note this does not return an error if there are less than 400 items - /// the caller should instead check the fsrs_items count in the return @@ -33,6 +52,7 @@ impl Collection { pub fn compute_weights( &mut self, search: &str, + ignore_revlogs_before: TimestampMillis, current_preset: u32, total_presets: u32, current_weights: &Weights, @@ -45,7 +65,8 @@ impl Collection { count: revlogs.len(), }); } - let items = fsrs_items_for_training(revlogs.clone(), timing.next_day_at); + let items = + fsrs_items_for_training(revlogs.clone(), timing.next_day_at, ignore_revlogs_before); let fsrs_items = items.len() as u32; anki_progress.update(false, |p| { p.fsrs_items = fsrs_items; @@ -122,11 +143,16 @@ impl Collection { Ok(()) } - pub fn evaluate_weights(&mut self, weights: &Weights, search: &str) -> Result { + pub fn evaluate_weights( + &mut self, + weights: &Weights, + search: &str, + ignore_revlogs_before: TimestampMillis, + ) -> Result { let timing = self.timing_today()?; let mut anki_progress = self.new_progress_handler::(); let guard = self.search_cards_into_table(search, SortMode::NoOrder)?; - let revlogs = guard + let revlogs: Vec = guard .col .storage .get_revlog_entries_for_searched_cards_in_card_order()?; @@ -135,8 +161,8 @@ impl Collection { count: revlogs.len(), }); } - let items = fsrs_items_for_training(revlogs, timing.next_day_at); - anki_progress.state.fsrs_items = items.len() as u32; + anki_progress.state.fsrs_items = revlogs.len() as u32; + let items = fsrs_items_for_training(revlogs, timing.next_day_at, ignore_revlogs_before); let fsrs = FSRS::new(Some(weights))?; Ok(fsrs.evaluate(items, |ip| { anki_progress @@ -161,13 +187,17 @@ pub struct ComputeWeightsProgress { } /// Convert a series of revlog entries sorted by card id into FSRS items. -fn fsrs_items_for_training(revlogs: Vec, next_day_at: TimestampSecs) -> Vec { +fn fsrs_items_for_training( + revlogs: Vec, + next_day_at: TimestampSecs, + review_revlogs_before: TimestampMillis, +) -> Vec { let mut revlogs = revlogs .into_iter() .group_by(|r| r.cid) .into_iter() .filter_map(|(_cid, entries)| { - single_card_revlog_to_items(entries.collect(), next_day_at, true) + single_card_revlog_to_items(entries.collect(), next_day_at, true, review_revlogs_before) }) .flat_map(|i| i.0) .collect_vec(); @@ -189,24 +219,25 @@ pub(crate) fn single_card_revlog_to_items( mut entries: Vec, next_day_at: TimestampSecs, training: bool, + ignore_revlogs_before: TimestampMillis, ) -> Option<(Vec, bool)> { - let mut last_learn_entry = None; + let mut first_of_last_learn_entries = None; let mut revlogs_complete = false; for (index, entry) in entries.iter().enumerate().rev() { if matches!( (entry.review_kind, entry.button_chosen), (RevlogReviewKind::Learning, 1..=4) ) { - last_learn_entry = Some(index); + first_of_last_learn_entries = Some(index); revlogs_complete = true; - } else if last_learn_entry.is_some() { + } else if first_of_last_learn_entries.is_some() { break; // if we find the `Forget` entry before the `Learn` entry, we should // ignore all the entries } else if matches!( (entry.review_kind, entry.ease_factor), (RevlogReviewKind::Manual, 0) - ) && last_learn_entry.is_none() + ) && first_of_last_learn_entries.is_none() { revlogs_complete = false; break; @@ -221,12 +252,40 @@ pub(crate) fn single_card_revlog_to_items( }) ); } + if training { + // While training ignore the entire card if the first learning step of the last + // group of learning steps is before the ignore_revlogs_before date + if let Some(idx) = first_of_last_learn_entries { + if entries[idx].id.0 < ignore_revlogs_before.0 { + return None; + } + } + } else { + // While reviewing if the first learning step is before the ignore date, + // ignore every review before and including the last learning step + if let Some(idx) = first_of_last_learn_entries { + if entries[idx].id.0 < ignore_revlogs_before.0 && idx < entries.len() - 1 { + let last_learn_entry = entries + .iter() + .enumerate() + .rev() + .find(|(_idx, e)| e.review_kind == RevlogReviewKind::Learning) + .map(|(idx, _)| idx); + + entries.drain(..(last_learn_entry? + 1)); + revlogs_complete = false; + first_of_last_learn_entries = None; + } + } + } let first_relearn = entries .iter() .enumerate() - .find(|(_idx, e)| e.review_kind == RevlogReviewKind::Relearning) + .find(|(_idx, e)| { + e.id.0 > ignore_revlogs_before.0 && e.review_kind == RevlogReviewKind::Relearning + }) .map(|(idx, _)| idx); - if let Some(idx) = last_learn_entry.or(first_relearn) { + if let Some(idx) = first_of_last_learn_entries.or(first_relearn) { // start from the (re)learning step if idx > 0 { entries.drain(..idx); @@ -315,10 +374,14 @@ pub(crate) mod tests { const NEXT_DAY_AT: TimestampSecs = TimestampSecs(86400 * 100); + fn days_ago_ms(days_ago: i64) -> TimestampMillis { + ((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into() + } + pub(crate) fn revlog(review_kind: RevlogReviewKind, days_ago: i64) -> RevlogEntry { RevlogEntry { review_kind, - id: ((NEXT_DAY_AT.0 - days_ago * 86400) * 1000).into(), + id: days_ago_ms(days_ago).into(), button_chosen: 3, ..Default::default() } @@ -328,8 +391,17 @@ pub(crate) mod tests { FSRSReview { rating: 3, delta_t } } + pub(crate) fn convert_ignore_before( + revlog: &[RevlogEntry], + training: bool, + ignore_before: TimestampMillis, + ) -> Option> { + single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training, ignore_before) + .map(|i| i.0) + } + pub(crate) fn convert(revlog: &[RevlogEntry], training: bool) -> Option> { - single_card_revlog_to_items(revlog.to_vec(), NEXT_DAY_AT, training).map(|i| i.0) + convert_ignore_before(revlog, training, 0.into()) } #[macro_export] @@ -454,4 +526,53 @@ pub(crate) mod tests { fsrs_items!([review(0)]) ); } + + #[test] + fn ignores_cards_before_ignore_before_date_when_training() { + let revlogs = &[ + revlog(RevlogReviewKind::Learning, 10), + revlog(RevlogReviewKind::Learning, 8), + ]; + // | = Ignore before + // L = learning step + // L L | + assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(7)), None); + // L | L + assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(9)), None); + // L (|L) (exact same millisecond) + assert_eq!( + convert_ignore_before(revlogs, true, days_ago_ms(10)), + convert(revlogs, true) + ); + // | L L + assert_eq!( + convert_ignore_before(revlogs, true, days_ago_ms(11)), + convert(revlogs, true) + ); + } + + #[test] + fn ignore_before_date_between_learning_steps_when_reviewing() { + let revlogs = &[ + revlog(RevlogReviewKind::Learning, 10), + revlog(RevlogReviewKind::Learning, 8), + revlog(RevlogReviewKind::Review, 2), + ]; + // L | L R + assert_ne!( + convert_ignore_before(revlogs, false, days_ago_ms(9)), + convert(revlogs, false) + ); + assert_eq!( + convert_ignore_before(revlogs, false, days_ago_ms(9)) + .unwrap() + .len(), + 1 + ); + // | L L R + assert_eq!( + convert_ignore_before(revlogs, false, days_ago_ms(11)), + convert(revlogs, false) + ); + } } diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 6a3d43cd8..83352beca 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -254,7 +254,13 @@ impl crate::services::SchedulerService for Collection { &mut self, input: scheduler::ComputeFsrsWeightsRequest, ) -> Result { - self.compute_weights(&input.search, 1, 1, &input.current_weights) + self.compute_weights( + &input.search, + input.ignore_revlogs_before_ms.into(), + 1, + 1, + &input.current_weights, + ) } fn compute_optimal_retention( @@ -270,7 +276,11 @@ impl crate::services::SchedulerService for Collection { &mut self, input: scheduler::EvaluateWeightsRequest, ) -> Result { - let ret = self.evaluate_weights(&input.weights, &input.search)?; + let ret = self.evaluate_weights( + &input.weights, + &input.search, + input.ignore_revlogs_before_ms.into(), + )?; Ok(scheduler::EvaluateWeightsResponse { log_loss: ret.log_loss, rmse_bins: ret.rmse_bins, diff --git a/ts/deck-options/AdvancedOptions.svelte b/ts/deck-options/AdvancedOptions.svelte index 7fdc72d62..72c109fc3 100644 --- a/ts/deck-options/AdvancedOptions.svelte +++ b/ts/deck-options/AdvancedOptions.svelte @@ -62,6 +62,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html help: tr.deckConfigRescheduleCardsOnChangeTooltip(), sched: HelpItemScheduler.FSRS, }, + ignoreRevlogsBeforeMs: { + title: tr.deckConfigIgnoreBefore(), + help: tr.deckConfigIgnoreBeforeTooltip(), + sched: HelpItemScheduler.FSRS, + }, computeOptimalWeights: { title: tr.deckConfigComputeOptimalWeights(), help: tr.deckConfigComputeOptimalWeightsTooltip(), diff --git a/ts/deck-options/DateInput.svelte b/ts/deck-options/DateInput.svelte new file mode 100644 index 000000000..a75339ba8 --- /dev/null +++ b/ts/deck-options/DateInput.svelte @@ -0,0 +1,34 @@ + + + +
+ + + + + + + + + + +
+ + diff --git a/ts/deck-options/FsrsOptions.svelte b/ts/deck-options/FsrsOptions.svelte index ccf498e58..5a438ff5d 100644 --- a/ts/deck-options/FsrsOptions.svelte +++ b/ts/deck-options/FsrsOptions.svelte @@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import SwitchRow from "components/SwitchRow.svelte"; import SettingTitle from "../components/SettingTitle.svelte"; + import DateInput from "./DateInput.svelte"; import GlobalLabel from "./GlobalLabel.svelte"; import type { DeckOptionsState } from "./lib"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; @@ -86,6 +87,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + function getIgnoreRevlogsBeforeMs() { + return BigInt( + $config.ignoreRevlogsBeforeDate + ? new Date($config.ignoreRevlogsBeforeDate).getTime() + : 0, + ); + } + async function computeWeights(): Promise { if (computingWeights) { await setWantsAbort({}); @@ -104,6 +113,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html search: $config.weightSearch ? $config.weightSearch : defaultWeightSearch, + ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), currentWeights: $config.fsrsWeights, }); if (computeWeightsProgress) { @@ -148,6 +158,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const resp = await evaluateWeights({ weights: $config.fsrsWeights, search, + ignoreRevlogsBeforeMs: getIgnoreRevlogsBeforeMs(), }); if (computeWeightsProgress) { computeWeightsProgress.current = computeWeightsProgress.total; @@ -317,6 +328,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {tr.deckConfigEvaluateButton()} {/if} + + openHelpModal("ignoreBefore")}> + {tr.deckConfigIgnoreBefore()} + + {#if computingWeights || checkingWeights}
{computeWeightsProgressString}
{/if}