From 781a23c6c49bab0ed0764af6605ba916d08eb967 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 15 Apr 2025 11:21:54 +0100 Subject: [PATCH] Feat/Ignored before card count (#3910) * GetIgnoredBeforeCount * get_card_count_with_ignore_before * Included / total * Respect search * Get frontend hooked up * Fix: Malformed sql and search * Variable names * Added: Alert colours * i18n * ./check * Remove console.log * Fix: Tooltip showing for default value * Update ftl/core/deck-config.ftl Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * Fix: Multiple backend calls * Message: (Approximately) * Fix: Bouncing info message * Added: Change delay * Added: ignore_before_updated * ./check * Fix typing, camelCase and improve wording * Temporarily enable the check on startup --------- Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> --- ftl/core/deck-config.ftl | 1 + proto/anki/deck_config.proto | 12 ++++ qt/aqt/mediasrv.py | 2 + rslib/src/deckconfig/service.rs | 20 ++++++ rslib/src/scheduler/fsrs/params.rs | 2 +- .../storage/card/get_ignored_before_count.sql | 5 ++ rslib/src/storage/card/mod.rs | 14 ++++ ts/routes/deck-options/AdvancedOptions.svelte | 71 +++++++++++++++++++ 8 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 rslib/src/storage/card/get_ignored_before_count.sql diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 03eb04ea7..11ae8a392 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -372,6 +372,7 @@ deck-config-good-above-easy = The easy interval should be at least as long as th deck-config-relearning-steps-above-minimum-interval = The minimum lapse interval should be at least as long as your final relearning step. deck-config-maximum-answer-secs-above-recommended = Anki can schedule your reviews more efficiently when you keep each question short. deck-config-too-short-maximum-interval = A maximum interval less than 6 months is not recommended. +deck-config-ignore-before-info = (Approximately) { $included }/{ $totalCards } cards will be used to optimize the FSRS parameters. ## Selecting a deck diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index efd8a80d0..1abb74e43 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -23,6 +23,8 @@ service DeckConfigService { rpc GetDeckConfigsForUpdate(decks.DeckId) returns (DeckConfigsForUpdate); rpc UpdateDeckConfigs(UpdateDeckConfigsRequest) returns (collection.OpChanges); + rpc GetIgnoredBeforeCount(GetIgnoredBeforeCountRequest) + returns (GetIgnoredBeforeCountResponse); } // Implicitly includes any of the above methods that are not listed in the @@ -33,6 +35,16 @@ message DeckConfigId { int64 dcid = 1; } +message GetIgnoredBeforeCountRequest { + string ignore_revlogs_before_date = 1; + string search = 2; +} + +message GetIgnoredBeforeCountResponse { + uint64 included = 1; + uint64 total = 2; +} + message DeckConfig { message Config { enum NewCardInsertOrder { diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index b444d1ec0..9f75e5464 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -652,6 +652,8 @@ exposed_backend_list = [ "evaluate_params", "get_optimal_retention_parameters", "simulate_fsrs_review", + # DeckConfigService + "get_ignored_before_count", ] diff --git a/rslib/src/deckconfig/service.rs b/rslib/src/deckconfig/service.rs index 5776299fb..7ac08902e 100644 --- a/rslib/src/deckconfig/service.rs +++ b/rslib/src/deckconfig/service.rs @@ -8,6 +8,7 @@ use crate::deckconfig::DeckConfig; use crate::deckconfig::DeckConfigId; use crate::deckconfig::UpdateDeckConfigsRequest; use crate::error::Result; +use crate::scheduler::fsrs::params::ignore_revlogs_before_date_to_ms; impl crate::services::DeckConfigService for Collection { fn add_or_update_deck_config_legacy( @@ -76,6 +77,25 @@ impl crate::services::DeckConfigService for Collection { ) -> Result { self.update_deck_configs(input.into()).map(Into::into) } + + fn get_ignored_before_count( + &mut self, + input: anki_proto::deck_config::GetIgnoredBeforeCountRequest, + ) -> Result { + let timestamp = ignore_revlogs_before_date_to_ms(&input.ignore_revlogs_before_date)?; + let guard = self.search_cards_into_table( + &format!("{} -is:new", input.search), + crate::search::SortMode::NoOrder, + )?; + + Ok(anki_proto::deck_config::GetIgnoredBeforeCountResponse { + included: guard + .col + .storage + .get_card_count_with_ignore_before(timestamp)?, + total: guard.cards.try_into().unwrap_or(0), + }) + } } impl From for anki_proto::deck_config::DeckConfig { diff --git a/rslib/src/scheduler/fsrs/params.rs b/rslib/src/scheduler/fsrs/params.rs index 2bc3338eb..13e588535 100644 --- a/rslib/src/scheduler/fsrs/params.rs +++ b/rslib/src/scheduler/fsrs/params.rs @@ -33,7 +33,7 @@ use crate::search::SortMode; pub(crate) type Params = Vec; -fn ignore_revlogs_before_date_to_ms( +pub(crate) fn ignore_revlogs_before_date_to_ms( ignore_revlogs_before_date: &String, ) -> Result { Ok(match ignore_revlogs_before_date { diff --git a/rslib/src/storage/card/get_ignored_before_count.sql b/rslib/src/storage/card/get_ignored_before_count.sql new file mode 100644 index 000000000..b21e2fb74 --- /dev/null +++ b/rslib/src/storage/card/get_ignored_before_count.sql @@ -0,0 +1,5 @@ +SELECT COUNT(DISTINCT cid) +FROM revlog +WHERE id > ? + AND type == 0 + AND cid IN search_cids \ No newline at end of file diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index a699d5ef2..35819bcc5 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -732,6 +732,20 @@ impl super::SqliteStorage { Ok(()) } + pub(crate) fn get_card_count_with_ignore_before( + &self, + ignore_before: TimestampMillis, + ) -> Result { + Ok(self + .db + .prepare(include_str!("get_ignored_before_count.sql"))? + .query(params![ignore_before.0])? + .next() + .unwrap() + .unwrap() + .get(0)?) + } + #[cfg(test)] pub(crate) fn get_all_cards(&self) -> Vec { self.db diff --git a/ts/routes/deck-options/AdvancedOptions.svelte b/ts/routes/deck-options/AdvancedOptions.svelte index 93cfd41e3..31c3f0d4c 100644 --- a/ts/routes/deck-options/AdvancedOptions.svelte +++ b/ts/routes/deck-options/AdvancedOptions.svelte @@ -21,6 +21,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import SpinBoxRow from "./SpinBoxRow.svelte"; import DateInput from "./DateInput.svelte"; import Warning from "./Warning.svelte"; + import { getIgnoredBeforeCount } from "@generated/backend"; + import type { GetIgnoredBeforeCountResponse } from "@generated/anki/deck_config_pb"; export let state: DeckOptionsState; export let api: Record; @@ -91,6 +93,68 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ? tr.deckConfigTooShortMaximumInterval() : ""; + let ignoreRevlogsBeforeCount: GetIgnoredBeforeCountResponse | null = null; + let lastIgnoreRevlogsBeforeDate = ""; + function updateIgnoreRevlogsBeforeCount(ignoreRevlogsBeforeDate: string) { + if (lastIgnoreRevlogsBeforeDate == ignoreRevlogsBeforeDate) { + return; + } + if ( + cutoffUpdatedSinceLoad && + ignoreRevlogsBeforeDate && + ignoreRevlogsBeforeDate != "1970-01-01" + ) { + lastIgnoreRevlogsBeforeDate = ignoreRevlogsBeforeDate; + getIgnoredBeforeCount({ + search: + $config.paramSearch || + `preset:"${state.getCurrentNameForSearch()}" -is:suspended`, + ignoreRevlogsBeforeDate, + }).then((resp) => { + ignoreRevlogsBeforeCount = resp; + }); + } else { + ignoreRevlogsBeforeCount = null; + } + cutoffUpdatedSinceLoad = true; + } + + let timeoutId: ReturnType | undefined = undefined; + // Running the card count check on startup is inefficient. After users have had a few months + // to notice + update (e.g. from ~Oct 2025), we should change this to false. + let cutoffUpdatedSinceLoad = true; + const IGNORE_REVLOG_COUNT_DELAY_MS = 1000; + + $: { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + updateIgnoreRevlogsBeforeCount($config.ignoreRevlogsBeforeDate); + }, IGNORE_REVLOG_COUNT_DELAY_MS); + } + let ignoreRevlogsBeforeWarningClass = "alert-warning"; + $: if (ignoreRevlogsBeforeCount) { + // If there is less than a tenth of reviews included + if ( + Number(ignoreRevlogsBeforeCount.included) / + Number(ignoreRevlogsBeforeCount.total) < + 0.1 + ) { + ignoreRevlogsBeforeWarningClass = "alert-danger"; + } else if ( + ignoreRevlogsBeforeCount.included != ignoreRevlogsBeforeCount.total + ) { + ignoreRevlogsBeforeWarningClass = "alert-warning"; + } else { + ignoreRevlogsBeforeWarningClass = "alert-info"; + } + } + $: ignoreRevlogsBeforeWarning = ignoreRevlogsBeforeCount + ? tr.deckConfigIgnoreBeforeInfo({ + included: ignoreRevlogsBeforeCount.included.toString(), + totalCards: ignoreRevlogsBeforeCount.total.toString(), + }) + : ""; + let modal: Modal; let carousel: Carousel; @@ -249,6 +313,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + + + {/if}