diff --git a/.vscode.dist/settings.json b/.vscode.dist/settings.json index a3338ec6462..690b5bda024 100644 --- a/.vscode.dist/settings.json +++ b/.vscode.dist/settings.json @@ -2,7 +2,7 @@ "editor.formatOnSave": true, "[python]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" } }, "files.watcherExclude": { diff --git a/Cargo.lock b/Cargo.lock index 63078a33435..6d665180005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2099,7 +2099,7 @@ dependencies = [ [[package]] name = "fsrs" version = "3.0.0" -source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=96520531415e032781adfe212f8a5eed216006be#96520531415e032781adfe212f8a5eed216006be" +source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=22f8e453c120f5bc5996f86558a559c6b7abfc49#22f8e453c120f5bc5996f86558a559c6b7abfc49" dependencies = [ "burn", "itertools 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index 6bd0c35e183..e3299a740e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] # version = "=2.0.3" git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -rev = "96520531415e032781adfe212f8a5eed216006be" +rev = "22f8e453c120f5bc5996f86558a559c6b7abfc49" # path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index b657ff3fa24..42befe816dc 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -393,6 +393,7 @@ message SimulateFsrsReviewRequest { bool new_cards_ignore_review_limit = 9; repeated float easy_days_percentages = 10; deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11; + optional uint32 suspend_after_lapse_count = 12; } message SimulateFsrsReviewResponse { @@ -409,6 +410,7 @@ message ComputeOptimalRetentionRequest { string search = 4; double loss_aversion = 5; repeated float easy_days_percentages = 6; + optional uint32 suspend_after_lapse_count = 7; } message ComputeOptimalRetentionResponse { diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index c89c08aab52..23df5b33a1e 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -44,9 +44,9 @@ impl Collection { let post_scheduling_fn: Option = if self.get_config_bool(BoolKey::LoadBalancerEnabled) { Some(PostSchedulingFn(Arc::new( - move |interval, max_interval, today, due_cnt_per_day, rng| { + move |card, max_interval, today, due_cnt_per_day, rng| { apply_load_balance_and_easy_days( - interval, + card.interval, max_interval, today, due_cnt_per_day, @@ -78,6 +78,7 @@ impl Collection { learn_limit, review_limit: usize::MAX, new_cards_ignore_review_limit: true, + suspend_after_lapses: None, post_scheduling_fn, review_priority_fn: None, }, diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 1cfc2e959bc..34bec1317f9 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -142,11 +142,13 @@ impl Collection { .min(req.new_limit as usize); if req.new_limit > 0 { let new_cards = (0..new_cards).map(|i| fsrs::Card { + id: -(i as i64), difficulty: f32::NEG_INFINITY, stability: 1e-8, // Not filtered by fsrs-rs last_date: f32::NEG_INFINITY, // Treated as a new card in simulation due: ((introduced_today_count + i) / req.new_limit as usize) as f32, interval: f32::NEG_INFINITY, + lapses: 0, }); converted_cards.extend(new_cards); } @@ -159,9 +161,9 @@ impl Collection { let post_scheduling_fn: Option = if self.get_config_bool(BoolKey::LoadBalancerEnabled) { Some(PostSchedulingFn(Arc::new( - move |interval, max_interval, today, due_cnt_per_day, rng| { + move |card, max_interval, today, due_cnt_per_day, rng| { apply_load_balance_and_easy_days( - interval, + card.interval, max_interval, today, due_cnt_per_day, @@ -198,6 +200,7 @@ impl Collection { learn_limit: req.new_limit as usize, review_limit: req.review_limit as usize, new_cards_ignore_review_limit: req.new_cards_ignore_review_limit, + suspend_after_lapses: req.suspend_after_lapse_count, post_scheduling_fn, review_priority_fn, }; @@ -234,21 +237,25 @@ impl Card { let relative_due = due - days_elapsed; let last_date = (relative_due - card.interval as i32).min(0) as f32; Some(fsrs::Card { + id: card.id.0, difficulty: state.difficulty, stability: state.stability, last_date, due: relative_due as f32, interval: card.interval as f32, + lapses: card.lapses, }) } CardQueue::New => None, CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => { Some(fsrs::Card { + id: card.id.0, difficulty: state.difficulty, stability: state.stability, last_date: 0.0, due: 0.0, interval: card.interval as f32, + lapses: card.lapses, }) } CardQueue::PreviewRepeat => None, diff --git a/ts/routes/deck-options/EasyDays.svelte b/ts/routes/deck-options/EasyDays.svelte index aea4e026e04..e1fe4507d67 100644 --- a/ts/routes/deck-options/EasyDays.svelte +++ b/ts/routes/deck-options/EasyDays.svelte @@ -9,6 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import TitledContainer from "$lib/components/TitledContainer.svelte"; import type { DeckOptionsState } from "./lib"; import Warning from "./Warning.svelte"; + import EasyDaysInput from "./EasyDaysInput.svelte"; export let state: DeckOptionsState; export let api: Record; @@ -35,16 +36,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html easyDaysChanged && !($fsrsEnabled && $reschedule) ? tr.deckConfigEasyDaysChange() : ""; - - const easyDays = [ - tr.deckConfigEasyDaysMonday(), - tr.deckConfigEasyDaysTuesday(), - tr.deckConfigEasyDaysWednesday(), - tr.deckConfigEasyDaysThursday(), - tr.deckConfigEasyDaysFriday(), - tr.deckConfigEasyDaysSaturday(), - tr.deckConfigEasyDaysSunday(), - ]; @@ -53,43 +44,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -
- - - - - - - - - - - {#each easyDays as day, index} - - - - - {/each} - -
- {tr.deckConfigEasyDaysMinimum()} - - {tr.deckConfigEasyDaysReduced()} - - {tr.deckConfigEasyDaysNormal()} -
{day} - -
-
-
+ @@ -98,36 +53,3 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
- - diff --git a/ts/routes/deck-options/EasyDaysInput.svelte b/ts/routes/deck-options/EasyDaysInput.svelte new file mode 100644 index 00000000000..a7c13e2e211 --- /dev/null +++ b/ts/routes/deck-options/EasyDaysInput.svelte @@ -0,0 +1,91 @@ + + + + +
+ + + + + + + + + + + {#each easyDays as day, index} + + + + + {/each} + +
+ {tr.deckConfigEasyDaysMinimum()} + + {tr.deckConfigEasyDaysReduced()} + + {tr.deckConfigEasyDaysNormal()} +
{day} + +
+
+
+ + diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index 6bb30620c53..1b587c98540 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -28,6 +28,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; import { reviewOrderChoices } from "./choices"; import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte"; + import { DeckConfig_Config_LeechAction } from "@generated/anki/deck_config_pb"; + import EasyDaysInput from "./EasyDaysInput.svelte"; export let shown = false; export let state: DeckOptionsState; @@ -48,6 +50,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let points: Point[] = []; const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit; let smooth = true; + let suspendLeeches = $config.leechAction == DeckConfig_Config_LeechAction.SUSPEND; + let leechThreshold = $config.leechThreshold; $: daysToSimulate = 365; $: deckSize = 0; @@ -75,6 +79,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let resp: SimulateFsrsReviewResponse | undefined; simulateFsrsRequest.daysToSimulate = daysToSimulate; simulateFsrsRequest.deckSize = deckSize; + simulateFsrsRequest.suspendAfterLapseCount = suspendLeeches + ? leechThreshold + : undefined; + simulateFsrsRequest.easyDaysPercentages = easyDayPercentages; try { await runWithBackendProgress( async () => { @@ -169,6 +177,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html simulateSubgraph, ); } + + let easyDayPercentages = [...$config.easyDaysPercentages]; - - - - - - +
+ + + + + + +
@@ -359,6 +423,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html