import { db } from '../../db/database.js'; import { aiRouter } from '../../ai/router.js'; export class LearnView { constructor(containerId) { this.container = document.getElementById(containerId); this.decks = []; this.activeDeckId = null; this.dueCards = []; this.currentCardIndex = 0; } async init() { this.loadDecks(); window.addEventListener('flashcards-updated', () => { this.loadDecks(); // Don't interrupt if user is actively studying const isStudyMode = this.container.querySelector('.study-mode-container') !== null; if (isStudyMode) return; if (this.activeDeckId) { this.renderDeckDetails(this.activeDeckId); } else if (this.container.innerHTML.trim() !== '') { this.renderOverview(); } }); window.addEventListener('cloud_sync_updated', () => { this.loadDecks(); const isStudyMode = this.container.querySelector('.study-mode-container') !== null; if (isStudyMode) return; // Don't interrupt study if (this.activeDeckId) { this.renderDeckDetails(this.activeDeckId); } else if (this.container.innerHTML.trim() !== '') { this.renderOverview(); } }); } loadDecks() { this.decks = db.getDecks(); } render() { this.renderOverview(); } appAlert(message) { return new Promise(resolve => { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; overlay.innerHTML = `

Hinweis

${message}

`; document.body.appendChild(overlay); const btn = overlay.querySelector('.confirm'); btn.onclick = () => { overlay.classList.add('fade-out'); setTimeout(() => { overlay.remove(); resolve(); }, 300); }; }); } renderOverview() { this.container.style.overflow = ''; this.loadDecks(); let decksHtml = ''; if (this.decks.length === 0) { decksHtml = `
📚

Du hast noch keine Karteikarten.

`; } else { decksHtml = `
${this.decks.map(deck => `
${deck.title.charAt(0).toUpperCase()}

${deck.title}

${deck.total_count} Karten ${deck.due_count > 0 ? `${deck.due_count} fÀllig` : `Alles erledigt`}
`).join('')}
`; } this.container.innerHTML = `
Ziehen zum Aktualisieren

Lernen

${decksHtml}
`; // Pull-to-refresh logic const scrollContainer = document.getElementById('learn-scroll-container'); const ptrIndicator = document.getElementById('ptr-indicator'); if (scrollContainer && ptrIndicator) { let startY = 0; let currentY = 0; let isPulling = false; let threshold = 60; scrollContainer.addEventListener('touchstart', (e) => { if (scrollContainer.scrollTop === 0) { startY = e.touches[0].clientY; isPulling = true; ptrIndicator.style.transition = 'none'; } }, { passive: true }); scrollContainer.addEventListener('touchmove', (e) => { if (!isPulling) return; currentY = e.touches[0].clientY; const diff = currentY - startY; if (diff > 0 && scrollContainer.scrollTop === 0) { const move = Math.min(diff * 0.4, threshold + 20); ptrIndicator.style.height = `${move}px`; ptrIndicator.style.opacity = move / threshold; if (move >= threshold) { ptrIndicator.innerHTML = ' Loslassen zum Aktualisieren'; } else { ptrIndicator.innerHTML = ' Ziehen zum Aktualisieren'; } } }, { passive: true }); scrollContainer.addEventListener('touchend', () => { if (!isPulling) return; isPulling = false; ptrIndicator.style.transition = 'height 0.3s, opacity 0.3s'; const diff = currentY - startY; if (diff * 0.4 >= threshold) { ptrIndicator.innerHTML = ' Lade...'; ptrIndicator.style.height = '40px'; ptrIndicator.style.opacity = '1'; // Trigger global background sync check manually document.dispatchEvent(new Event('visibilitychange')); setTimeout(() => { this.renderOverview(); }, 800); } else { ptrIndicator.style.height = '0px'; ptrIndicator.style.opacity = '0'; } }); } const addBtn = document.getElementById('add-deck-btn'); if (addBtn) { addBtn.addEventListener('click', () => { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; overlay.innerHTML = `

Neuer Stapel

`; document.body.appendChild(overlay); // Auto-focus title setTimeout(() => { const input = overlay.querySelector('#new-deck-title'); if(input) input.focus(); }, 100); overlay.querySelector('.cancel').onclick = () => { overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); }; overlay.querySelector('.confirm').onclick = () => { const title = overlay.querySelector('#new-deck-title').value.trim(); const source = overlay.querySelector('#new-deck-source').value.trim(); const target = overlay.querySelector('#new-deck-target').value.trim(); if (title && source && target) { db.createDeck(title, '', '#8a53e6', source, target); this.renderOverview(); overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); } }; }); } const searchInput = document.getElementById('global-vocab-search'); const searchResults = document.getElementById('vocab-search-results'); if (searchInput) { searchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); if (query.length < 2) { searchResults.style.display = 'none'; searchResults.innerHTML = ''; return; } const results = db.searchFlashcards(query); if (results.length === 0) { searchResults.style.display = 'flex'; searchResults.innerHTML = '
Keine Vokabeln gefunden.
'; return; } searchResults.style.display = 'flex'; searchResults.innerHTML = results.map(card => `
${card.front}
${card.deck_title}
${(card.back || '').split('\n')[0]}
`).join(''); }); } } renderDeckDetails(deckId) { this.container.style.overflow = ''; this.activeDeckId = deckId; const deck = this.decks.find(d => d.id === deckId); if (!deck) return this.renderOverview(); // Re-fetch counts const dueCards = db.getDueCards(deckId); // Fetch all cards for the list const allCards = db.getCardsForDeck(deckId); deck.due_count = dueCards.length; deck.total_count = allCards.length; this.container.innerHTML = `

${deck.title}

${deck.title.charAt(0).toUpperCase()}

${deck.total_count} Karten insgesamt

Neue Karte

Alle Karten (${deck.total_count})

${deck.total_count === 0 ? '
Noch keine Karten in diesem Stapel.
' : `
${allCards.map((card, index) => { // Extract the first part of back for the preview (before the explanation) let previewBack = (card.back || '').split('\n\n**ErklÀrung:**')[0].trim(); if (previewBack.length > 60) previewBack = previewBack.substring(0, 60) + '...'; // Escape quotes and newlines for onclick const safeFront = card.front.replace(/'/g, "\\'").replace(/"/g, """).replace(/\n/g, "\\n").replace(/\r/g, "\\r"); const safeBack = card.back.replace(/'/g, "\\'").replace(/"/g, """).replace(/\n/g, "\\n").replace(/\r/g, "\\r"); return `
${card.front}
${previewBack}
`}).join('')}
`}
`; } async previewCard(cardId, deckId) { // Fetch fresh card data const res = db.exec("SELECT * FROM flashcards WHERE id = ?", [cardId]); if (!res || res.length === 0) return; const card = res[0]; const formatBack = (text) => { if (!text) return ''; let t = text; // Try to center the first line if it's the translation const lines = t.split(/\n/); if (lines.length > 0 && lines[0].trim() !== '' && !lines[0].includes('**')) { lines[0] = `
${lines[0]}
`; t = lines.join('\n'); } t = t.replace(/\\n/g, '
'); t = t.replace(/\n/g, '
'); t = t.replace(/\*\*(.*?)\*\*/g, '$1'); return t; }; const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; overlay.style.zIndex = '999999'; overlay.style.backdropFilter = 'blur(10px)'; // We render a standard flashcard inside the modal overlay.innerHTML = `

Karten-Vorschau (Antippen zum Bearbeiten)

${card.front}
${card.image_url ? `` : ''}
${formatBack(card.back)}
`; document.body.appendChild(overlay); overlay.querySelector('.cancel').onclick = () => { overlay.classList.add('fade-out'); setTimeout(() => { overlay.remove(); this.renderDeckDetails(deckId); }, 300); }; overlay.querySelector('.save').onclick = () => { const frontEl = overlay.querySelector('#edit-front-' + card.id); const backEl = overlay.querySelector('#edit-back-' + card.id); if (frontEl && backEl) { this.handleSaveCard(card, deckId, frontEl.innerText, backEl.innerHTML, overlay); } }; } async handleSaveCard(card, deckId, newFront, htmlBack, overlay) { if (!newFront || newFront.trim() === '') return; let newBack = htmlBack; newBack = newBack.replace(/
]*>(.*?)<\/div>/gi, '$1'); newBack = newBack.replace(/

<\/div>/gi, '\n'); newBack = newBack.replace(/]*>/gi, '\n'); newBack = newBack.replace(/<\/div>/gi, ''); newBack = newBack.replace(//gi, '\n'); newBack = newBack.replace(/]*>(.*?)<\/strong>/gi, '**$1**'); newBack = newBack.replace(/]*>(.*?)<\/b>/gi, '**$1**'); newBack = newBack.replace(/ /g, ' '); newBack = newBack.replace(/</g, '<'); newBack = newBack.replace(/>/g, '>'); newBack = newBack.replace(/&/g, '&'); if (card.front.trim() === newFront.trim()) { db.editFlashcard(card.id, newFront.trim(), newBack); overlay.classList.add('fade-out'); setTimeout(() => { overlay.remove(); this.renderDeckDetails(deckId); }, 300); return; } db.editFlashcard(card.id, newFront.trim(), newBack); this.renderDeckDetails(deckId); const saveBtn = overlay.querySelector('.save'); if(saveBtn) { saveBtn.disabled = true; saveBtn.style.opacity = '0.5'; saveBtn.innerText = 'Generiere...'; } const backEl = overlay.querySelector('.flashcard-back'); if (backEl) { const loader = document.createElement('div'); loader.id = 'ai-regenerate-loader'; loader.style = 'position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(10,10,12,0.85); display: flex; flex-direction: column; align-items:center; justify-content:center; z-index: 10; color: var(--primary); font-weight: 500; font-size: 14px; backdrop-filter: blur(4px);'; loader.innerHTML = `
✹
KI generiert neue Karte...
`; backEl.appendChild(loader); const inner = overlay.querySelector('.flashcard-inner'); if (inner && !inner.classList.contains('flipped')) { inner.classList.add('flipped'); } } try { const deck = this.decks.find(d => d.id === deckId); const sourceLang = deck?.source_lang || 'die jeweilige Sprache'; const targetLang = deck?.target_lang || 'die Zielsprache'; const prompt = `Du bist ein KI-Tutor. Generiere die RĂŒckseite fĂŒr eine Vokabelkarte. Stapel-Sprachen: Wort in ${sourceLang} -> Übersetzung/ErklĂ€rung in ${targetLang}. Wort: "${newFront.trim()}" WICHTIGE REGELN: 1. "translation" MUSS zwingend die reine, korrekte Übersetzung in ${targetLang} sein. NICHT LEER LASSEN! 2. "explanation" MUSS in ${targetLang} sein und extrem kurz (maximal 15 Wörter!). 3. "example" MUSS ein sinnvoller Beispielsatz in ${targetLang} sein. Gib AUSSCHLIESSLICH ein valides JSON-Objekt zurĂŒck, ohne Markdown-Formatierung wie \`\`\`json. Format: { "translation": "Die kurze Übersetzung", "explanation": "Eine sehr kurze ErklĂ€rung", "example": "Ein spezifischer Beispielsatz", "search_query": "Ein simpler englischer Suchbegriff fĂŒr ein passendes Bild" }`; const aiResponse = await aiRouter.sendMessage(prompt, { history: [] }); let jsonStr = aiResponse; if (jsonStr.includes('\`\`\`json')) { jsonStr = jsonStr.split('\`\`\`json')[1].split('\`\`\`')[0]; } else if (jsonStr.includes('\`\`\`')) { jsonStr = jsonStr.split('\`\`\`')[1].split('\`\`\`')[0]; } const data = JSON.parse(jsonStr.trim()); const aiBack = `${data.translation}\n\n**ErklĂ€rung:** ${data.explanation}\n\n**Beispiel:** ${data.example}`; let newImageUrl = null; if (data.search_query) { const encodedQuery = encodeURIComponent(data.search_query + ' single photo, clear focus, high quality'); newImageUrl = `https://tse1.mm.bing.net/th?q=${encodedQuery}&w=800&h=1000&c=7&rs=1`; } db.editFlashcard(card.id, newFront.trim(), aiBack); if (newImageUrl) { db.exec("UPDATE flashcards SET image_url = ? WHERE id = ?", [newImageUrl, card.id]); } overlay.classList.add('fade-out'); setTimeout(() => { overlay.remove(); this.renderDeckDetails(deckId); }, 300); } catch (e) { console.error("Failed to regenerate card:", e); const loader = overlay.querySelector('#ai-regenerate-loader'); if (loader) loader.remove(); const saveBtn = overlay.querySelector('.save'); if (saveBtn) { saveBtn.disabled = false; saveBtn.style.opacity = '1'; saveBtn.innerText = 'Speichern'; } } } renameDeck(deckId) { const deck = this.decks.find(d => d.id === deckId); if (!deck) return; const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; overlay.innerHTML = `

Stapel bearbeiten

`; document.body.appendChild(overlay); overlay.querySelector('.cancel').onclick = () => { overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); }; overlay.querySelector('.confirm').onclick = () => { const title = overlay.querySelector('#edit-deck-title').value.trim(); const source = overlay.querySelector('#edit-deck-source').value.trim(); const target = overlay.querySelector('#edit-deck-target').value.trim(); if (title) { if (title !== deck.title) { db.updateDeckTitle(deckId, title); } db.updateDeckLanguages(deckId, source, target); this.renderDeckDetails(deckId); overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); } }; } async deleteDeck(deckId) { if (await window.appConfirm("Möchtest du diesen Stapel wirklich löschen? Alle Karten darin gehen unwiderruflich verloren!")) { db.deleteDeck(deckId); this.renderOverview(); } } async deleteCard(cardId, deckId) { if (await window.appConfirm("Möchtest du diese Karte wirklich löschen?")) { db.deleteFlashcard(cardId); this.renderDeckDetails(deckId); } } async editCard(cardId, deckId, currentFront, currentBack) { const newFront = await window.appPrompt("Bearbeite die Vorderseite (Frage):", currentFront); if (newFront === null || newFront.trim() === "") return; // We only want to edit the raw back, not the HTML/Markdown formatting that was appended (ErklÀrung, Beispiel) if possible. // But since currentBack might already contain everything, we'll let them edit the whole thing. // For a better UX in the future, we could split it, but for now we let them edit the text. let rawBack = currentBack; if (currentBack.includes('\\n\\n**ErklÀrung:**')) { // Just extract the raw back for simple editing if we want to be smart, but wait, if they edit it, they lose the explanation? // No, they might want to edit the explanation too. We just pass the whole string. // But prompt doesn't support multiline. We should use a custom modal for this. } // Custom Modal for editing return new Promise(resolve => { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; const unescapedFront = currentFront.replace(/"/g, '"'); const unescapedBack = currentBack.replace(/"/g, '"').replace(/\\n/g, '\n'); overlay.innerHTML = `

Karte bearbeiten

`; document.body.appendChild(overlay); const inputFront = overlay.querySelector('#edit-front'); const inputBack = overlay.querySelector('#edit-back'); inputFront.focus(); const save = () => { const valF = inputFront.value.trim(); const valB = inputBack.value.trim(); if (valF && valB) { db.editFlashcard(cardId, valF, valB); this.renderDeckDetails(deckId); } overlay.classList.add('fade-out'); setTimeout(() => { overlay.remove(); resolve(); }, 300); }; overlay.querySelector('.confirm').onclick = save; overlay.querySelector('.cancel').onclick = () => { overlay.classList.add('fade-out'); setTimeout(() => { overlay.remove(); resolve(); }, 300); }; }); } async addCard(deckId) { if (document.activeElement) { document.activeElement.blur(); } const frontEl = document.getElementById('new-card-front'); const backEl = document.getElementById('new-card-back'); const addBtn = document.getElementById('add-card-btn'); const front = frontEl.value.trim(); const originalBack = backEl.value.trim(); if (!front || !originalBack) { this.appAlert('Bitte fĂŒlle Vorder- und RĂŒckseite aus.'); return; } // Set Loading State const originalBtnContent = addBtn.innerHTML; addBtn.innerHTML = ' Generiere Karte...'; addBtn.disabled = true; addBtn.style.opacity = '0.7'; // Inject spinner CSS if not exists if (!document.getElementById('spinner-css')) { const style = document.createElement('style'); style.id = 'spinner-css'; style.innerHTML = '@keyframes spin { to { transform: rotate(360deg); } }'; document.head.appendChild(style); } try { const systemInstruction = `Du bist ein Flashcard-Assistent. Der User erstellt eine Karteikarte: Vorderseite = "${front}", RĂŒckseite (Lernwort) = "${originalBack}". Antworte AUSSCHLIESSLICH mit einem validen JSON-Objekt ohne Markdown-Block drumherum, das folgende Felder enthĂ€lt: { "explanation": "1-2 kurze SĂ€tze in der Sprache des LERNWORTS ('${originalBack}'), die genau dieses Wort auf der RĂŒckseite erklĂ€ren. WICHTIG: Markiere das Lernwort (und seine Abwandlungen) IMMER fett mit HTML-Tags (z.B. ${originalBack}).", "example": "Ein prĂ€gnanter Beispielsatz mit dem Lernwort ('${originalBack}'). Das Lernwort MUSS auch hier mit ... fett markiert sein.", "imageQuery": "Ein prĂ€ziser englischer Suchbegriff fĂŒr die Bing-Bildersuche. WICHTIG: Das Bild muss GANZ OFFENSICHTLICH und EINDEUTIG zum Wort passen! Es muss so simpel und klar sein, dass man es SOFORT ohne spezielles Vorwissen erkennt. WĂ€hle bei abstrakten Wörtern ein extrem simples, alltĂ€gliches und leicht verstĂ€ndliches Motiv statt komplexer Kunst oder Metaphern. Das Motiv muss im Zentrum stehen. FĂŒge IMMER 'single photo, clear focus, high quality' am Ende hinzu. ABSOLUT VERBOTEN: Grafiken, Text, Collagen, Cartoons. (Beispiel fĂŒr 'Hund': 'a cute dog sitting on grass single photo clear focus', Beispiel fĂŒr 'Zeit': 'a classic alarm clock single photo clear focus')." }`; // We send a short empty prompt because the instruction handles everything // Wir nutzen Llama 3.3 70B ĂŒber Groq als Sweetspot fĂŒr Kompetenz und Geschwindigkeit const responseStr = await aiRouter.sendMessage("Generate JSON", { systemInstruction, extendedThinking: false, model: 'groq-llama-3.3-70b-versatile' }); // Parse JSON let data; try { const jsonMatch = responseStr.match(/\{[\s\S]*\}/); if (jsonMatch) { data = JSON.parse(jsonMatch[0]); } else { data = JSON.parse(responseStr); } } catch (e) { console.error("AI JSON Parse Error:", e, responseStr); data = { explanation: "Fehler bei der KI-Generierung.", example: "Bitte manuell prĂŒfen.", imageQuery: encodeURIComponent(front) }; } const back = `${originalBack}\n\n**ErklĂ€rung:** ${data.explanation}\n\n**Beispiel:** ${data.example}`; const searchTerms = encodeURIComponent(data.imageQuery); const imageUrl = `https://tse1.mm.bing.net/th?q=${searchTerms}&w=800&h=1000&c=7&rs=1`; db.addFlashcard(deckId, front, back, imageUrl); // Clear inputs frontEl.value = ''; backEl.value = ''; this.renderDeckDetails(deckId); // Refresh stats } catch (e) { console.error(e); this.appAlert('Fehler beim KI-Aufruf: ' + e.message); // Restore button addBtn.innerHTML = originalBtnContent; addBtn.disabled = false; addBtn.style.opacity = '1'; } } startStudyDue(deckId) { this.dueCards = db.getDueCards(deckId); if (this.dueCards.length === 0) { this.appAlert('FĂŒr heute hast du alle Karten in diesem Stapel gelernt!'); return; } this.isFreePractice = false; this.currentCardIndex = 0; this.renderStudyMode(); } startStudyAll(deckId) { this.dueCards = db.getAllCards(deckId); if (this.dueCards.length === 0) { this.appAlert('Dieser Stapel ist noch leer.'); return; } this.isFreePractice = true; this.currentCardIndex = 0; this.renderStudyMode(); } renderStudyMode() { if (this.currentCardIndex >= this.dueCards.length) { this.container.innerHTML = `
🎉

Stapel abgeschlossen!

Du hast alle anstehenden Karten gelernt.

`; return; } const card = this.dueCards[this.currentCardIndex]; const hasImage = !!card.image_url && card.image_url !== 'null'; const deck = db.exec("SELECT * FROM flashcard_decks WHERE id = ?", [this.activeDeckId])[0]; const deckName = (deck && deck.title) ? deck.title.toLowerCase() : ''; let lang = 'en-US'; if (deckName.includes('spanisch') || deckName.includes('spanish')) lang = 'es-ES'; else if (deckName.includes('französisch') || deckName.includes('french')) lang = 'fr-FR'; else if (deckName.includes('italienisch') || deckName.includes('italian')) lang = 'it-IT'; else if (deckName.includes('deutsch') || deckName.includes('german')) lang = 'de-DE'; let backgroundHtml = ''; let overlayHtml = ''; if (hasImage) { backgroundHtml = `Background`; overlayHtml = `
`; } else { // Elegant placeholder for cards without an image backgroundHtml = `
`; // Still need a slight dark gradient at the bottom for text readability, but no blur overlayHtml = `
`; } const cardsLeft = this.dueCards.length - this.currentCardIndex - 1; let stackHtml = ''; if (cardsLeft > 1) { stackHtml += `
`; } if (cardsLeft > 0) { stackHtml += `
`; } // Disable scrolling globally for study mode this.container.style.overflow = 'hidden'; this.container.innerHTML = `
${this.currentCardIndex + 1} / ${this.dueCards.length}
${stackHtml}
${backgroundHtml} ${overlayHtml}

${card.front}

Tippe auf die Karte, um die Antwort zu sehen.

`; // Parse the back content for AI generated fields const backContentEl = document.getElementById('flashcard-back-content'); let formattedBack = card.back || ""; let htmlContent = ''; if (formattedBack.includes('**ErklÀrung:**')) { const parts = formattedBack.split('**ErklÀrung:**'); const translation = parts[0].trim(); let rest = parts[1]; let examplePart = ''; if (rest.includes('**Beispiel:**')) { const subParts = rest.split('**Beispiel:**'); rest = subParts[0].trim(); examplePart = subParts[1].trim(); } else { rest = rest.trim(); } // Parse markdown bold const parseBold = (str) => str ? str.replace(/\*\*(.*?)\*\*/g, '$1') : ''; const parsedTranslation = parseBold(translation); const parsedRest = parseBold(rest); const parsedExample = parseBold(examplePart); htmlContent = `
${parsedTranslation}
ErklÀrung
${parsedRest}
${parsedExample ? `
Beispiel
"${parsedExample}"
` : ''}
Bewerte nun, wie gut du die Antwort wusstest.
`; } else { htmlContent = `
${formattedBack}
Bewerte nun, wie gut du die Antwort wusstest.
`; } backContentEl.innerHTML = htmlContent; const cardEl = document.getElementById('current-flashcard'); const innerEl = cardEl.querySelector('.flashcard-inner'); const controls = document.getElementById('feedback-controls'); let isFlipped = false; cardEl.addEventListener('click', () => { if (!isFlipped) { innerEl.classList.add('flipped'); controls.classList.add('visible'); isFlipped = true; } }); } handleFeedback(quality) { const card = this.dueCards[this.currentCardIndex]; if (!this.isFreePractice) { db.updateCardProgress(card.id, quality); } // Anki Intra-Session Learning Steps if (quality === 0) { // Nochmal: Direkt als nĂ€chste Karte card.hasBeenGood = false; this.dueCards.splice(this.currentCardIndex + 1, 0, card); } else if (quality === 1) { // Schwer: Nach den nĂ€chsten 3 Karten einreihen this.dueCards.splice(this.currentCardIndex + 4, 0, card); } else if (quality === 2) { // Gut: Bei NEUEN Karten nach 10 Karten nochmal abfragen (wie der 10-Minuten Schritt in Anki) // Bei Wiederholungskarten (repetitions > 0) oder wenn schonmal 'Gut' geklickt wurde -> fertig fĂŒr heute! const isReviewCard = card.repetitions > 0; if (!isReviewCard && !card.hasBeenGood) { card.hasBeenGood = true; this.dueCards.splice(this.currentCardIndex + 11, 0, card); } } // quality === 3 (Leicht) fliegt immer sofort aus dem aktuellen Durchgang raus. this.currentCardIndex++; this.renderStudyMode(); } destroy() { this.container.innerHTML = ''; } }