move to SRS Algo

This commit is contained in:
aknuth 2024-12-11 21:33:52 +01:00
parent 26518bef56
commit ac69a11db5
3 changed files with 127 additions and 33 deletions

View File

@ -4,7 +4,6 @@ import { HttpClient } from '@angular/common/http';
import { map, Observable, switchMap } from 'rxjs';
export interface Deck {
id: number; // Hinzugefügt
name: string;
images: DeckImage[];
}
@ -16,10 +15,16 @@ export interface DeckImage {
}
export interface Box {
id?:number;
x1:number;
x2:number;
y1:number;
y2:number;
due?: number;
ivl?: number;
factor?: number;
reps?: number;
lapses?: number;
}
export interface BackendBox {
@ -58,7 +63,6 @@ export class DeckService {
getDecks(): Observable<Deck[]> {
return this.http.get<any[]>(this.apiUrl).pipe(
map(decks => decks.map(deck => ({
id: deck.id, // Annahme: Jeder Deck hat eine eindeutige ID
name: deck.name,
images: this.groupImagesByName(deck.images)
})))
@ -76,10 +80,16 @@ export class DeckService {
};
}
imageMap[image.id].boxes.push({
id: image.boxid,
x1: image.x1,
x2: image.x2,
y1: image.y1,
y2: image.y2
y2: image.y2,
due: image.due,
ivl:image.ivl,
factor:image.factor,
reps:image.reps,
lapses:image.lapses
});
});
@ -110,4 +120,8 @@ export class DeckService {
moveImage(imageId: string, targetDeckId: number): Observable<any> {
return this.http.post(`${this.apiUrl}/images/${imageId}/move`, { targetDeckId });
}
updateBox(box: Box): Observable<any> {
return this.http.put(`${this.apiUrl}/boxes/${box.id}`, box);
}
}

View File

@ -14,22 +14,31 @@
Anzeigen
</button>
<!-- Gewusst Button -->
<!-- Nochmal Button -->
<button
(click)="markKnown()"
(click)="markAgain()"
class="bg-orange-500 disabled:bg-orange-200 text-white py-2 px-4 rounded hover:bg-orange-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Nochmal
</button>
<!-- Gut Button -->
<button
(click)="markGood()"
class="bg-blue-500 disabled:bg-blue-200 text-white py-2 px-4 rounded hover:bg-blue-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Gewusst
Gut
</button>
<!-- Nicht gewusst Button -->
<!-- Einfach Button -->
<button
(click)="markUnknown()"
class="bg-red-500 disabled:bg-red-200 text-white py-2 px-4 rounded hover:bg-red-600"
(click)="markEasy()"
class="bg-green-500 disabled:bg-green-200 text-white py-2 px-4 rounded hover:bg-green-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Nicht gewusst
Einfach
</button>
<!-- Nächstes Bild Button -->
@ -43,7 +52,7 @@
</div>
<p class="mt-2">{{ progress }}</p>
<p class="mt-2">Gewusst: {{ knownCount }} | Nicht gewusst: {{ unknownCount }}</p>
<!-- <p class="mt-2">Gewusst: {{ knownCount }} | Nicht gewusst: {{ unknownCount }}</p> -->
<button
(click)="closeTraining()"

View File

@ -1,4 +1,4 @@
// src/app/training.component.ts
// training.component.ts
import { Component, Input, Output, EventEmitter, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Deck, DeckImage, DeckService, Box } from '../deck.service';
import { CommonModule } from '@angular/common';
@ -19,20 +19,18 @@ export class TrainingComponent implements OnInit {
currentImageIndex: number = 0;
currentImageData: DeckImage | null = null;
// Ändere currentBoxIndex zu boxesToReview als Array
currentBoxIndex: number = 0;
boxesToReview: Box[] = [];
boxRevealed: boolean[] = [];
knownCount: number = 0;
unknownCount: number = 0;
isShowingBox: boolean = false;
isTrainingFinished: boolean = false;
constructor(private deckService: DeckService) { }
ngOnInit(): void {
// Initialisiere die boxesToReview basierend auf SRS
this.initializeBoxesToReview();
}
ngAfterViewInit(){
@ -44,6 +42,27 @@ export class TrainingComponent implements OnInit {
}
}
initializeBoxesToReview(): void {
// Filtere alle Boxen, die fällig sind (due <= heute)
const today = this.getTodayInDays();
this.deck.images.forEach(image => {
image.boxes.forEach(box => {
if (box.due === undefined || box.due <= today) {
this.boxesToReview.push(box);
this.boxRevealed.push(false);
}
});
});
// Mische die Boxen
this.boxesToReview = this.shuffleArray(this.boxesToReview);
}
getTodayInDays(): number {
const epoch = new Date(1970, 0, 1); // Anki verwendet UNIX-Epoch
const today = new Date();
return Math.floor((today.getTime() - epoch.getTime()) / (1000 * 60 * 60 * 24));
}
loadImage(imageIndex: number): void {
if (imageIndex >= this.deck.images.length) {
this.endTraining();
@ -51,8 +70,8 @@ export class TrainingComponent implements OnInit {
}
this.currentImageData = this.deck.images[imageIndex];
// Initialisiere boxesToReview mit allen Boxen, gemischt
this.boxesToReview = this.shuffleArray([...this.currentImageData.boxes]);
// Initialisiere boxesToReview mit allen Boxen, die fällig sind, gemischt
this.boxesToReview = this.shuffleArray([...this.currentImageData.boxes].filter(box => box.due! <= this.getTodayInDays()));
this.boxRevealed = new Array(this.boxesToReview.length).fill(false);
this.isShowingBox = false;
this.drawCanvas();
@ -132,23 +151,73 @@ export class TrainingComponent implements OnInit {
this.drawCanvas();
}
markKnown(): void {
this.knownCount++;
// Entferne die aktuelle Box aus boxesToReview, da sie bekannt ist
this.boxesToReview.splice(this.currentBoxIndex, 1);
this.boxRevealed.splice(this.currentBoxIndex, 1);
// Neue Methoden für Anki-Optionen
async markAgain(): Promise<void> {
await this.updateCard('again');
this.nextBox();
}
markUnknown(): void {
this.unknownCount++;
// Behalte die aktuelle Box in der Liste und verschiebe sie an eine zufällige Position am Ende
const box = this.boxesToReview.splice(this.currentBoxIndex, 1)[0];
this.boxesToReview.push(box);
this.boxRevealed.splice(this.currentBoxIndex, 1);
async markGood(): Promise<void> {
await this.updateCard('good');
this.nextBox();
}
async markEasy(): Promise<void> {
await this.updateCard('easy');
this.nextBox();
}
async updateCard(action: 'again' | 'good' | 'easy'): Promise<void> {
if (this.currentBoxIndex >= this.boxesToReview.length) return;
const box = this.boxesToReview[this.currentBoxIndex];
const today = this.getTodayInDays();
let newIvl = box.ivl || 0;
let newFactor = box.factor || 2.5;
let newReps = box.reps || 0;
let newLapses = box.lapses || 0;
switch(action) {
case 'again':
newIvl = 1 / 1440; // weniger als ein Tag, z.B., 1 Minute in Tagen
newReps = 0;
newLapses += 1;
break;
case 'good':
if (newReps === 0) {
newIvl = 1; // nächste Wiederholung am nächsten Tag
} else {
newIvl = newIvl * newFactor;
}
newReps += 1;
break;
case 'easy':
if (newReps === 0) {
newIvl = 4; // nächste Wiederholung in 4 Tagen
} else {
newIvl = newIvl * newFactor * 1.3; // Anki's "easy" kann zu einem leicht erhöhten Intervall führen
}
newReps += 1;
newFactor = newFactor * 1.15; // optional: Anki erhöht den Faktor leicht
break;
}
// Update due
const nextDue = today + Math.floor(newIvl);
// Aktualisiere das Box-Objekt
box.ivl = newIvl;
box.factor = newFactor;
box.reps = newReps;
box.lapses = newLapses;
box.due = nextDue;
// Sende das Update an das Backend
await this.deckService.updateBox(box).toPromise();
}
nextBox(): void {
this.isShowingBox = false;
@ -158,10 +227,12 @@ export class TrainingComponent implements OnInit {
return;
}
if (this.currentBoxIndex >= this.boxesToReview.length) {
if (this.currentBoxIndex >= this.boxesToReview.length - 1) {
this.currentBoxIndex = 0;
} else {
this.currentBoxIndex++;
}
this.boxRevealed[this.currentBoxIndex] = false;
this.drawCanvas();
}
@ -181,7 +252,8 @@ export class TrainingComponent implements OnInit {
endTraining(): void {
this.isTrainingFinished = true;
alert(`Training beendet!\nGewusst: ${this.knownCount}\nNicht gewusst: ${this.unknownCount}`);
//alert(`Training beendet!\nGewusst: ${this.knownCount}\nNicht gewusst: ${this.unknownCount}`);
alert(`Training beendet!`);
this.close.emit();
}
@ -195,4 +267,3 @@ export class TrainingComponent implements OnInit {
return `Bild ${this.currentImageIndex + 1} von ${this.deck.images.length}`;
}
}