351 lines
11 KiB
Svelte
351 lines
11 KiB
Svelte
<!--
|
|
Copyright: Ankitects Pty Ltd and contributors
|
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
-->
|
|
<script lang="ts">
|
|
import {
|
|
ComputeRetentionProgress,
|
|
type ComputeWeightsProgress,
|
|
} from "@tslib/anki/collection_pb";
|
|
import { OptimalRetentionParameters } from "@tslib/anki/scheduler_pb";
|
|
import {
|
|
computeFsrsWeights,
|
|
computeOptimalRetention,
|
|
evaluateWeights,
|
|
getOptimalRetentionParameters,
|
|
setWantsAbort,
|
|
} from "@tslib/backend";
|
|
import * as tr from "@tslib/ftl";
|
|
import { runWithBackendProgress } from "@tslib/progress";
|
|
|
|
import SettingTitle from "../components/SettingTitle.svelte";
|
|
import type { DeckOptionsState } from "./lib";
|
|
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
|
import Warning from "./Warning.svelte";
|
|
import WeightsInputRow from "./WeightsInputRow.svelte";
|
|
|
|
export let state: DeckOptionsState;
|
|
|
|
const config = state.currentConfig;
|
|
const defaults = state.defaults;
|
|
|
|
let computeWeightsProgress: ComputeWeightsProgress | undefined;
|
|
let computeWeightsWarning = "";
|
|
let customSearch = "";
|
|
let computing = false;
|
|
|
|
let computeRetentionProgress:
|
|
| ComputeWeightsProgress
|
|
| ComputeRetentionProgress
|
|
| undefined;
|
|
|
|
let optimalParams = new OptimalRetentionParameters({});
|
|
async function computeWeights(): Promise<void> {
|
|
if (computing) {
|
|
await setWantsAbort({});
|
|
return;
|
|
}
|
|
computing = true;
|
|
try {
|
|
await runWithBackendProgress(
|
|
async () => {
|
|
const search = customSearch ?? `preset:"${state.getCurrentName()}"`;
|
|
const resp = await computeFsrsWeights({
|
|
search,
|
|
});
|
|
if (computeWeightsProgress) {
|
|
computeWeightsProgress.current = computeWeightsProgress.total;
|
|
}
|
|
if (resp.fsrsItems < 1000) {
|
|
computeWeightsWarning = tr.deckConfigLimitedHistory({
|
|
count: resp.fsrsItems,
|
|
});
|
|
} else {
|
|
computeWeightsWarning = "";
|
|
}
|
|
$config.fsrsWeights = resp.weights;
|
|
},
|
|
(progress) => {
|
|
if (progress.value.case === "computeWeights") {
|
|
computeWeightsProgress = progress.value.value;
|
|
}
|
|
},
|
|
);
|
|
} finally {
|
|
computing = false;
|
|
}
|
|
}
|
|
|
|
async function checkWeights(): Promise<void> {
|
|
if (computing) {
|
|
await setWantsAbort({});
|
|
return;
|
|
}
|
|
computing = true;
|
|
try {
|
|
await runWithBackendProgress(
|
|
async () => {
|
|
const search = customSearch ?? `preset:"${state.getCurrentName()}"`;
|
|
const resp = await evaluateWeights({
|
|
weights: $config.fsrsWeights,
|
|
search,
|
|
});
|
|
if (computeWeightsProgress) {
|
|
computeWeightsProgress.current = computeWeightsProgress.total;
|
|
}
|
|
setTimeout(
|
|
() =>
|
|
alert(
|
|
`Log loss: ${resp.logLoss.toFixed(
|
|
3,
|
|
)}, RMSE(bins): ${resp.rmseBins.toFixed(
|
|
3,
|
|
)}. ${tr.deckConfigSmallerIsBetter()}`,
|
|
),
|
|
200,
|
|
);
|
|
},
|
|
(progress) => {
|
|
if (progress.value.case === "computeWeights") {
|
|
computeWeightsProgress = progress.value.value;
|
|
}
|
|
},
|
|
);
|
|
} finally {
|
|
computing = false;
|
|
}
|
|
}
|
|
|
|
async function computeRetention(): Promise<void> {
|
|
if (computing) {
|
|
await setWantsAbort({});
|
|
return;
|
|
}
|
|
computing = true;
|
|
try {
|
|
await runWithBackendProgress(
|
|
async () => {
|
|
const resp = await computeOptimalRetention({
|
|
params: optimalParams,
|
|
weights: $config.fsrsWeights,
|
|
});
|
|
$config.desiredRetention = resp.optimalRetention;
|
|
if (computeRetentionProgress) {
|
|
computeRetentionProgress.current =
|
|
computeRetentionProgress.total;
|
|
}
|
|
},
|
|
(progress) => {
|
|
if (progress.value.case === "computeRetention") {
|
|
computeRetentionProgress = progress.value.value;
|
|
}
|
|
},
|
|
);
|
|
} finally {
|
|
computing = false;
|
|
}
|
|
}
|
|
|
|
async function getRetentionParams(): Promise<void> {
|
|
if (computing) {
|
|
return;
|
|
}
|
|
computing = true;
|
|
try {
|
|
// await
|
|
const resp = await getOptimalRetentionParameters({
|
|
search: `preset:"${state.getCurrentName()}"`,
|
|
});
|
|
optimalParams = resp.params!;
|
|
optimalParams.maxInterval = $config.maximumReviewInterval;
|
|
} finally {
|
|
computing = false;
|
|
}
|
|
}
|
|
|
|
$: computeWeightsProgressString = renderWeightProgress(computeWeightsProgress);
|
|
$: computeRetentionProgressString = renderRetentionProgress(
|
|
computeRetentionProgress,
|
|
);
|
|
|
|
function renderWeightProgress(val: ComputeWeightsProgress | undefined): String {
|
|
if (!val || !val.total) {
|
|
return "";
|
|
}
|
|
let pct = ((val.current / val.total) * 100).toFixed(2);
|
|
pct = `${pct}%`;
|
|
if (val instanceof ComputeRetentionProgress) {
|
|
return pct;
|
|
} else {
|
|
return `${pct} of ${val.fsrsItems} reviews`;
|
|
}
|
|
}
|
|
|
|
function renderRetentionProgress(
|
|
val: ComputeRetentionProgress | undefined,
|
|
): String {
|
|
if (!val || !val.total) {
|
|
return "";
|
|
}
|
|
const pct = ((val.current / val.total) * 100).toFixed(2);
|
|
return `${pct}%`;
|
|
}
|
|
</script>
|
|
|
|
<SpinBoxFloatRow
|
|
bind:value={$config.desiredRetention}
|
|
defaultValue={defaults.desiredRetention}
|
|
min={0.8}
|
|
max={0.97}
|
|
>
|
|
<SettingTitle>
|
|
{tr.deckConfigDesiredRetention()}
|
|
</SettingTitle>
|
|
</SpinBoxFloatRow>
|
|
|
|
<div class="ms-1 me-1">
|
|
<WeightsInputRow bind:value={$config.fsrsWeights} defaultValue={[]}>
|
|
<SettingTitle>{tr.deckConfigWeights()}</SettingTitle>
|
|
</WeightsInputRow>
|
|
</div>
|
|
|
|
<div class="m-2">
|
|
<details>
|
|
<summary>{tr.deckConfigComputeOptimalWeights()}</summary>
|
|
<input
|
|
bind:value={customSearch}
|
|
placeholder={tr.deckConfigComputeWeightsSearch()}
|
|
class="w-100 mb-1"
|
|
/>
|
|
<button
|
|
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
|
|
on:click={() => computeWeights()}
|
|
>
|
|
{#if computing}
|
|
{tr.actionsCancel()}
|
|
{:else}
|
|
{tr.deckConfigComputeButton()}
|
|
{/if}
|
|
</button>
|
|
<button
|
|
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
|
|
on:click={() => checkWeights()}
|
|
>
|
|
{#if computing}
|
|
{tr.actionsCancel()}
|
|
{:else}
|
|
{tr.deckConfigAnalyzeButton()}
|
|
{/if}
|
|
</button>
|
|
{#if computing}<div>{computeWeightsProgressString}</div>{/if}
|
|
<Warning warning={computeWeightsWarning} />
|
|
</details>
|
|
</div>
|
|
|
|
<div class="m-2">
|
|
<details>
|
|
<summary>{tr.deckConfigComputeOptimalRetention()}</summary>
|
|
|
|
Deck size:
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.deckSize} />
|
|
<br />
|
|
|
|
Days to simulate
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.daysToSimulate} />
|
|
<br />
|
|
|
|
Max seconds of study per day:
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.maxSecondsOfStudyPerDay} />
|
|
<br />
|
|
|
|
Seconds to forget a card (again):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.forgetSecs} />
|
|
<br />
|
|
|
|
Seconds to recall a card (hard):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.recallSecsHard} />
|
|
<br />
|
|
|
|
Seconds to recall a card (good):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.recallSecsGood} />
|
|
<br />
|
|
|
|
Seconds to recall a card (easy):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.recallSecsEasy} />
|
|
<br />
|
|
|
|
Seconds to learn a card:
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.learnSecs} />
|
|
<br />
|
|
|
|
First rating probability (again):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.firstRatingProbabilityAgain} />
|
|
<br />
|
|
|
|
First rating probability (hard):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.firstRatingProbabilityHard} />
|
|
<br />
|
|
|
|
First rating probability (good):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.firstRatingProbabilityGood} />
|
|
<br />
|
|
|
|
First rating probability (easy):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.firstRatingProbabilityEasy} />
|
|
<br />
|
|
|
|
Review rating probability (hard):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.reviewRatingProbabilityHard} />
|
|
<br />
|
|
|
|
Review rating probability (good):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.reviewRatingProbabilityGood} />
|
|
<br />
|
|
|
|
Review rating probability (easy):
|
|
<br />
|
|
<input type="number" bind:value={optimalParams.reviewRatingProbabilityEasy} />
|
|
<br />
|
|
|
|
<button
|
|
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
|
|
on:click={() => getRetentionParams()}
|
|
>
|
|
{#if computing}
|
|
{tr.actionsCancel()}
|
|
{:else}
|
|
{tr.deckConfigGetParams()}
|
|
{/if}
|
|
</button>
|
|
|
|
<button
|
|
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
|
|
on:click={() => computeRetention()}
|
|
>
|
|
{#if computing}
|
|
{tr.actionsCancel()}
|
|
{:else}
|
|
{tr.deckConfigComputeButton()}
|
|
{/if}
|
|
</button>
|
|
<div>{computeRetentionProgressString}</div>
|
|
</details>
|
|
</div>
|
|
|
|
<style>
|
|
</style>
|