mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Feat/CMRR uses simulate config (#3947)
* Added: simulate_request_to_config * Use SimulateConfig for CMRR * ./check * Fix: ComputingRetention * Use actual cards for optimal_retention
This commit is contained in:
parent
e748cec5d1
commit
ad073ab10c
7 changed files with 117 additions and 207 deletions
|
@ -51,7 +51,7 @@ service SchedulerService {
|
|||
returns (ComputeFsrsParamsResponse);
|
||||
rpc GetOptimalRetentionParameters(GetOptimalRetentionParametersRequest)
|
||||
returns (GetOptimalRetentionParametersResponse);
|
||||
rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest)
|
||||
rpc ComputeOptimalRetention(SimulateFsrsReviewRequest)
|
||||
returns (ComputeOptimalRetentionResponse);
|
||||
rpc SimulateFsrsReview(SimulateFsrsReviewRequest)
|
||||
returns (SimulateFsrsReviewResponse);
|
||||
|
@ -409,16 +409,6 @@ message SimulateFsrsReviewResponse {
|
|||
repeated float daily_time_cost = 4;
|
||||
}
|
||||
|
||||
message ComputeOptimalRetentionRequest {
|
||||
repeated float params = 1;
|
||||
uint32 days_to_simulate = 2;
|
||||
uint32 max_interval = 3;
|
||||
string search = 4;
|
||||
double loss_aversion = 5;
|
||||
repeated float easy_days_percentages = 6;
|
||||
optional uint32 suspend_after_lapse_count = 7;
|
||||
}
|
||||
|
||||
message ComputeOptimalRetentionResponse {
|
||||
float optimal_retention = 1;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
use std::sync::Arc;
|
||||
|
||||
use anki_proto::scheduler::ComputeOptimalRetentionRequest;
|
||||
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
||||
use fsrs::extract_simulator_config;
|
||||
use fsrs::PostSchedulingFn;
|
||||
use fsrs::SimulatorConfig;
|
||||
use fsrs::FSRS;
|
||||
|
||||
use super::simulator::apply_load_balance_and_easy_days;
|
||||
use crate::prelude::*;
|
||||
use crate::revlog::RevlogEntry;
|
||||
use crate::scheduler::states::load_balancer::parse_easy_days_percentages;
|
||||
use crate::search::SortMode;
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct ComputeRetentionProgress {
|
||||
|
@ -21,65 +15,16 @@ pub struct ComputeRetentionProgress {
|
|||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn compute_optimal_retention(
|
||||
&mut self,
|
||||
req: ComputeOptimalRetentionRequest,
|
||||
) -> Result<f32> {
|
||||
pub fn compute_optimal_retention(&mut self, req: SimulateFsrsReviewRequest) -> Result<f32> {
|
||||
let mut anki_progress = self.new_progress_handler::<ComputeRetentionProgress>();
|
||||
let fsrs = FSRS::new(None)?;
|
||||
if req.days_to_simulate == 0 {
|
||||
invalid_input!("no days to simulate")
|
||||
}
|
||||
let revlogs = self
|
||||
.search_cards_into_table(&req.search, SortMode::NoOrder)?
|
||||
.col
|
||||
.storage
|
||||
.get_revlog_entries_for_searched_cards_in_card_order()?;
|
||||
let p = self.get_optimal_retention_parameters(revlogs)?;
|
||||
let learn_span = req.days_to_simulate as usize;
|
||||
let learn_limit = 10;
|
||||
let deck_size = learn_span * learn_limit;
|
||||
let easy_days_percentages = parse_easy_days_percentages(req.easy_days_percentages)?;
|
||||
let next_day_at = self.timing_today()?.next_day_at;
|
||||
let post_scheduling_fn: Option<PostSchedulingFn> =
|
||||
if self.get_config_bool(BoolKey::LoadBalancerEnabled) {
|
||||
Some(PostSchedulingFn(Arc::new(
|
||||
move |card, max_interval, today, due_cnt_per_day, rng| {
|
||||
apply_load_balance_and_easy_days(
|
||||
card.interval,
|
||||
max_interval,
|
||||
today,
|
||||
due_cnt_per_day,
|
||||
rng,
|
||||
next_day_at,
|
||||
&easy_days_percentages,
|
||||
)
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (config, cards) = self.simulate_request_to_config(&req)?;
|
||||
Ok(fsrs
|
||||
.optimal_retention(
|
||||
&SimulatorConfig {
|
||||
deck_size,
|
||||
learn_span: req.days_to_simulate as usize,
|
||||
max_cost_perday: f32::MAX,
|
||||
max_ivl: req.max_interval as f32,
|
||||
first_rating_prob: p.first_rating_prob,
|
||||
review_rating_prob: p.review_rating_prob,
|
||||
learn_limit,
|
||||
review_limit: usize::MAX,
|
||||
new_cards_ignore_review_limit: true,
|
||||
suspend_after_lapses: None,
|
||||
post_scheduling_fn,
|
||||
review_priority_fn: None,
|
||||
learning_step_transitions: p.learning_step_transitions,
|
||||
relearning_step_transitions: p.relearning_step_transitions,
|
||||
state_rating_costs: p.state_rating_costs,
|
||||
learning_step_count: p.learning_step_count,
|
||||
relearning_step_count: p.relearning_step_count,
|
||||
},
|
||||
&config,
|
||||
&req.params,
|
||||
|ip| {
|
||||
anki_progress
|
||||
|
@ -88,7 +33,7 @@ impl Collection {
|
|||
})
|
||||
.is_ok()
|
||||
},
|
||||
None,
|
||||
Some(cards),
|
||||
)?
|
||||
.clamp(0.7, 0.95))
|
||||
}
|
||||
|
|
|
@ -115,10 +115,10 @@ fn create_review_priority_fn(
|
|||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn simulate_review(
|
||||
pub fn simulate_request_to_config(
|
||||
&mut self,
|
||||
req: SimulateFsrsReviewRequest,
|
||||
) -> Result<SimulateFsrsReviewResponse> {
|
||||
req: &SimulateFsrsReviewRequest,
|
||||
) -> Result<(SimulatorConfig, Vec<fsrs::Card>)> {
|
||||
let guard = self.search_cards_into_table(&req.search, SortMode::NoOrder)?;
|
||||
let revlogs = guard
|
||||
.col
|
||||
|
@ -170,7 +170,7 @@ impl Collection {
|
|||
let deck_size = converted_cards.len();
|
||||
let p = self.get_optimal_retention_parameters(revlogs)?;
|
||||
|
||||
let easy_days_percentages = parse_easy_days_percentages(req.easy_days_percentages)?;
|
||||
let easy_days_percentages = parse_easy_days_percentages(&req.easy_days_percentages)?;
|
||||
let next_day_at = self.timing_today()?.next_day_at;
|
||||
|
||||
let post_scheduling_fn: Option<PostSchedulingFn> =
|
||||
|
@ -217,12 +217,21 @@ impl Collection {
|
|||
learning_step_count: p.learning_step_count,
|
||||
relearning_step_count: p.relearning_step_count,
|
||||
};
|
||||
|
||||
Ok((config, converted_cards))
|
||||
}
|
||||
|
||||
pub fn simulate_review(
|
||||
&mut self,
|
||||
req: SimulateFsrsReviewRequest,
|
||||
) -> Result<SimulateFsrsReviewResponse> {
|
||||
let (config, cards) = self.simulate_request_to_config(&req)?;
|
||||
let result = simulate(
|
||||
&config,
|
||||
&req.params,
|
||||
req.desired_retention,
|
||||
None,
|
||||
Some(converted_cards),
|
||||
Some(cards),
|
||||
)?;
|
||||
Ok(SimulateFsrsReviewResponse {
|
||||
accumulated_knowledge_acquisition: result.memorized_cnt_per_day,
|
||||
|
|
|
@ -9,7 +9,6 @@ use anki_proto::generic;
|
|||
use anki_proto::scheduler;
|
||||
use anki_proto::scheduler::ComputeFsrsParamsResponse;
|
||||
use anki_proto::scheduler::ComputeMemoryStateResponse;
|
||||
use anki_proto::scheduler::ComputeOptimalRetentionRequest;
|
||||
use anki_proto::scheduler::ComputeOptimalRetentionResponse;
|
||||
use anki_proto::scheduler::FsrsBenchmarkResponse;
|
||||
use anki_proto::scheduler::FuzzDeltaRequest;
|
||||
|
@ -284,7 +283,7 @@ impl crate::services::SchedulerService for Collection {
|
|||
|
||||
fn compute_optimal_retention(
|
||||
&mut self,
|
||||
input: ComputeOptimalRetentionRequest,
|
||||
input: SimulateFsrsReviewRequest,
|
||||
) -> Result<ComputeOptimalRetentionResponse> {
|
||||
Ok(ComputeOptimalRetentionResponse {
|
||||
optimal_retention: self.compute_optimal_retention(input)?,
|
||||
|
|
|
@ -277,7 +277,7 @@ impl LoadBalancer {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_easy_days_percentages(percentages: Vec<f32>) -> Result<[EasyDay; 7]> {
|
||||
pub(crate) fn parse_easy_days_percentages(percentages: &[f32]) -> Result<[EasyDay; 7]> {
|
||||
if percentages.is_empty() {
|
||||
return Ok([EasyDay::Normal; 7]);
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ pub(crate) fn build_easy_days_percentages(
|
|||
.into_iter()
|
||||
.map(|(dcid, conf)| {
|
||||
let easy_days_percentages =
|
||||
parse_easy_days_percentages(conf.inner.easy_days_percentages)?;
|
||||
parse_easy_days_percentages(&conf.inner.easy_days_percentages)?;
|
||||
Ok((dcid, easy_days_percentages))
|
||||
})
|
||||
.collect()
|
||||
|
|
|
@ -7,13 +7,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
ComputeRetentionProgress,
|
||||
type ComputeParamsProgress,
|
||||
} from "@generated/anki/collection_pb";
|
||||
import {
|
||||
ComputeOptimalRetentionRequest,
|
||||
SimulateFsrsReviewRequest,
|
||||
} from "@generated/anki/scheduler_pb";
|
||||
import { SimulateFsrsReviewRequest } from "@generated/anki/scheduler_pb";
|
||||
import {
|
||||
computeFsrsParams,
|
||||
computeOptimalRetention,
|
||||
evaluateParams,
|
||||
setWantsAbort,
|
||||
} from "@generated/backend";
|
||||
|
@ -26,7 +22,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import GlobalLabel from "./GlobalLabel.svelte";
|
||||
import { commitEditing, fsrsParams, type DeckOptionsState } from "./lib";
|
||||
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||
import Warning from "./Warning.svelte";
|
||||
import ParamsInputRow from "./ParamsInputRow.svelte";
|
||||
import ParamsSearchRow from "./ParamsSearchRow.svelte";
|
||||
|
@ -37,8 +32,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let openHelpModal: (String) => void;
|
||||
export let onPresetChange: () => void;
|
||||
|
||||
const presetName = state.currentPresetName;
|
||||
|
||||
const config = state.currentConfig;
|
||||
const defaults = state.defaults;
|
||||
const fsrsReschedule = state.fsrsReschedule;
|
||||
|
@ -50,13 +43,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let computeParamsProgress: ComputeParamsProgress | undefined;
|
||||
let computingParams = false;
|
||||
let checkingParams = false;
|
||||
let computingRetention = false;
|
||||
|
||||
let optimalRetention = 0;
|
||||
$: if ($presetName) {
|
||||
optimalRetention = 0;
|
||||
}
|
||||
$: computing = computingParams || checkingParams || computingRetention;
|
||||
$: computing = computingParams || checkingParams;
|
||||
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
|
||||
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
|
||||
$: desiredRetentionWarning = getRetentionWarning(
|
||||
|
@ -65,19 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
);
|
||||
$: retentionWarningClass = getRetentionWarningClass(roundedRetention);
|
||||
|
||||
let computeRetentionProgress:
|
||||
| ComputeParamsProgress
|
||||
| ComputeRetentionProgress
|
||||
| undefined;
|
||||
|
||||
const optimalRetentionRequest = new ComputeOptimalRetentionRequest({
|
||||
daysToSimulate: 365,
|
||||
lossAversion: 2.5,
|
||||
});
|
||||
$: if (optimalRetentionRequest.daysToSimulate > 3650) {
|
||||
optimalRetentionRequest.daysToSimulate = 3650;
|
||||
}
|
||||
|
||||
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
|
||||
|
||||
$: simulateFsrsRequest = new SimulateFsrsReviewRequest({
|
||||
|
@ -233,44 +208,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
async function computeRetention(): Promise<void> {
|
||||
if (computingRetention) {
|
||||
await setWantsAbort({});
|
||||
return;
|
||||
}
|
||||
if (state.presetAssignmentsChanged()) {
|
||||
alert(tr.deckConfigPleaseSaveYourChangesFirst());
|
||||
return;
|
||||
}
|
||||
computingRetention = true;
|
||||
computeRetentionProgress = undefined;
|
||||
try {
|
||||
await runWithBackendProgress(
|
||||
async () => {
|
||||
optimalRetentionRequest.maxInterval = $config.maximumReviewInterval;
|
||||
optimalRetentionRequest.params = fsrsParams($config);
|
||||
optimalRetentionRequest.search = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
|
||||
optimalRetentionRequest.easyDaysPercentages =
|
||||
$config.easyDaysPercentages;
|
||||
const resp = await computeOptimalRetention(optimalRetentionRequest);
|
||||
optimalRetention = resp.optimalRetention;
|
||||
computeRetentionProgress = undefined;
|
||||
},
|
||||
(progress) => {
|
||||
if (progress.value.case === "computeRetention") {
|
||||
computeRetentionProgress = progress.value.value;
|
||||
}
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
computingRetention = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: computeParamsProgressString = renderWeightProgress(computeParamsProgress);
|
||||
$: computeRetentionProgressString = renderRetentionProgress(
|
||||
computeRetentionProgress,
|
||||
);
|
||||
$: totalReviews = computeParamsProgress?.reviews ?? undefined;
|
||||
|
||||
function renderWeightProgress(val: ComputeParamsProgress | undefined): String {
|
||||
|
@ -285,22 +223,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
function renderRetentionProgress(
|
||||
val: ComputeRetentionProgress | undefined,
|
||||
): String {
|
||||
if (!val) {
|
||||
return "";
|
||||
}
|
||||
return tr.deckConfigIterations({ count: val.current });
|
||||
}
|
||||
|
||||
function estimatedRetention(retention: number): String {
|
||||
if (!retention) {
|
||||
return "";
|
||||
}
|
||||
return tr.deckConfigPredictedOptimalRetention({ num: retention.toFixed(2) });
|
||||
}
|
||||
|
||||
async function computeAllParams(): Promise<void> {
|
||||
await commitEditing();
|
||||
state.save(UpdateDeckConfigsMode.COMPUTE_ALL_PARAMS);
|
||||
|
@ -390,49 +312,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="m-2">
|
||||
<details>
|
||||
<summary>{tr.deckConfigComputeOptimalRetention()}</summary>
|
||||
|
||||
<SpinBoxRow
|
||||
bind:value={optimalRetentionRequest.daysToSimulate}
|
||||
defaultValue={365}
|
||||
min={1}
|
||||
max={3650}
|
||||
>
|
||||
<SettingTitle on:click={() => openHelpModal("computeOptimalRetention")}>
|
||||
{tr.deckConfigDaysToSimulate()}
|
||||
</SettingTitle>
|
||||
</SpinBoxRow>
|
||||
|
||||
<button
|
||||
class="btn {computingRetention ? 'btn-warning' : 'btn-primary'}"
|
||||
disabled={!computingRetention && computing}
|
||||
on:click={() => computeRetention()}
|
||||
>
|
||||
{#if computingRetention}
|
||||
{tr.actionsCancel()}
|
||||
{:else}
|
||||
{tr.deckConfigComputeButton()}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if optimalRetention}
|
||||
{estimatedRetention(optimalRetention)}
|
||||
{#if optimalRetention - $config.desiredRetention >= 0.01}
|
||||
<Warning
|
||||
warning={tr.deckConfigDesiredRetentionBelowOptimal()}
|
||||
className="alert-warning"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if computingRetention}
|
||||
<div>{computeRetentionProgressString}</div>
|
||||
{/if}
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="m-2">
|
||||
<button class="btn btn-primary" on:click={() => (showSimulator = true)}>
|
||||
{tr.deckConfigFsrsSimulatorExperimental()}
|
||||
|
|
|
@ -16,9 +16,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import { SimulateSubgraph, type Point } from "../graphs/simulator";
|
||||
import * as tr from "@generated/ftl";
|
||||
import { renderSimulationChart } from "../graphs/simulator";
|
||||
import { simulateFsrsReview } from "@generated/backend";
|
||||
import { computeOptimalRetention, simulateFsrsReview } from "@generated/backend";
|
||||
import { runWithBackendProgress } from "@tslib/progress";
|
||||
import type {
|
||||
ComputeOptimalRetentionResponse,
|
||||
SimulateFsrsReviewRequest,
|
||||
SimulateFsrsReviewResponse,
|
||||
} from "@generated/anki/scheduler_pb";
|
||||
|
@ -30,6 +31,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte";
|
||||
import { DeckConfig_Config_LeechAction } from "@generated/anki/deck_config_pb";
|
||||
import EasyDaysInput from "./EasyDaysInput.svelte";
|
||||
import Warning from "./Warning.svelte";
|
||||
import type { ComputeRetentionProgress } from "@generated/anki/collection_pb";
|
||||
|
||||
export let shown = false;
|
||||
export let state: DeckOptionsState;
|
||||
|
@ -53,9 +56,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let suspendLeeches = $config.leechAction == DeckConfig_Config_LeechAction.SUSPEND;
|
||||
let leechThreshold = $config.leechThreshold;
|
||||
|
||||
let optimalRetention: null | number = null;
|
||||
let computingRetention = false;
|
||||
let computeRetentionProgress: ComputeRetentionProgress | undefined = undefined;
|
||||
|
||||
$: daysToSimulate = 365;
|
||||
$: deckSize = 0;
|
||||
$: windowSize = Math.ceil(daysToSimulate / 365);
|
||||
$: processing = simulating || computingRetention;
|
||||
|
||||
function movingAverage(y: number[], windowSize: number): number[] {
|
||||
const result: number[] = [];
|
||||
|
@ -75,14 +83,61 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
return arr1.map((value, index) => value + arr2[index]);
|
||||
}
|
||||
|
||||
async function simulateFsrs(): Promise<void> {
|
||||
let resp: SimulateFsrsReviewResponse | undefined;
|
||||
function estimatedRetention(retention: number): String {
|
||||
if (!retention) {
|
||||
return "";
|
||||
}
|
||||
return tr.deckConfigPredictedOptimalRetention({ num: retention.toFixed(2) });
|
||||
}
|
||||
|
||||
function updateRequest() {
|
||||
simulateFsrsRequest.daysToSimulate = daysToSimulate;
|
||||
simulateFsrsRequest.deckSize = deckSize;
|
||||
simulateFsrsRequest.suspendAfterLapseCount = suspendLeeches
|
||||
? leechThreshold
|
||||
: undefined;
|
||||
simulateFsrsRequest.easyDaysPercentages = easyDayPercentages;
|
||||
}
|
||||
|
||||
function renderRetentionProgress(
|
||||
val: ComputeRetentionProgress | undefined,
|
||||
): String {
|
||||
if (!val) {
|
||||
return "";
|
||||
}
|
||||
return tr.deckConfigIterations({ count: val.current });
|
||||
}
|
||||
|
||||
$: computeRetentionProgressString = renderRetentionProgress(
|
||||
computeRetentionProgress,
|
||||
);
|
||||
|
||||
async function computeRetention() {
|
||||
let resp: ComputeOptimalRetentionResponse | undefined;
|
||||
updateRequest();
|
||||
try {
|
||||
await runWithBackendProgress(
|
||||
async () => {
|
||||
computingRetention = true;
|
||||
resp = await computeOptimalRetention(simulateFsrsRequest);
|
||||
},
|
||||
(progress) => {
|
||||
if (progress.value.case === "computeRetention") {
|
||||
computeRetentionProgress = progress.value.value;
|
||||
}
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
computingRetention = false;
|
||||
if (resp) {
|
||||
optimalRetention = resp.optimalRetention;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function simulateFsrs(): Promise<void> {
|
||||
let resp: SimulateFsrsReviewResponse | undefined;
|
||||
updateRequest();
|
||||
try {
|
||||
await runWithBackendProgress(
|
||||
async () => {
|
||||
|
@ -328,6 +383,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{/if}
|
||||
</details>
|
||||
|
||||
<div class="m-2">
|
||||
<details>
|
||||
<summary>{tr.deckConfigComputeOptimalRetention()}</summary>
|
||||
<button
|
||||
class="btn {computingRetention
|
||||
? 'btn-warning'
|
||||
: 'btn-primary'}"
|
||||
disabled={!computingRetention && computing}
|
||||
on:click={() => computeRetention()}
|
||||
>
|
||||
{#if computingRetention}
|
||||
{tr.actionsCancel()}
|
||||
{:else}
|
||||
{tr.deckConfigComputeButton()}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if optimalRetention}
|
||||
{estimatedRetention(optimalRetention)}
|
||||
{#if optimalRetention - $config.desiredRetention >= 0.01}
|
||||
<Warning
|
||||
warning={tr.deckConfigDesiredRetentionBelowOptimal()}
|
||||
className="alert-warning"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if computingRetention}
|
||||
<div>{computeRetentionProgressString}</div>
|
||||
{/if}
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
|
||||
disabled={computing}
|
||||
|
@ -366,7 +454,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{tr.deckConfigSaveOptionsToPreset()}
|
||||
</button>
|
||||
|
||||
{#if simulating}
|
||||
{#if processing}
|
||||
{tr.actionsProcessing()}
|
||||
{/if}
|
||||
|
||||
|
|
Loading…
Reference in a new issue