Anki/ts/deck-options/FsrsOptions.svelte
RumovZ 14de8451dc
Merging Notetypes on Import (#2612)
* Remember original id when importing notetype

* Reuse notetypes with matching original id

* Add field and template ids

* Enable merging imported notetypes

* Fix test

Note should be updated if the incoming note's notetype is
remapped to the existing note's notetype.
On the other hand, it should be skipped if its notetype id is mapped
to some new notetype.

* Change field and template ids to i32

* Add merge notetypes flag to proto message

* Add dialog for apkg import

* Move HelpModal into components

* Generalize import dialog

* Move SettingTitle into components

* Add help modal to ImportAnkiPackagePage

* Move SwitchRow into components

* Fix backend method import

* Make testable in browser

* Fix broken modal

* Wrap in container and fix margins

* Update commented Anki version of new proto fields

* Check ids when comparing notetype schemas

* Add tooltip for merging notetypes.

* Allow updating notes regardless of mtime

* Gitignore yarn-error.log

* Allow updating notetypes regardless of mtime

* Fix apkg help carousel

* Use i64s for template and field ids

* Add option to omit importing scheduling info

* Restore last settings in apkg import dialog

* Display error when getting metadata in webview

* Update manual links for apkg importing

* Apply suggestions from code review

Co-authored-by: Damien Elmes <dae@users.noreply.github.com>

* Omit schduling -> Import all cards as new cards

* Tweak importing-update-notes-help

* UpdateCondition → ImportAnkiPackageUpdateCondition

* Load keyboard.ftl

* Skip updating dupes in 'update alwyas' case

* Explain more when merging notetypes is required

* "omit scheduling" → "with scheduling"

* Skip updating notetype dupes if 'update always'

* Merge duplicated notetypes from previous imports

* Fix rebase aftermath

* Fix panic when merging

* Clarify 'update notetypes' help

* Mention 'merge notetypes' in the log

* Add a test which covers the previously panicking path

* Use nested ftl messages to ensure consistency

* Make order of merged fields deterministic

* Rewrite test to trigger panic

* Update version comment on new fields
2023-09-09 09:00:55 +10:00

289 lines
8.6 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 {
Progress_ComputeRetention,
type Progress_ComputeWeights,
} from "@tslib/anki/collection_pb";
import { ComputeOptimalRetentionRequest } from "@tslib/anki/scheduler_pb";
import {
computeFsrsWeights,
computeOptimalRetention,
evaluateWeights,
setWantsAbort,
} from "@tslib/backend";
import { runWithBackendProgress } from "@tslib/progress";
import TitledContainer from "components/TitledContainer.svelte";
import ConfigInput from "../components/ConfigInput.svelte";
import RevertButton from "../components/RevertButton.svelte";
import SettingTitle from "../components/SettingTitle.svelte";
import type { DeckOptionsState } from "./lib";
import WeightsInputRow from "./WeightsInputRow.svelte";
export let state: DeckOptionsState;
const config = state.currentConfig;
let computeWeightsProgress: Progress_ComputeWeights | undefined;
let customSearch = "";
let computing = false;
let computeRetentionProgress:
| Progress_ComputeWeights
| Progress_ComputeRetention
| undefined;
const computeOptimalRequest = new ComputeOptimalRetentionRequest({
deckSize: 10000,
daysToSimulate: 365,
maxSecondsOfStudyPerDay: 1800,
maxInterval: 36500,
recallSecs: 10,
forgetSecs: 50,
learnSecs: 20,
});
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;
}
$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: ${resp.rmse.toFixed(3)}`,
),
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 () => {
computeOptimalRequest.weights = $config.fsrsWeights;
const resp = await computeOptimalRetention(computeOptimalRequest);
$config.desiredRetention = resp.optimalRetention;
if (computeRetentionProgress) {
computeRetentionProgress.current =
computeRetentionProgress.total;
}
},
(progress) => {
if (progress.value.case === "computeRetention") {
computeRetentionProgress = progress.value.value;
}
},
);
} finally {
computing = false;
}
}
$: computeWeightsProgressString = renderWeightProgress(computeWeightsProgress);
$: computeRetentionProgressString = renderRetentionProgress(
computeRetentionProgress,
);
function renderWeightProgress(val: Progress_ComputeWeights | undefined): String {
if (!val || !val.total) {
return "";
}
let pct = ((val.current / val.total) * 100).toFixed(2);
pct = `${pct}%`;
if (val instanceof Progress_ComputeRetention) {
return pct;
} else {
return `${pct} of ${val.revlogEntries} reviews`;
}
}
function renderRetentionProgress(
val: Progress_ComputeRetention | undefined,
): String {
if (!val || !val.total) {
return "";
}
const pct = ((val.current / val.total) * 100).toFixed(2);
return `${pct}%`;
}
</script>
<TitledContainer title={"FSRS"}>
<WeightsInputRow
bind:value={$config.fsrsWeights}
defaultValue={[
0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05,
0.34, 1.26, 0.29, 2.61,
]}
>
<SettingTitle>Weights</SettingTitle>
</WeightsInputRow>
<div>Optimal retention</div>
<ConfigInput>
<input type="number" bind:value={$config.desiredRetention} />
<RevertButton
slot="revert"
bind:value={$config.desiredRetention}
defaultValue={0.9}
/>
</ConfigInput>
<div class="mb-3" />
<div class="bordered">
<b>Optimize weights</b>
<br />
<input
bind:value={customSearch}
placeholder="Search; leave blank for all cards using this preset"
class="w-100 mb-1"
/>
<button
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
on:click={() => computeWeights()}
>
{#if computing}
Cancel
{:else}
Compute
{/if}
</button>
<button
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
on:click={() => checkWeights()}
>
{#if computing}
Cancel
{:else}
Check
{/if}
</button>
<div>{computeWeightsProgressString}</div>
</div>
<div class="bordered">
<b>Calculate optimal retention</b>
<br />
Deck size:
<br />
<input type="number" bind:value={computeOptimalRequest.deckSize} />
<br />
Days to simulate
<br />
<input type="number" bind:value={computeOptimalRequest.daysToSimulate} />
<br />
Max seconds of study per day:
<br />
<input
type="number"
bind:value={computeOptimalRequest.maxSecondsOfStudyPerDay}
/>
<br />
Maximum interval:
<br />
<input type="number" bind:value={computeOptimalRequest.maxInterval} />
<br />
Seconds to recall a card:
<br />
<input type="number" bind:value={computeOptimalRequest.recallSecs} />
<br />
Seconds to forget a card:
<br />
<input type="number" bind:value={computeOptimalRequest.forgetSecs} />
<br />
Seconds to learn a card:
<br />
<input type="number" bind:value={computeOptimalRequest.learnSecs} />
<br />
<button
class="btn {computing ? 'btn-warning' : 'btn-primary'}"
on:click={() => computeRetention()}
>
{#if computing}
Cancel
{:else}
Compute
{/if}
</button>
<div>{computeRetentionProgressString}</div>
</div>
</TitledContainer>
<style>
.bordered {
border: 1px solid #777;
padding: 1em;
margin-bottom: 2px;
}
</style>