Anki/ts/card-info/CardStats.svelte
Damien Elmes 5004cd332b
Integrate FSRS into Anki (#2654)
* Pack FSRS data into card.data

* Update FSRS card data when preset or weights change

+ Show FSRS stats in card stats

* Show a warning when there's a limited review history

* Add some translations; tweak UI

* Fix default requested retention

* Add browser columns, fix calculation of R

* Property searches

eg prop:d>0.1

* Integrate FSRS into reviewer

* Warn about long learning steps

* Hide minimum interval when FSRS is on

* Don't apply interval multiplier to FSRS intervals

* Expose memory state to Python

* Don't set memory state on new cards

* Port Jarret's new tests; add some helpers to make tests more compact

https://github.com/open-spaced-repetition/fsrs-rs/pull/64

* Fix learning cards not being given memory state

* Require update to v3 scheduler

* Don't exclude single learning step when calculating memory state

* Use relearning step when learning steps unavailable

* Update docstring

* fix single_card_revlog_to_items (#2656)

* not need check the review_kind for unique_dates

* add email address to CONTRIBUTORS

* fix last first learn & keep early review

* cargo fmt

* cargo clippy --fix

* Add Jarrett to about screen

* Fix fsrs_memory_state being initialized to default in get_card()

* Set initial memory state on graduate

* Update to latest FSRS

* Fix experiment.log being empty

* Fix broken colpkg imports

Introduced by "Update FSRS card data when preset or weights change"

* Update memory state during (re)learning; use FSRS for graduating intervals

* Reset memory state when cards are manually rescheduled as new

* Add difficulty graph; hide eases when FSRS enabled

* Add retrievability graph

* Derive memory_state from revlog when it's missing and shouldn't be

---------

Co-authored-by: Jarrett Ye <jarrett.ye@outlook.com>
2023-09-16 16:09:26 +10:00

159 lines
4.9 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 type { CardStatsResponse } from "@tslib/anki/stats_pb";
import * as tr2 from "@tslib/ftl";
import { DAY, timeSpan, Timestamp } from "@tslib/time";
export let stats: CardStatsResponse;
function dateString(timestamp: bigint): string {
return new Timestamp(Number(timestamp)).dateString();
}
interface StatsRow {
label: string;
value: string | number | bigint;
}
function rowsFromStats(stats: CardStatsResponse): StatsRow[] {
const statsRows: StatsRow[] = [];
statsRows.push({ label: tr2.cardStatsAdded(), value: dateString(stats.added) });
if (stats.firstReview != null) {
statsRows.push({
label: tr2.cardStatsFirstReview(),
value: dateString(stats.firstReview),
});
}
if (stats.latestReview != null) {
statsRows.push({
label: tr2.cardStatsLatestReview(),
value: dateString(stats.latestReview),
});
}
if (stats.dueDate != null) {
statsRows.push({
label: tr2.statisticsDueDate(),
value: dateString(stats.dueDate),
});
}
if (stats.duePosition != null) {
statsRows.push({
label: tr2.cardStatsNewCardPosition(),
value: stats.duePosition,
});
}
if (stats.interval) {
statsRows.push({
label: tr2.cardStatsInterval(),
value: timeSpan(stats.interval * DAY),
});
}
if (stats.fsrsMemoryState) {
let stability = timeSpan(
stats.fsrsMemoryState.stability * 86400,
false,
false,
);
if (stats.fsrsMemoryState.stability > 31) {
const nativeStability = stats.fsrsMemoryState.stability.toFixed(0);
stability += ` (${nativeStability})`;
}
statsRows.push({
label: tr2.cardStatsFsrsStability(),
value: stability,
});
const difficulty = (
((stats.fsrsMemoryState.difficulty - 1.0) / 9.0) *
100.0
).toFixed(0);
statsRows.push({
label: tr2.cardStatsFsrsDifficulty(),
value: `${difficulty}%`,
});
if (stats.fsrsRetrievability) {
const retrievability = (stats.fsrsRetrievability * 100).toFixed(0);
statsRows.push({
label: tr2.cardStatsFsrsRetrievability(),
value: `${retrievability}%`,
});
}
} else {
if (stats.ease) {
statsRows.push({
label: tr2.cardStatsEase(),
value: `${stats.ease / 10}%`,
});
}
}
statsRows.push({ label: tr2.cardStatsReviewCount(), value: stats.reviews });
statsRows.push({ label: tr2.cardStatsLapseCount(), value: stats.lapses });
if (stats.totalSecs) {
statsRows.push({
label: tr2.cardStatsAverageTime(),
value: timeSpan(stats.averageSecs),
});
statsRows.push({
label: tr2.cardStatsTotalTime(),
value: timeSpan(stats.totalSecs),
});
}
statsRows.push({ label: tr2.cardStatsCardTemplate(), value: stats.cardType });
statsRows.push({ label: tr2.cardStatsNoteType(), value: stats.notetype });
statsRows.push({ label: tr2.cardStatsDeckName(), value: stats.deck });
statsRows.push({ label: tr2.cardStatsCardId(), value: stats.cardId });
statsRows.push({ label: tr2.cardStatsNoteId(), value: stats.noteId });
if (stats.customData) {
let value: string;
try {
const obj = JSON.parse(stats.customData);
value = Object.entries(obj)
.map(([k, v]) => `${k}=${v}`)
.join(" ");
} catch (exc) {
value = stats.customData;
}
statsRows.push({
label: tr2.cardStatsCustomData(),
value: value,
});
}
return statsRows;
}
let statsRows: StatsRow[];
$: statsRows = rowsFromStats(stats);
</script>
<table class="stats-table align-start">
{#each statsRows as row}
<tr>
<th class="align-start">{row.label}</th>
<td>{row.value}</td>
</tr>
{/each}
</table>
<style>
.stats-table {
width: 100%;
border-spacing: 1em 0;
border-collapse: collapse;
}
.align-start {
text-align: start;
}
</style>