mirror of
https://github.com/ankitects/anki.git
synced 2026-01-12 13:33:55 -05:00
Feat/per-deck desired retention
This commit is contained in:
parent
51cf09daf3
commit
eef199830e
8 changed files with 126 additions and 23 deletions
|
|
@ -219,6 +219,8 @@ message DeckConfigsForUpdate {
|
|||
bool review_today_active = 5;
|
||||
// Whether new_today applies to today or a past day.
|
||||
bool new_today_active = 6;
|
||||
// Deck-specific desired retention override
|
||||
optional float desired_retention = 7;
|
||||
}
|
||||
string name = 1;
|
||||
int64 config_id = 2;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ message Deck {
|
|||
optional uint32 new_limit = 7;
|
||||
DayLimit review_limit_today = 8;
|
||||
DayLimit new_limit_today = 9;
|
||||
// Deck-specific desired retention override
|
||||
optional float desired_retention = 10;
|
||||
|
||||
reserved 12 to 15;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,6 +409,7 @@ fn normal_deck_to_limits(deck: &NormalDeck, today: u32) -> Limits {
|
|||
.new_limit_today
|
||||
.map(|limit| limit.today == today)
|
||||
.unwrap_or_default(),
|
||||
desired_retention: deck.desired_retention,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -417,6 +418,7 @@ fn update_deck_limits(deck: &mut NormalDeck, limits: &Limits, today: u32) {
|
|||
deck.new_limit = limits.new;
|
||||
update_day_limit(&mut deck.review_limit_today, limits.review_today, today);
|
||||
update_day_limit(&mut deck.new_limit_today, limits.new_today, today);
|
||||
deck.desired_retention = limits.desired_retention;
|
||||
}
|
||||
|
||||
fn update_day_limit(day_limit: &mut Option<DayLimit>, new_limit: Option<u32>, today: u32) {
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ impl From<NormalDeckSchema11> for NormalDeck {
|
|||
new_limit: deck.new_limit,
|
||||
review_limit_today: deck.review_limit_today,
|
||||
new_limit_today: deck.new_limit_today,
|
||||
desired_retention: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -444,6 +444,15 @@ impl Collection {
|
|||
.get_deck(card.deck_id)?
|
||||
.or_not_found(card.deck_id)?;
|
||||
let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?;
|
||||
|
||||
// Get deck-specific desired retention if available, otherwise use config
|
||||
// default
|
||||
let desired_retention = if let Some(deck_dr) = deck.normal()?.desired_retention {
|
||||
deck_dr
|
||||
} else {
|
||||
config.inner.desired_retention
|
||||
};
|
||||
|
||||
let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs);
|
||||
let fsrs_next_states = if fsrs_enabled {
|
||||
let params = config.fsrs_params();
|
||||
|
|
@ -473,13 +482,13 @@ impl Collection {
|
|||
};
|
||||
Some(fsrs.next_states(
|
||||
card.memory_state.map(Into::into),
|
||||
config.inner.desired_retention,
|
||||
desired_retention,
|
||||
days_elapsed,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let desired_retention = fsrs_enabled.then_some(config.inner.desired_retention);
|
||||
let desired_retention = fsrs_enabled.then_some(desired_retention);
|
||||
let fsrs_short_term_with_steps =
|
||||
self.get_config_bool(BoolKey::FsrsShortTermWithStepsEnabled);
|
||||
let fsrs_allow_short_term = if fsrs_enabled {
|
||||
|
|
@ -662,6 +671,46 @@ pub(crate) mod test {
|
|||
col.get_scheduling_states(card_id).unwrap().current
|
||||
}
|
||||
|
||||
// Test that deck-specific desired retention is used when available
|
||||
#[test]
|
||||
fn deck_specific_desired_retention() -> Result<()> {
|
||||
let mut col = Collection::new();
|
||||
|
||||
// Enable FSRS
|
||||
col.set_config_bool(BoolKey::Fsrs, true, false)?;
|
||||
|
||||
// Create a deck with specific desired retention
|
||||
let deck_id = DeckId(1);
|
||||
let deck = col.get_deck(deck_id)?.unwrap();
|
||||
let mut deck_clone = (*deck).clone();
|
||||
if let DeckKind::Normal(ref mut normal) = deck_clone.kind {
|
||||
normal.desired_retention = Some(0.85); // Set deck-specific desired
|
||||
// retention
|
||||
}
|
||||
col.update_deck(&mut deck_clone)?;
|
||||
|
||||
// Create a card in this deck
|
||||
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||
let mut note = nt.new_note();
|
||||
col.add_note(&mut note, deck_id)?;
|
||||
|
||||
// Get the card using search_cards
|
||||
let cards = col.search_cards(note.id, SortMode::NoOrder)?;
|
||||
let card = col.storage.get_card(cards[0])?.unwrap();
|
||||
|
||||
// Test that the card state updater uses deck-specific desired retention
|
||||
let updater = col.card_state_updater(card)?;
|
||||
|
||||
// Print debug information
|
||||
println!("FSRS enabled: {}", col.get_config_bool(BoolKey::Fsrs));
|
||||
println!("Desired retention: {:?}", updater.desired_retention);
|
||||
|
||||
// Verify that the desired retention is from the deck, not the config
|
||||
assert_eq!(updater.desired_retention, Some(0.85));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// make sure the 'current' state for a card matches the
|
||||
// state we applied to it
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -205,7 +205,15 @@ impl Collection {
|
|||
.storage
|
||||
.get_deck_config(conf_id)?
|
||||
.or_not_found(conf_id)?;
|
||||
let desired_retention = config.inner.desired_retention;
|
||||
|
||||
// Get deck-specific desired retention if available, otherwise use config
|
||||
// default
|
||||
let desired_retention = if let Some(deck_dr) = deck.normal()?.desired_retention {
|
||||
deck_dr
|
||||
} else {
|
||||
config.inner.desired_retention
|
||||
};
|
||||
|
||||
let historical_retention = config.inner.historical_retention;
|
||||
let params = config.fsrs_params();
|
||||
let decay = get_decay_from_params(params);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import SwitchRow from "$lib/components/SwitchRow.svelte";
|
||||
|
||||
import GlobalLabel from "./GlobalLabel.svelte";
|
||||
import { commitEditing, fsrsParams, type DeckOptionsState } from "./lib";
|
||||
import { commitEditing, fsrsParams, type DeckOptionsState, ValueTab } from "./lib";
|
||||
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
||||
import Warning from "./Warning.svelte";
|
||||
import ParamsInputRow from "./ParamsInputRow.svelte";
|
||||
|
|
@ -32,6 +32,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
UpdateDeckConfigsMode,
|
||||
} from "@generated/anki/deck_config_pb";
|
||||
import type Modal from "bootstrap/js/dist/modal";
|
||||
import TabbedValue from "./TabbedValue.svelte";
|
||||
import Item from "$lib/components/Item.svelte";
|
||||
import DynamicallySlottable from "$lib/components/DynamicallySlottable.svelte";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
export let openHelpModal: (String) => void;
|
||||
|
|
@ -42,13 +45,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const defaults = state.defaults;
|
||||
const fsrsReschedule = state.fsrsReschedule;
|
||||
const daysSinceLastOptimization = state.daysSinceLastOptimization;
|
||||
const limits = state.deckLimits;
|
||||
|
||||
$: lastOptimizationWarning =
|
||||
$daysSinceLastOptimization > 30 ? tr.deckConfigTimeToOptimize() : "";
|
||||
let desiredRetentionFocused = false;
|
||||
let desiredRetentionEverFocused = false;
|
||||
let optimized = false;
|
||||
const startingDesiredRetention = $config.desiredRetention.toFixed(2);
|
||||
$: if (desiredRetentionFocused) {
|
||||
desiredRetentionEverFocused = true;
|
||||
}
|
||||
|
|
@ -63,7 +66,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
$: computing = computingParams || checkingParams;
|
||||
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
|
||||
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
|
||||
$: roundedRetention = Number(effectiveDesiredRetention.toFixed(2));
|
||||
$: desiredRetentionWarning = getRetentionLongShortWarning(roundedRetention);
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
|
|
@ -85,9 +88,33 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
|
||||
|
||||
// Create tabs for desired retention
|
||||
const desiredRetentionTabs: ValueTab[] = [
|
||||
new ValueTab(
|
||||
tr.deckConfigSharedPreset(),
|
||||
$config.desiredRetention,
|
||||
(value) => ($config.desiredRetention = value!),
|
||||
$config.desiredRetention,
|
||||
null,
|
||||
),
|
||||
new ValueTab(
|
||||
tr.deckConfigDeckOnly(),
|
||||
$limits.desiredRetention ?? null,
|
||||
(value) => ($limits.desiredRetention = value ?? undefined),
|
||||
null,
|
||||
null,
|
||||
),
|
||||
];
|
||||
|
||||
let desiredRetentionValue = $config.desiredRetention;
|
||||
|
||||
// Get the effective desired retention value (deck-specific if set, otherwise config default)
|
||||
$: effectiveDesiredRetention = $limits.desiredRetention ?? $config.desiredRetention;
|
||||
const startingDesiredRetention = effectiveDesiredRetention.toFixed(2);
|
||||
|
||||
$: simulateFsrsRequest = new SimulateFsrsReviewRequest({
|
||||
params: fsrsParams($config),
|
||||
desiredRetention: $config.desiredRetention,
|
||||
desiredRetention: effectiveDesiredRetention,
|
||||
newLimit: $config.newPerDay,
|
||||
reviewLimit: $config.reviewsPerDay,
|
||||
maxInterval: $config.maximumReviewInterval,
|
||||
|
|
@ -301,18 +328,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let simulatorModal: Modal;
|
||||
</script>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.desiredRetention}
|
||||
defaultValue={defaults.desiredRetention}
|
||||
min={0.7}
|
||||
max={0.99}
|
||||
percentage={true}
|
||||
bind:focused={desiredRetentionFocused}
|
||||
>
|
||||
<SettingTitle on:click={() => openHelpModal("desiredRetention")}>
|
||||
{tr.deckConfigDesiredRetention()}
|
||||
</SettingTitle>
|
||||
</SpinBoxFloatRow>
|
||||
<DynamicallySlottable slotHost={Item} api={{}}>
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={desiredRetentionValue}
|
||||
defaultValue={defaults.desiredRetention}
|
||||
min={0.7}
|
||||
max={0.99}
|
||||
percentage={true}
|
||||
bind:focused={desiredRetentionFocused}
|
||||
>
|
||||
<TabbedValue
|
||||
slot="tabs"
|
||||
tabs={desiredRetentionTabs}
|
||||
bind:value={desiredRetentionValue}
|
||||
/>
|
||||
<SettingTitle on:click={() => openHelpModal("desiredRetention")}>
|
||||
{tr.deckConfigDesiredRetention()}
|
||||
</SettingTitle>
|
||||
</SpinBoxFloatRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
|
||||
<Warning warning={desiredRetentionChangeInfo} className={"alert-info two-line"} />
|
||||
<Warning warning={desiredRetentionWarning} className={retentionWarningClass} />
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@
|
|||
<slot />
|
||||
</Col>
|
||||
<Col --col-size={6} breakpoint="xs">
|
||||
<ConfigInput>
|
||||
<SpinBox bind:value {min} {max} {step} {percentage} bind:focused />
|
||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||
</ConfigInput>
|
||||
<Row class="flex-grow-1">
|
||||
<slot name="tabs" />
|
||||
<ConfigInput>
|
||||
<SpinBox bind:value {min} {max} {step} {percentage} bind:focused />
|
||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||
</ConfigInput>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
Loading…
Reference in a new issue