Add Wortbaukasten, Preposition+Case trainer, and Listening Dictation modules

This commit is contained in:
chrisf committed 2026-10-21 04:53:52 +00:00
1 parent e636350940
commit 5f013dbdfe
5 files changed
+1646 -23

No files matched your search

+807
View File
@@ -25,6 +25,26 @@ class WortKraftApp {
this.spotlightWords = [];
this.pendingTranslations = new Set();
// 1) Wortbaukasten (Compound & Prefix Lab) state
this.builderSubmode = "prefixes"; // 'prefixes' | 'compounds' | 'quiz'
this.selectedVerbRoot = "nehmen";
this.builderQuizIndex = 0;
this.builderQuizResult = null;
// 2) Verben mit Präposition (Rektion & Da-/Wo-) state
this.prepSubmode = "drill"; // 'drill' | 'dawo' | 'reference'
this.prepIndex = 0;
this.prepResult = null;
this.daWoIndex = 0;
this.daWoResult = null;
// 3) Hörtraining (Listening Dictation & Shadowing) state
this.listeningIndex = 0;
this.listeningShowHint = false;
this.listeningResult = null;
this.speechResult = null;
this.isRecordingMic = false;
this.loadState();
this.hoverTranslateEnabled = this.settings.hoverTranslateEnabled !== false;
this.initScenarioStates();
@@ -477,6 +497,12 @@ class WortKraftApp {
} else if (viewName === "trainer") {
if (!this.currentCard) this.pickNextCard();
this.renderTrainerStage();
} else if (viewName === "builder") {
this.renderBuilderStage();
} else if (viewName === "prepositions") {
this.renderPrepositionsStage();
} else if (viewName === "listening") {
this.renderListeningStage();
} else if (viewName === "reader") {
this.renderReaderSidebar();
this.renderReaderMain();
@@ -1924,6 +1950,787 @@ class WortKraftApp {
.replace(/\n/g, " ");
}
// === 1) WORTBAUKASTEN (COMPOUND WORD & PREFIX LAB) ===
setBuilderSubmode(submode) {
this.builderSubmode = submode;
this.builderQuizResult = null;
["prefixes", "compounds", "quiz"].forEach(m => {
const btn = document.getElementById(`builder-tab-${m}`);
if (btn) btn.classList.toggle("active", m === submode);
});
this.renderBuilderStage();
}
selectVerbRoot(rootName) {
this.selectedVerbRoot = rootName;
this.renderBuilderStage();
}
saveLabItemToDeck(word, article, plural, english, exampleDe, exampleEn, note) {
const existing = this.vocab.find(v => v.word.toLowerCase() === word.toLowerCase());
if (existing) {
existing.srs.dueDate = Date.now() - 1000;
this.saveState();
this.showToast(`⚡ „${word}“ wurde im SRS-Trainer priorisiert!`);
return;
}
const newEntry = this.createVocabEntry({
id: "lab_" + Date.now() + "_" + Math.floor(Math.random() * 1000),
word,
article: article || null,
plural: plural || null,
pos: article ? "noun" : "verb",
level: "A2/B1",
category: "Everyday & Bureaucracy",
english,
breakdown: note,
forms: plural,
exampleDe,
exampleEn: exampleEn || "",
clozeSentence: exampleDe.replace(new RegExp(word, "i"), "___"),
clozeAnswer: word,
usageTip: note
});
this.vocab.unshift(newEntry);
this.saveState();
this.showToast(`➕ „${word}“ zum SRS-Trainer hinzugefügt!`);
}
getBuilderQuizItems() {
const data = window.WORD_BUILDER_DATA;
if (!data) return [];
const items = [];
data.verbRoots.forEach(r => {
r.derivatives.forEach(d => {
items.push({
type: "prefix",
root: r.root,
word: d.word,
separable: d.separable,
english: d.english,
promptDe: d.quizCloze,
answer: d.quizAnswer,
explanation: `${d.word} (${d.forms}) — ${d.logic}`
});
});
});
data.compounds.forEach(c => {
items.push({
type: "compound",
compound: c.compound,
finalArticle: c.finalArticle,
english: c.english,
parts: c.parts,
explanation: c.ruleNote,
exampleDe: c.exampleDe
});
});
return items;
}
checkBuilderQuizAnswer(userVal) {
const items = this.getBuilderQuizItems();
const item = items[this.builderQuizIndex % items.length];
if (!item) return;
this.stats.totalChecks++;
if (item.type === "prefix") {
const isCorrect = this.normalizeGerman(userVal) === this.normalizeGerman(item.answer);
if (isCorrect) this.stats.correctChecks++;
this.builderQuizResult = {
isCorrect,
message: isCorrect
? `✅ Richtig! „${item.answer}“ (${item.word} — ${item.separable ? "trennbar" : "untrennbar"}).`
: `❌ Fast! Gesucht war: „${item.answer}“ (${item.word}).`
};
this.speak(item.promptDe.replace("___", item.answer));
} else {
const isCorrect = userVal === item.finalArticle;
if (isCorrect) this.stats.correctChecks++;
this.builderQuizResult = {
isCorrect,
message: isCorrect
? `✅ Perfekt! Das letzte Wort bestimmt den Artikel: ${item.finalArticle} ${item.compound}!`
: `❌ Nicht ganz: Das letzte Wort in „${item.compound}“ bestimmt den Artikel → ${item.finalArticle} ${item.compound}!`
};
this.speak(`${item.finalArticle} ${item.compound}`);
}
this.saveState();
this.renderBuilderStage();
}
nextBuilderQuiz() {
const items = this.getBuilderQuizItems();
this.builderQuizIndex = (this.builderQuizIndex + 1) % items.length;
this.builderQuizResult = null;
this.renderBuilderStage();
}
renderBuilderStage() {
const container = document.getElementById("builder-stage-container");
const data = window.WORD_BUILDER_DATA;
if (!container || !data) return;
if (this.builderSubmode === "prefixes") {
const activeRoot = data.verbRoots.find(r => r.root === this.selectedVerbRoot) || data.verbRoots[0];
const rootPills = data.verbRoots
.map(
r => `
<button class="btn ${r.root === activeRoot.root ? "btn-primary" : "btn-secondary"}" onclick="app.selectVerbRoot('${r.root}')">
Stamm: <strong>${r.root}</strong>
</button>
`
)
.join("");
const cardsHtml = activeRoot.derivatives
.map(
d => `
<div class="prefix-card">
<div style="display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;">
<div>
<span class="sep-pill ${d.separable ? "sep-yes" : "sep-no"}">
${d.separable ? "✂️ Trennbar (mit -ge-)" : "🔒 Untrennbar (ohne -ge-)"}
</span>
</div>
<div style="display: flex; gap: 0.35rem;">
<button class="icon-btn" style="width: 30px; height: 30px; font-size: 0.8rem;" onclick="app.speak('${this.escapeJs(d.word + ". " + d.exampleDe)}')" title="Anhören">🔊</button>
<button class="icon-btn" style="width: 30px; height: 30px; font-size: 0.8rem;" onclick="app.saveLabItemToDeck('${this.escapeJs(d.word)}', null, '${this.escapeJs(d.forms)}', '${this.escapeJs(d.english)}', '${this.escapeJs(d.exampleDe)}', '${this.escapeJs(d.exampleEn)}', '${this.escapeJs(d.logic)}')" title="Ins SRS-Deck speichern">➕</button>
</div>
</div>
<div style="font-size: 1.2rem; font-weight: 800; margin-top: 0.2rem;">
<span style="color: var(--primary);">${d.prefix}</span>${activeRoot.root} → ${d.word}
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); font-weight: 600;">${d.forms}</div>
<div style="font-weight: 700; color: var(--text-main);">${d.english}</div>
<div style="font-size: 0.82rem; color: var(--text-secondary);">💡 <strong>Logik:</strong> ${d.logic}</div>
<div style="font-size: 0.88rem; background: var(--bg-main); padding: 0.6rem 0.75rem; border-radius: 8px; margin-top: 0.25rem;">
„${this.tokenizeSentenceForHover(d.exampleDe)}“
<div style="font-size: 0.78rem; color: var(--text-muted); margin-top: 0.2rem;">${d.exampleEn}</div>
</div>
</div>
`
)
.join("");
container.innerHTML = `
<div class="panel-card">
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
<div>
<h2 style="font-size: 1.35rem; font-weight: 800;">🔀 Verb-Stämme & Präfix-Labor</h2>
<p style="font-size: 0.9rem; color: var(--text-secondary);">
Ein einziger deutscher Verbstamm bildet durch Vorsilben (Präfixe) dutzende neue Wörter.
<strong>Trennbare</strong> Präfixe (an-, ab-, auf-, aus-, mit-, vor-, teil-) sind betont und bekommen im Perfekt <code>-ge-</code> in der Mitte.
<strong>Untrennbare</strong> Präfixe (be-, ver-, ent-, er-, über-, unter-) bekommen <u>kein</u> <code>-ge-</code>!
</p>
</div>
</div>
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem;">
${rootPills}
</div>
<div style="padding: 0.75rem 1rem; background: var(--bg-subtle); border-radius: 10px; font-weight: 600; font-size: 0.92rem;">
Grundverb: <strong>${activeRoot.root}</strong> — ${activeRoot.baseMeaning}
</div>
<div class="prefix-grid">
${cardsHtml}
</div>
</div>
`;
} else if (this.builderSubmode === "compounds") {
const compoundsHtml = data.compounds
.map(c => {
const blocksHtml = c.parts
.map((part, idx) => {
const isLast = idx === c.parts.length - 1;
return `
${idx > 0 ? `<div class="compound-plus">+</div>` : ""}
<div class="compound-block ${isLast ? "head-word" : ""}">
<div style="display: flex; align-items: center; gap: 0.35rem;">
${part.article ? this.getGenderBadgeHtml(part.article) : ""}
<strong style="font-size: 1.02rem;">${part.word}</strong>
</div>
<span style="font-size: 0.78rem; color: var(--text-secondary);">${part.meaning}</span>
</div>
`;
})
.join("");
return `
<div class="panel-card" style="margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;">
${this.getGenderBadgeHtml(c.finalArticle)}
<strong style="font-size: 1.35rem;">${this.tokenizeSentenceForHover(c.compound)}</strong>
<span style="font-size: 0.85rem; color: var(--text-muted);">(${c.plural})</span>
<span style="font-weight: 600; color: var(--text-secondary);">— ${c.english}</span>
</div>
<div style="display: flex; gap: 0.45rem;">
<button class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.82rem;" onclick="app.speak('${this.escapeJs(c.finalArticle + " " + c.compound + ". " + c.exampleDe)}')">🔊 Anhören</button>
<button class="btn btn-primary" style="padding: 0.35rem 0.75rem; font-size: 0.82rem;" onclick="app.saveLabItemToDeck('${this.escapeJs(c.compound)}', '${c.finalArticle}', '${this.escapeJs(c.plural)}', '${this.escapeJs(c.english)}', '${this.escapeJs(c.exampleDe)}', '', '${this.escapeJs(c.ruleNote)}')">➕ Ins SRS-Deck</button>
</div>
</div>
<div class="compound-blocks-row">
${blocksHtml}
</div>
<div style="font-size: 0.86rem; color: var(--text-secondary); margin-bottom: 0.45rem;">
🎯 <strong>Artikel-Regel:</strong> ${c.ruleNote}
</div>
<div style="font-size: 0.9rem; background: var(--bg-main); padding: 0.6rem 0.85rem; border-radius: 8px;">
„${this.tokenizeSentenceForHover(c.exampleDe)}“
</div>
</div>
`;
})
.join("");
container.innerHTML = `
<div style="margin-bottom: 1.25rem;">
<h2 style="font-size: 1.35rem; font-weight: 800;">🧱 Komposita-Knacker: Keine Angst vor langen deutschen Wörtern!</h2>
<p style="font-size: 0.92rem; color: var(--text-secondary);">
Goldene Regel: Egal wie lang ein zusammengesetztes Nomen ist — <strong>das allerletzte Baustein-Wort (gelb umrandet) bestimmt immer 100% den Artikel (der/die/das)!</strong>
</p>
</div>
${compoundsHtml}
`;
} else {
// Quiz submode
const items = this.getBuilderQuizItems();
const item = items[this.builderQuizIndex % items.length];
let quizBody = "";
if (item.type === "prefix") {
quizBody = `
<div class="prompt-instruction">🔀 Präfix-Verb im Kontext • Stamm: „${item.root}“</div>
<div class="prompt-main">${item.english}</div>
<div class="cloze-box">
<div class="cloze-de">${this.tokenizeSentenceForHover(item.promptDe)}</div>
</div>
<form onsubmit="event.preventDefault(); app.checkBuilderQuizAnswer(document.getElementById('builder-quiz-input').value)">
<div class="answer-input-row">
<input type="text" id="builder-quiz-input" class="german-input" placeholder="Fehlende Verbform eingeben (z.B. ${item.answer.slice(0, 3)}...)" autocomplete="off" />
<button type="submit" class="btn btn-primary">Prüfen</button>
<button type="button" class="btn btn-secondary" onclick="app.nextBuilderQuiz()">Nächste Frage →</button>
</div>
${this.renderUmlautBar("builder-quiz-input")}
</form>
`;
} else {
const partsLine = item.parts.map(p => `${p.article ? p.article + " " : ""}${p.word}`).join(" + ");
quizBody = `
<div class="prompt-instruction">🧱 Komposita-Artikel-Challenge • Welcher Artikel gewinnt?</div>
<div class="prompt-main">___ ${item.compound}</div>
<div style="font-size: 0.95rem; color: var(--text-secondary); margin-bottom: 1rem;">
Bausteine: <strong>${partsLine}</strong> (${item.english})
</div>
<div class="gender-choice-grid" style="grid-template-columns: repeat(3, 1fr);">
<button class="gender-choice-btn gender-der" onclick="app.checkBuilderQuizAnswer('der')">der</button>
<button class="gender-choice-btn gender-die" onclick="app.checkBuilderQuizAnswer('die')">die</button>
<button class="gender-choice-btn gender-das" onclick="app.checkBuilderQuizAnswer('das')">das</button>
</div>
`;
}
container.innerHTML = `
<div class="srs-card-stage">
<div class="card-meta-top">
<span class="category-tag">Wortbaukasten-Quiz • Frage ${(this.builderQuizIndex % items.length) + 1} / ${items.length}</span>
</div>
${quizBody}
${
this.builderQuizResult
? `
<div class="feedback-panel ${this.builderQuizResult.isCorrect ? "correct" : "incorrect"}">
<div style="font-weight: 700; margin-bottom: 0.45rem;">${this.builderQuizResult.message}</div>
<div style="font-size: 0.9rem; color: var(--text-secondary);">${item.explanation}</div>
<div style="margin-top: 0.85rem;">
<button class="btn btn-primary" onclick="app.nextBuilderQuiz()">Weiter zur nächsten Aufgabe →</button>
</div>
</div>
`
: ""
}
</div>
`;
}
}
// === 2) VERBEN MIT PRÄPOSITION & KASUS (REKTION & DA-/WO-) ===
setPrepSubmode(submode) {
this.prepSubmode = submode;
this.prepResult = null;
this.daWoResult = null;
["drill", "dawo", "reference"].forEach(m => {
const btn = document.getElementById(`prep-tab-${m}`);
if (btn) btn.classList.toggle("active", m === submode);
});
this.renderPrepositionsStage();
}
checkPrepDrillChoice(chosenOption) {
const drills = window.PREPOSITION_DRILLS || [];
const d = drills[this.prepIndex % drills.length];
if (!d) return;
this.stats.totalChecks++;
const cleanChosen = chosenOption.split(" (")[0].trim().toLowerCase();
const isCorrect =
cleanChosen === d.correctCombo.toLowerCase() ||
(d.acceptedAnswers || []).some(a => a.toLowerCase() === cleanChosen);
if (isCorrect) this.stats.correctChecks++;
const fullSentence = `${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`;
this.prepResult = {
isCorrect,
fullSentence,
message: isCorrect
? `✅ Perfekt! „${d.verb} + ${d.preposition} + ${d.grammaticalCase}“ → „${d.correctCombo}“.`
: `❌ Fast! Du hast „${chosenOption}“ gewählt, aber richtig ist: „${d.correctCombo}“ (${d.verb} + ${d.preposition} + ${d.grammaticalCase}).`
};
this.saveState();
this.speak(fullSentence);
this.renderPrepositionsStage();
}
nextPrepDrill() {
const drills = window.PREPOSITION_DRILLS || [];
this.prepIndex = (this.prepIndex + 1) % drills.length;
this.prepResult = null;
this.renderPrepositionsStage();
}
checkDaWoAnswer(chosenWord) {
const drills = window.PREPOSITION_DRILLS || [];
const d = drills[this.daWoIndex % drills.length];
if (!d) return;
this.stats.totalChecks++;
const isCorrect = chosenWord.toLowerCase() === d.daForm.toLowerCase();
if (isCorrect) this.stats.correctChecks++;
this.daWoResult = {
isCorrect,
message: isCorrect
? `✅ Genau richtig! da(r) + ${d.preposition} = „${d.daForm}“ (Fragewort: „${d.woForm}“).`
: `❌ Nicht ganz: Zu „${d.verb} + ${d.preposition}“ gehört „${d.daForm}“ (und als Frage: „${d.woForm}“).`
};
this.saveState();
this.speak(d.daExampleDe);
this.renderPrepositionsStage();
}
nextDaWoDrill() {
const drills = window.PREPOSITION_DRILLS || [];
this.daWoIndex = (this.daWoIndex + 1) % drills.length;
this.daWoResult = null;
this.renderPrepositionsStage();
}
renderPrepositionsStage() {
const container = document.getElementById("prepositions-stage-container");
const drills = window.PREPOSITION_DRILLS || [];
if (!container || drills.length === 0) return;
if (this.prepSubmode === "drill") {
const d = drills[this.prepIndex % drills.length];
const optionsHtml = d.options
.map(
opt => `
<button
class="btn btn-secondary"
style="justify-content: center; font-size: 1.05rem; padding: 0.85rem;"
onclick="app.checkPrepDrillChoice('${this.escapeJs(opt)}')"
>
${opt}
</button>
`
)
.join("");
container.innerHTML = `
<div class="srs-card-stage">
<div class="card-meta-top">
<span class="category-tag">Präposition & Kasus • ${(this.prepIndex % drills.length) + 1} / ${drills.length}</span>
<span class="${d.grammaticalCase === "Akkusativ" ? "case-badge-akk" : "case-badge-dat"}">
Verb: ${d.verb} (${d.english})
</span>
</div>
<div class="prompt-instruction">🎯 Wähle die richtige Präposition + Artikel im richtigen Fall (Akkusativ / Dativ)</div>
<div class="cloze-box" style="margin: 1rem 0;">
<div class="cloze-de">
${this.tokenizeSentenceForHover(d.sentenceBefore)}
<span style="color:var(--primary);border-bottom:2px solid var(--primary);padding:0 0.5rem;font-weight:800;">[ ___ ]</span>
${this.tokenizeSentenceForHover(d.sentenceAfter)}
</div>
<div class="cloze-en" style="margin-top: 0.35rem;">
Nomen-Hinweis: <strong>${d.nounHint}</strong>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; margin: 1.25rem 0;">
${optionsHtml}
</div>
${
this.prepResult
? `
<div class="feedback-panel ${this.prepResult.isCorrect ? "correct" : "incorrect"}">
<div style="font-weight: 700; margin-bottom: 0.5rem;">${this.prepResult.message}</div>
<div style="font-size: 1rem; font-weight: 600; margin-bottom: 0.45rem;">
🗣️ „${this.tokenizeSentenceForHover(this.prepResult.fullSentence)}“
<button class="icon-btn" style="width: 28px; height: 28px; margin-left: 0.35rem;" onclick="app.speak('${this.escapeJs(this.prepResult.fullSentence)}')">🔊</button>
</div>
<div style="font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
💡 <strong>Erklärung:</strong> ${d.explanation}
</div>
<div style="font-size: 0.86rem; background: rgba(255,255,255,0.75); padding: 0.55rem 0.8rem; border-radius: 8px; margin-bottom: 0.85rem;">
🔗 <strong>Da-/Wo-Form:</strong> <code>${d.daForm}</code> / <code>${d.woForm}</code> — <em>„${this.tokenizeSentenceForHover(d.daExampleDe)}“</em>
</div>
<button class="btn btn-primary" onclick="app.nextPrepDrill()">Nächster Satz →</button>
</div>
`
: ""
}
</div>
`;
} else if (this.prepSubmode === "dawo") {
const d = drills[this.daWoIndex % drills.length];
const daChoices = ["darauf", "darüber", "darum", "daran", "davon", "dafür"];
const clozeDaSentence = d.daExampleDe.replace(new RegExp(d.daForm, "i"), "___");
container.innerHTML = `
<div class="srs-card-stage">
<div class="card-meta-top">
<span class="category-tag">Da- & Wo-Baukasten • ${(this.daWoIndex % drills.length) + 1} / ${drills.length}</span>
<span class="gender-badge gender-none">${d.verb} + ${d.preposition} (${d.grammaticalCase})</span>
</div>
<div class="prompt-instruction">🔗 Wie ersetzen Deutsche einen ganzen Satzteil durch ein „da-Wort“?</div>
<p style="font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 1rem;">
Regel: <code>da + Präposition</code> (bei Vokal a/o/u/ü mit <strong>-r-</strong> dazwischen: <em>da + r + auf = darauf</em>).
</p>
<div class="cloze-box">
<div class="cloze-de">${this.tokenizeSentenceForHover(clozeDaSentence)}</div>
<div class="cloze-en" style="margin-top: 0.3rem;">Gesuchtes Verb: <strong>${d.verb} (${d.english})</strong></div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.65rem; margin: 1.25rem 0;">
${daChoices
.map(
ch => `
<button class="btn btn-secondary" style="justify-content: center; font-weight: 700;" onclick="app.checkDaWoAnswer('${ch}')">
${ch}
</button>
`
)
.join("")}
</div>
${
this.daWoResult
? `
<div class="feedback-panel ${this.daWoResult.isCorrect ? "correct" : "incorrect"}">
<div style="font-weight: 700; margin-bottom: 0.45rem;">${this.daWoResult.message}</div>
<div style="font-size: 0.95rem; margin-bottom: 0.75rem;">
„${this.tokenizeSentenceForHover(d.daExampleDe)}“
<button class="icon-btn" style="width: 28px; height: 28px; margin-left: 0.35rem;" onclick="app.speak('${this.escapeJs(d.daExampleDe)}')">🔊</button>
</div>
<button class="btn btn-primary" onclick="app.nextDaWoDrill()">Weiter →</button>
</div>
`
: ""
}
</div>
`;
} else {
// Reference table
container.innerHTML = `
<div class="panel-card">
<h2 style="font-size: 1.3rem; font-weight: 800; margin-bottom: 0.35rem;">📋 Die wichtigsten A2/B1 Verben mit fester Präposition</h2>
<p style="font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 1.25rem;">
Lerne jedes Verb immer zusammen mit seiner Präposition und dem Fall (<span class="case-badge-akk">Akkusativ</span> oder <span class="case-badge-dat">Dativ</span>).
</p>
<div class="vocab-grid">
${drills
.map(
d => `
<div class="vocab-item-card">
<div>
<div style="display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;">
<strong style="font-size: 1.08rem;">${d.verb} <span style="color: var(--primary);">${d.preposition}</span></strong>
<span class="${d.grammaticalCase === "Akkusativ" ? "case-badge-akk" : "case-badge-dat"}">+ ${d.grammaticalCase}</span>
</div>
<div style="font-weight: 600; margin: 0.35rem 0;">${d.english}</div>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.45rem;">
Da-/Wo-Form: <code>${d.daForm}</code> • <code>${d.woForm}?</code>
</div>
<div style="font-size: 0.86rem; background: var(--bg-main); padding: 0.55rem 0.7rem; border-radius: 8px;">
„${this.tokenizeSentenceForHover(`${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`)}“
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 0.5rem; border-top: 1px solid var(--border-color);">
<button class="btn btn-secondary" style="padding: 0.3rem 0.65rem; font-size: 0.78rem;" onclick="app.speak('${this.escapeJs(`${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`)}')">🔊 Anhören</button>
<button class="btn btn-primary" style="padding: 0.3rem 0.65rem; font-size: 0.78rem;" onclick="app.saveLabItemToDeck('${this.escapeJs(d.verb + " " + d.preposition)}', null, '+ ${d.grammaticalCase} (${d.daForm})', '${this.escapeJs(d.english)}', '${this.escapeJs(`${d.sentenceBefore} ${d.correctCombo} ${d.sentenceAfter}`)}', '', '${this.escapeJs(d.explanation)}')">➕ Ins SRS-Deck</button>
</div>
</div>
`
)
.join("")}
</div>
</div>
`;
}
}
// === 3) HÖRTRAINING & AUSSPRACHE (LISTENING DICTATION & SHADOWING) ===
computeWordDiffHtml(expectedSentence, userSentence) {
const cleanWord = w => w.toLowerCase().replace(/[.,!?„“"']/g, "");
const expectedWords = expectedSentence.split(/\s+/).filter(Boolean);
const userWords = (userSentence || "").split(/\s+/).filter(Boolean).map(cleanWord);
let matchedCount = 0;
const diffHtml = expectedWords
.map(expW => {
const normExp = cleanWord(expW);
const foundIdx = userWords.indexOf(normExp);
if (foundIdx !== -1) {
userWords.splice(foundIdx, 1);
matchedCount++;
return `<span class="diff-word diff-ok word-token" data-word="${this.escapeJs(normExp)}">${expW}</span>`;
}
return `<span class="diff-word diff-miss word-token" data-word="${this.escapeJs(normExp)}">${expW}</span>`;
})
.join(" ");
const scorePct = Math.round((matchedCount / expectedWords.length) * 100);
return { diffHtml, scorePct, matchedCount, total: expectedWords.length };
}
checkListeningDictation(e) {
e.preventDefault();
const input = document.getElementById("listening-dictation-input");
if (!input) return;
const userText = input.value.trim();
if (!userText) return;
const list = window.LISTENING_EXERCISES || [];
const ex = list[this.listeningIndex % list.length];
if (!ex) return;
const { diffHtml, scorePct, matchedCount, total } = this.computeWordDiffHtml(ex.de, userText);
this.stats.totalChecks++;
this.stats.sentencesWritten++;
if (scorePct >= 80) this.stats.correctChecks++;
this.listeningResult = {
userText,
diffHtml,
scorePct,
matchedCount,
total
};
this.saveState();
this.renderListeningStage();
}
toggleListeningHint() {
this.listeningShowHint = !this.listeningShowHint;
this.renderListeningStage();
}
nextListeningExercise() {
const list = window.LISTENING_EXERCISES || [];
this.listeningIndex = (this.listeningIndex + 1) % list.length;
this.listeningShowHint = false;
this.listeningResult = null;
this.speechResult = null;
this.renderListeningStage();
const nextEx = list[this.listeningIndex % list.length];
if (nextEx) this.speak(nextEx.de, 0.96);
}
startMicrophonePronunciationCheck() {
const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRec) {
this.showToast("Dein Browser unterstützt die Web Speech Mikrofon-Erkennung leider nicht (am besten Chrome oder Edge nutzen).");
return;
}
const list = window.LISTENING_EXERCISES || [];
const ex = list[this.listeningIndex % list.length];
if (!ex) return;
if (this.isRecordingMic && this.activeRecognition) {
this.activeRecognition.stop();
return;
}
const recognition = new SpeechRec();
recognition.lang = "de-DE";
recognition.interimResults = false;
recognition.maxAlternatives = 1;
this.isRecordingMic = true;
this.activeRecognition = recognition;
this.renderListeningStage();
recognition.onresult = event => {
const transcript = event.results?.[0]?.[0]?.transcript || "";
const { diffHtml, scorePct } = this.computeWordDiffHtml(ex.de, transcript);
this.speechResult = {
transcript,
diffHtml,
scorePct
};
this.stats.sentencesWritten++;
this.saveState();
};
recognition.onerror = event => {
this.showToast(`Mikrofon-Hinweis: ${event.error}`);
};
recognition.onend = () => {
this.isRecordingMic = false;
this.renderListeningStage();
};
recognition.start();
}
renderListeningStage() {
const container = document.getElementById("listening-stage-container");
const list = window.LISTENING_EXERCISES || [];
if (!container || list.length === 0) return;
const ex = list[this.listeningIndex % list.length];
const wordInitialsHint = ex.de
.split(" ")
.map(w => w[0] + "___")
.join(" ");
container.innerHTML = `
<div class="srs-card-stage">
<div class="card-meta-top">
<span class="category-tag">🎧 Hörtraining & Aussprache • ${(this.listeningIndex % list.length) + 1} / ${list.length} (${ex.level})</span>
<span style="font-size: 0.82rem; font-weight: 600; color: var(--text-secondary);">${ex.situation}</span>
</div>
<div class="audio-hero-box">
<div>
<div style="font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.85;">
Schritt 1: Höre den deutschen Satz (ohne Text!)
</div>
<div style="font-size: 1.15rem; font-weight: 800; margin-top: 0.2rem;">
Klicke auf Play und tippe unten genau, was du hörst.
</div>
</div>
<div style="display: flex; gap: 0.6rem; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="app.speak('${this.escapeJs(ex.de)}', 0.96)">
🔊 Normal abspielen (1.0x)
</button>
<button class="btn btn-light" onclick="app.speak('${this.escapeJs(ex.de)}', 0.78)">
🐢 Langsam (0.8x)
</button>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="app.toggleListeningHint()">
💡 ${this.listeningShowHint ? "Hinweis ausblenden" : "Englische Bedeutung & Anfangsbuchstaben zeigen"}
</button>
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="app.nextListeningExercise()">
⏭️ Nächster Hör-Satz →
</button>
</div>
${
this.listeningShowHint
? `
<div class="cloze-box" style="margin-bottom: 1rem;">
<div style="font-weight: 700; font-size: 0.92rem;">🇬🇧 Bedeutung: „${ex.en}“</div>
<div style="font-size: 0.84rem; color: var(--text-secondary); margin-top: 0.25rem;">
🔤 Wort-Struktur (${ex.de.split(" ").length} Wörter): <code>${wordInitialsHint}</code>
</div>
</div>
`
: ""
}
<form onsubmit="app.checkListeningDictation(event)">
<div class="prompt-instruction">✍️ Diktat: Tippe den gehörten deutschen Satz</div>
<div class="answer-input-row">
<input
type="text"
id="listening-dictation-input"
class="german-input"
placeholder="Tippe hier, was du gehört hast..."
value="${this.listeningResult ? this.escapeJs(this.listeningResult.userText) : ""}"
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">Diktat prüfen</button>
</div>
${this.renderUmlautBar("listening-dictation-input")}
</form>
${
this.listeningResult
? `
<div class="feedback-panel ${this.listeningResult.scorePct >= 80 ? "correct" : "incorrect"}">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap;">
<strong>🎯 Diktat-Ergebnis: ${this.listeningResult.scorePct}% (${this.listeningResult.matchedCount}/${this.listeningResult.total} Wörter erkannt)</strong>
<button class="btn btn-secondary" style="padding: 0.3rem 0.7rem; font-size: 0.8rem;" onclick="app.speak('${this.escapeJs(ex.de)}', 0.96)">🔊 Nochmal hören</button>
</div>
<div style="margin-bottom: 0.5rem; line-height: 1.9;">
${this.listeningResult.diffHtml}
</div>
<div style="font-size: 0.86rem; color: var(--text-secondary); margin-bottom: 0.45rem;">
🇬🇧 „${ex.en}“
</div>
<div style="font-size: 0.84rem; color: var(--text-muted);">
💡 <strong>Hör-Fokus:</strong> ${ex.focusNote}
</div>
</div>
`
: ""
}
<div style="margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;">
<div>
<div class="prompt-instruction">🎙️ Schritt 2: Shadowing & Aussprache-Check (Optional)</div>
<div style="font-size: 0.88rem; color: var(--text-secondary);">
Sprich den Satz laut auf Deutsch nach — die Spracherkennung prüft deine Aussprache Wort für Wort!
</div>
</div>
<button
type="button"
class="btn btn-secondary ${this.isRecordingMic ? "mic-btn-recording" : ""}"
onclick="app.startMicrophonePronunciationCheck()"
>
${this.isRecordingMic ? "🔴 Höre zu... (Jetzt sprechen)" : "🎙️ Satz laut sprechen"}
</button>
</div>
${
this.speechResult
? `
<div class="feedback-panel ${this.speechResult.scorePct >= 75 ? "correct" : ""}" style="margin-top: 0.85rem;">
<div style="font-weight: 700; margin-bottom: 0.35rem;">
🗣️ Erkannt: „${this.speechResult.transcript}“ (${this.speechResult.scorePct}% Übereinstimmung)
</div>
<div>${this.speechResult.diffHtml}</div>
</div>
`
: ""
}
</div>
</div>
`;
}
renderAll() {
this.updateDashboardCounters();
this.renderDailySpotlight();
+34 -1
View File
@@ -236,5 +236,38 @@ window.HOVER_DICT = {
"exportieren": "to export", "lernstand": "learning progress", "zurücksetzen": "to reset",
"verständnis": "understanding / comprehension (das Verständnis)",
"klicke": "click (imperative)", "wähle": "choose / select (imperative)", "versuche": "try (imperative)",
"fahre": "move / hover / drive", "maus": "mouse (die Maus)", "beliebiges": "any", "stammform": "base form / lemma"
"fahre": "move / hover / drive", "maus": "mouse (die Maus)", "beliebiges": "any", "stammform": "base form / lemma",
// Labs: Wortbaukasten, Hörtraining & Verben mit Präposition
"wortbaukasten": "Word Building Kit / Compound & Prefix Lab (der Wortbaukasten)",
"hörtraining": "Listening Training (das Hörtraining)",
"diktat": "Dictation (das Diktat)",
"rektion": "Verb Preposition & Case Governance (die Rektion)",
"annehmen": "to accept / assume", "angenommen": "accepted / assumed", "paket": "package / parcel (das Paket)", "jobangebot": "job offer (das Jobangebot)",
"abnehmen": "to lose weight / take off", "abgenommen": "lost weight / taken off", "kilo": "kilogram(s) (das Kilo)", "frühling": "spring (der Frühling)",
"nimmst": "take (nimmst ... teil = take part)", "teil": "part (der/das Teil)", "kollegen": "colleagues (die Kollegen)", "besprechung": "meeting (die Besprechung)", "cappuccino": "cappuccino (der Cappuccino)",
"unternehmen": "to undertake / do an activity", "unternommen": "undertaken / done", "langen": "long", "schönes": "something nice", "tolle": "great / awesome", "ausflüge": "day trips / excursions (die Ausflüge)",
"übernehmen": "to take over / cover (costs)", "übernommen": "taken over / covered", "übernehme": "take over / cover", "kompletten": "complete / entire",
"vorstellen": "to introduce / imagine", "neuer": "new", "nachbar": "neighbor (der Nachbar)", "zweiten": "second", "ganzes": "whole / entire", "leben": "to live",
"bestellen": "to order", "bestellt": "ordered", "gewählt": "chosen / selected", "winterstiefel": "winter boots (die Winterstiefel)", "internet": "internet (das Internet)",
"ausstellen": "to issue (a document/prescription)", "ausgestellt": "issued",
"feststellen": "to realize / notice / determine", "festgestellt": "realized / noticed", "portemonnaie": "wallet (das Portemonnaie)", "liegt": "lies / is located", "auspacken": "unpacking (das Auspacken)", "fehlt": "is missing",
"welches": "which", "vorgeschlagen": "suggested / proposed", "nachschlagen": "to look up (in a dictionary)", "unbekannte": "unknown", "mühsam": "tediously / painstakingly",
"ankommen": "to arrive", "baustelle": "construction site / roadworks (die Baustelle)", "bekommen": "to get / receive", "steuer": "tax (die Steuer)", "nachricht": "message (die Nachricht)",
"lust": "desire / feel like (Lust haben = to feel like)", "glas": "glass (das Glas)", "wein": "wine (der Wein)",
"krankenversicherungskarte": "health insurance card (die Krankenversicherungskarte)", "legen": "to lay / present (legen ... vor)", "empfang": "reception desk (der Empfang)",
"wohnungsbesichtigung": "apartment viewing (die Wohnungsbesichtigung)", "samstagmittag": "Saturday noon", "online": "online", "freien": "free / available",
"bewahr": "keep / save (bewahr ... auf)", "falls": "in case / if", "gerät": "device / appliance (das Gerät)",
"feierabendbier": "after-work beer (das Feierabendbier)", "biergarten": "beer garden (der Biergarten)", "kühles": "cool / cold",
"nebenkostenabrechnung": "annual utility bill statement (die Nebenkostenabrechnung)", "nichts": "nothing", "nachzahlen": "to pay extra / back-pay",
"anschlussverbindung": "connecting train / transit connection (die Anschlussverbindung)", "warten": "to wait", "warte": "wait", "fahrgäste": "passengers (die Fahrgäste)",
"anrufen": "to call (on the phone)", "supermarktkasse": "supermarket checkout (die Supermarktkasse)", "wochenendausflug": "weekend trip (der Wochenendausflug)", "planen": "to plan", "reklamation": "complaint / return (die Reklamation)", "kaufhaus": "department store (das Kaufhaus)",
"kümmern": "to take care of (sich kümmern um)", "abendessen": "dinner / supper (das Abendessen)", "darum": "about it / of it (da + um)", "worum": "about what (wo + um)",
"freuen": "to be glad / look forward to", "zukunft": "future (die Zukunft)", "meer": "sea / ocean (das Meer)", "darauf": "to it / for it (da + auf)",
"gegenwart": "present (die Gegenwart)", "vergangenheit": "past (die Vergangenheit)", "nette": "nice / kind", "gefreut": "been happy / looked forward", "geholfen": "helped", "darüber": "about it (da + über)", "worüber": "about what (wo + über)",
"workshop": "workshop (der Workshop)", "daran": "in it / of it / to it (da + an)", "woran": "in what / of what (wo + an)",
"abhängen": "to depend (on)", "davon": "on it / from it (da + von)", "wovon": "on what / from what (wo + von)",
"gewöhnen": "to get used to (sich gewöhnen an)", "gewöhnt": "accustomed / used to", "ruhigen": "quiet / peaceful", "sonntage": "Sundays (die Sonntage)", "anfang": "beginning (am Anfang = at the beginning)", "bürokratie": "bureaucracy (die Bürokratie)",
"umzug": "move / relocation (der Umzug)", "interessieren": "to be interested (in)", "interessiert": "interested", "interessierst": "are interested", "wofür": "in what / for what (wo + für)", "dafür": "in it / for it (da + für)", "besonders": "especially / particularly", "kultur": "culture (die Kultur)",
"erinnern": "to remember (sich erinnern an)", "erinnerst": "remember", "erinnere": "remember"
};
+562
View File
@@ -0,0 +1,562 @@
// WortKraft Labs Data — 1) Wortbaukasten (Compounds & Prefixes), 2) Hörtraining (Dictation & Shadowing), 3) Verben mit Präposition (Rektion & Da-/Wo-)
window.WORD_BUILDER_DATA = {
verbRoots: [
{
root: "nehmen",
baseMeaning: "to take (nimmt • nahm • hat genommen)",
derivatives: [
{
word: "annehmen",
prefix: "an-",
separable: true,
forms: "nimmt an • nahm an • hat angenommen",
english: "to accept (a package/offer) / to assume",
logic: "an (at/toward) + nehmen (take) = take toward oneself → accept",
exampleDe: "Kannst du heute Nachmittag bitte das Paket für mich annehmen?",
exampleEn: "Can you please accept the package for me this afternoon?",
quizCloze: "Er hat das Jobangebot in München sofort ___.",
quizAnswer: "angenommen"
},
{
word: "abnehmen",
prefix: "ab-",
separable: true,
forms: "nimmt ab • nahm ab • hat abgenommen",
english: "to take off / to lose weight / to decrease",
logic: "ab (off/down) + nehmen (take) = take off weight or load",
exampleDe: "Seit ich jeden Tag mit dem Fahrrad fahre, habe ich drei Kilo abgenommen.",
exampleEn: "Since I started riding my bike every day, I've lost three kilos.",
quizCloze: "Im Frühling möchte ich ein paar Kilo ___.",
quizAnswer: "abnehmen"
},
{
word: "teilnehmen",
prefix: "teil-",
separable: true,
forms: "nimmt teil • nahm teil • hat teilgenommen",
english: "to participate / take part (an + Dative)",
logic: "Teil (part) + nehmen (take) = literally 'to take part'",
exampleDe: "Nimmst du morgen Abend auch an unserem Sprach-Stammtisch teil?",
exampleEn: "Are you also taking part in our language meetup tomorrow evening?",
quizCloze: "Wie viele Kollegen ___ heute an der Besprechung ___?",
quizAnswer: "nehmen teil"
},
{
word: "mitnehmen",
prefix: "mit-",
separable: true,
forms: "nimmt mit • nahm mit • hat mitgenommen",
english: "to take along / to get food to-go ('zum Mitnehmen')",
logic: "mit (with) + nehmen (take) = take along with you",
exampleDe: "Einen großen Cappuccino zum Mitnehmen, bitte!",
exampleEn: "A large cappuccino to go, please!",
quizCloze: "Möchten Sie den Kaffee hier trinken oder zum ___?",
quizAnswer: "Mitnehmen"
},
{
word: "unternehmen",
prefix: "unter-",
separable: false,
forms: "unternimmt • unternahm • hat unternommen (NO -ge-!)",
english: "to undertake / to do an activity together",
logic: "Inseparable prefix unter- (stressed on -nehmen): to venture into doing something",
exampleDe: "Wollen wir am langen Wochenende etwas Schönes zusammen unternehmen?",
exampleEn: "Shall we do something nice together over the long weekend?",
quizCloze: "Wir haben im Urlaub jeden Tag tolle Ausflüge ___.",
quizAnswer: "unternommen"
},
{
word: "übernehmen",
prefix: "über-",
separable: false,
forms: "übernimmt • übernahm • hat übernommen (NO -ge-!)",
english: "to take over (a task, shift, or cost)",
logic: "Inseparable prefix über-: take over responsibility or payment",
exampleDe: "Die Krankenkasse hat die kompletten Kosten für die Untersuchung übernommen.",
exampleEn: "The health insurance provider covered the entire cost of the examination.",
quizCloze: "Keine Sorge, heute Abend ___ ich die Rechnung!",
quizAnswer: "übernehme"
}
]
},
{
root: "stellen",
baseMeaning: "to place / put upright / set (stellt • stellte • hat gestellt)",
derivatives: [
{
word: "vorstellen",
prefix: "vor-",
separable: true,
forms: "stellt vor • stellte vor • hat vorgestellt",
english: "to introduce (sich + Acc) / to imagine (sich + Dat)",
logic: "vor (in front of) + stellen (place) = place before someone's eyes",
exampleDe: "Darf ich mich kurz vorstellen? Ich bin dein neuer Nachbar aus dem zweiten Stock.",
exampleEn: "May I introduce myself briefly? I'm your new neighbor from the second floor.",
quizCloze: "Kannst du dir ___, ein ganzes Jahr in Berlin zu leben?",
quizAnswer: "vorstellen"
},
{
word: "bestellen",
prefix: "be-",
separable: false,
forms: "bestellt • bestellte • hat bestellt (NO -ge-!)",
english: "to order (food in a restaurant or items online)",
logic: "Inseparable be- + stellen: arrange for delivery/service",
exampleDe: "Haben Sie schon gewählt, oder möchten Sie erst Getränke bestellen?",
exampleEn: "Have you already chosen, or would you like to order drinks first?",
quizCloze: "Ich habe die neuen Winterstiefel gestern im Internet ___.",
quizAnswer: "bestellt"
},
{
word: "ausstellen",
prefix: "aus-",
separable: true,
forms: "stellt aus • stellte aus • hat ausgestellt",
english: "to issue (a document/certificate/prescription) / to exhibit",
logic: "aus (out) + stellen = put out officially",
exampleDe: "Die Ärztin hat mir sofort ein Rezept und eine Krankschreibung ausgestellt.",
exampleEn: "The doctor immediately issued me a prescription and a sick note.",
quizCloze: "Das Bürgeramt hat meinen neuen Reisepass in drei Wochen ___.",
quizAnswer: "ausgestellt"
},
{
word: "feststellen",
prefix: "fest-",
separable: true,
forms: "stellt fest • stellte fest • hat festgestellt",
english: "to realize / notice / determine",
logic: "fest (firm/fixed) + stellen = establish as a fact",
exampleDe: "An der Kasse habe ich plötzlich festgestellt, dass mein Portemonnaie zu Hause liegt.",
exampleEn: "At the checkout I suddenly realized that my wallet is at home.",
quizCloze: "Beim Auspacken musste ich leider ___, dass ein Teil fehlt.",
quizAnswer: "feststellen"
}
]
},
{
root: "schlagen",
baseMeaning: "to hit / beat / strike (schlägt • schlug • hat geschlagen)",
derivatives: [
{
word: "vorschlagen",
prefix: "vor-",
separable: true,
forms: "schlägt vor • schlug vor • hat vorgeschlagen",
english: "to suggest / propose",
logic: "vor (forward) + schlagen = put a proposal forward",
exampleDe: "Ich schlage vor, dass wir uns direkt vor dem Rathaus treffen.",
exampleEn: "I suggest that we meet right in front of the town hall.",
quizCloze: "Welches Restaurant hast du für heute Abend ___?",
quizAnswer: "vorgeschlagen"
},
{
word: "nachschlagen",
prefix: "nach-",
separable: true,
forms: "schlägt nach • schlug nach • hat nachgeschlagen",
english: "to look up (a word in a dictionary)",
logic: "nach (after) + schlagen = flip through pages after something",
exampleDe: "Mit dem Hover-Übersetzer musst du unbekannte Wörter nicht mehr mühsam nachschlagen.",
exampleEn: "With the hover translator, you no longer have to tediously look up unknown words.",
quizCloze: "Wenn ich den Artikel nicht weiß, ___ ich ihn kurz im Wörterbuch ___.",
quizAnswer: "schlage nach"
}
]
},
{
root: "kommen",
baseMeaning: "to come (kommt • kam • ist gekommen)",
derivatives: [
{
word: "ankommen",
prefix: "an-",
separable: true,
forms: "kommt an • kam an • ist angekommen",
english: "to arrive (also: 'Es kommt darauf an' = it depends!)",
logic: "an (at) + kommen = arrive at destination",
exampleDe: "Unser ICE ist trotz der Baustelle pünktlich in Hamburg angekommen.",
exampleEn: "Our ICE train arrived on time in Hamburg despite the roadworks.",
quizCloze: "Um wie viel Uhr ___ dein Zug heute Abend am Hauptbahnhof ___?",
quizAnswer: "kommt an"
},
{
word: "bekommen",
prefix: "be-",
separable: false,
forms: "bekommt • bekam • hat bekommen (FALSE FRIEND: means 'to get/receive', NOT 'become'!)",
english: "to get / to receive",
logic: "Inseparable be- + kommen: something comes to you",
exampleDe: "Ohne Anmeldung beim Bürgeramt bekommt man leider keine Steuer-ID.",
exampleEn: "Without registering at the civic office, you unfortunately don't get a tax ID.",
quizCloze: "Hast du meine Nachricht auf WhatsApp schon ___?",
quizAnswer: "bekommen"
},
{
word: "vorbeikommen",
prefix: "vorbei-",
separable: true,
forms: "kommt vorbei • kam vorbei • ist vorbeigekommen",
english: "to drop by / come over / swing by",
logic: "vorbei (by/past) + kommen = come by someone's place",
exampleDe: "Wenn du Lust hast, kannst du heute Abend gerne auf ein Glas Wein vorbeikommen.",
exampleEn: "If you feel like it, you're welcome to drop by for a glass of wine tonight.",
quizCloze: "Sie können heute um elf Uhr direkt in der Praxis ___.",
quizAnswer: "vorbeikommen"
}
]
}
],
compounds: [
{
id: "comp1",
compound: "Krankenversicherungskarte",
finalArticle: "die",
plural: "die Krankenversicherungskarten",
english: "health insurance card",
parts: [
{ word: "Kranken-", article: null, meaning: "sick people's (from der/die Kranke)" },
{ word: "Versicherungs-", article: "die", meaning: "insurance (die Versicherung)" },
{ word: "Karte", article: "die", meaning: "card (die Karte → determines 'die'!)" }
],
ruleNote: "Even though this word has 25 letters, the last building block is 'die Karte' (feminine), so the entire compound is 'die Krankenversicherungskarte'!",
exampleDe: "Bitte legen Sie am Empfang zuerst Ihre Krankenversicherungskarte vor."
},
{
id: "comp2",
compound: "Wohnungsbesichtigung",
finalArticle: "die",
plural: "die Wohnungsbesichtigungen",
english: "apartment viewing",
parts: [
{ word: "Wohnungs-", article: "die", meaning: "apartment (die Wohnung + linking -s-)" },
{ word: "Besichtigung", article: "die", meaning: "viewing / inspection (die Besichtigung → determines 'die'!)" }
],
ruleNote: "Notice the 'Fugen-s' (linking -s-) after -ung nouns when forming compounds: Wohnung + s + Besichtigung.",
exampleDe: "Am Samstagmittag gehen wir zusammen zu einer Wohnungsbesichtigung."
},
{
id: "comp3",
compound: "Bürgeramt",
finalArticle: "das",
plural: "die Bürgerämter",
english: "citizens' registration office",
parts: [
{ word: "Bürger", article: "der", meaning: "citizen (der Bürger)" },
{ word: "Amt", article: "das", meaning: "office / department (das Amt → determines 'das'!)" }
],
ruleNote: "'der Bürger' is masculine, but the final noun is 'das Amt' (neuter)—so it is ALWAYS 'das Bürgeramt'!",
exampleDe: "Ich habe online endlich einen freien Termin beim Bürgeramt gefunden."
},
{
id: "comp4",
compound: "Kassenbon",
finalArticle: "der",
plural: "die Kassenbons",
english: "checkout receipt",
parts: [
{ word: "Kassen-", article: "die", meaning: "cash register (die Kasse)" },
{ word: "Bon", article: "der", meaning: "voucher / slip (der Bon → determines 'der'!)" }
],
ruleNote: "'die Kasse' is feminine, but 'der Bon' is masculine—so the receipt is 'der Kassenbon'!",
exampleDe: "Bewahr den Kassenbon gut auf, falls wir das Gerät umtauschen müssen."
},
{
id: "comp5",
compound: "Feierabendbier",
finalArticle: "das",
plural: "die Feierabendbiere",
english: "after-work beer",
parts: [
{ word: "Feier", article: "die", meaning: "celebration / rest (die Feier)" },
{ word: "Abend", article: "der", meaning: "evening (der Abend)" },
{ word: "Bier", article: "das", meaning: "beer (das Bier → determines 'das'!)" }
],
ruleNote: "Three nouns with three different genders (die Feier + der Abend + das Bier) → last word wins: 'das Feierabendbier'!",
exampleDe: "Nach der langen Arbeitswoche trinken wir im Biergarten ein kühles Feierabendbier."
},
{
id: "comp6",
compound: "Mehrwegbecher",
finalArticle: "der",
plural: "die Mehrwegbecher",
english: "reusable cup (with deposit)",
parts: [
{ word: "Mehrweg-", article: "der", meaning: "multi-way / reusable (mehr + der Weg)" },
{ word: "Becher", article: "der", meaning: "cup / mug (der Becher → determines 'der'!)" }
],
ruleNote: "German contrasts 'Mehrweg' (reusable, literally 'more-way') with 'Einweg' (single-use/disposable, 'one-way').",
exampleDe: "Für den Mehrwegbecher auf dem Weihnachtsmarkt zahlt man zwei Euro Pfand."
},
{
id: "comp7",
compound: "Nebenkostenabrechnung",
finalArticle: "die",
plural: "die Nebenkostenabrechnungen",
english: "annual utility bill statement",
parts: [
{ word: "Neben-", article: null, meaning: "additional / side" },
{ word: "Kosten-", article: "die (Pl.)", meaning: "costs (die Kosten)" },
{ word: "Abrechnung", article: "die", meaning: "settlement / statement (die Abrechnung → determines 'die'!)" }
],
ruleNote: "Every tenant in Germany receives 'die Nebenkostenabrechnung' once a year comparing advance utility payments with actual heating/water usage.",
exampleDe: "Zum Glück mussten wir bei der Nebenkostenabrechnung dieses Jahr nichts nachzahlen."
},
{
id: "comp8",
compound: "Anschlussverbindung",
finalArticle: "die",
plural: "die Anschlussverbindungen",
english: "connecting train / transit connection",
parts: [
{ word: "Anschluss-", article: "der", meaning: "connection / follow-up (der Anschluss)" },
{ word: "Verbindung", article: "die", meaning: "link / connection (die Verbindung → determines 'die'!)" }
],
ruleNote: "'der Anschluss' is masculine, 'die Verbindung' is feminine → 'die Anschlussverbindung'.",
exampleDe: "Wegen der Verspätung warten viele Fahrgäste auf ihre Anschlussverbindung."
}
]
};
// 2) LISTENING DICTATION & SHADOWING SENTENCES (HÖRTRAINING)
window.LISTENING_EXERCISES = [
{
id: "listen1",
level: "A2",
situation: "Beim Arzt anrufen (Calling the doctor's office)",
de: "Ich möchte heute bitte kurzfristig einen Termin vereinbaren.",
en: "I would like to schedule an appointment on short notice today, please.",
focusNote: "Listen for 'kurzfristig' (at short notice) and the unstressed 'einen Termin'."
},
{
id: "listen2",
level: "A2",
situation: "Termin verschieben (Postponing an appointment)",
de: "Leider muss ich unsere Besichtigung auf nächste Woche verschieben.",
en: "Unfortunately I have to postpone our viewing to next week.",
focusNote: "Notice the V2 inversion after 'Leider': 'Leider muss ich...' and the -ung ending on 'Besichtigung'."
},
{
id: "listen3",
level: "A2",
situation: "An der Supermarktkasse (At the supermarket checkout)",
de: "Brauchen Sie den Kassenbon, oder haben Sie noch einen Pfandbon?",
en: "Do you need the receipt, or do you still have a bottle deposit slip?",
focusNote: "Two classic German checkout compounds: 'der Kassenbon' and 'der Pfandbon'."
},
{
id: "listen4",
level: "A2 / B1",
situation: "Feierabend mit Kollegen (After work with colleagues)",
de: "Ich schlage vor, dass wir endlich Feierabend machen.",
en: "I suggest that we finally call it a day.",
focusNote: "Hear how the separable verb 'vorschlagen' splits in the main clause ('Ich schlage vor'), while 'machen' goes to the end after 'dass'."
},
{
id: "listen5",
level: "A2 / B1",
situation: "Auf dem Bahnsteig (On the train platform)",
de: "Der Zug hat heute leider zwanzig Minuten Verspätung, deshalb müssen wir in Hannover umsteigen.",
en: "The train unfortunately has a twenty-minute delay today, therefore we have to transfer in Hanover.",
focusNote: "Notice the connector 'deshalb' followed immediately by the verb 'müssen'."
},
{
id: "listen6",
level: "B1",
situation: "Wochenendausflug planen (Planning a weekend trip)",
de: "Keine Sorge, ich kümmere mich um die Unterkunft, obwohl es am Wochenende regnen soll.",
en: "No worries, I'll take care of the accommodation, even though it's supposed to rain on the weekend.",
focusNote: "Listen for 'ich kümmere mich um' (+ Accusative) and 'obwohl' kicking 'regnen soll' to the very end."
},
{
id: "listen7",
level: "A2",
situation: "Kurze Nachricht an Freunde (Quick message to friends)",
de: "Gibst du mir bitte kurz Bescheid, sobald du zu Hause angekommen bist?",
en: "Will you please let me know briefly as soon as you have arrived home?",
focusNote: "Natural everyday idiom 'Bescheid geben' + subordinate clause connector 'sobald'."
},
{
id: "listen8",
level: "B1",
situation: "Reklamation im Kaufhaus (Exchanging an item at a store)",
de: "Ich möchte diese Jacke gerne umtauschen, weil der Reißverschluss kaputt ist.",
en: "I would like to exchange this jacket because the zipper is broken.",
focusNote: "Notice 'diese Jacke umtauschen' and 'der Reißverschluss' (zipper)."
}
];
// 3) VERBS WITH PREPOSITIONS & CASES (REKTION DER VERBEN + DA-/WO- TRAINER)
window.PREPOSITION_DRILLS = [
{
id: "prep1",
verb: "sich kümmern",
preposition: "um",
grammaticalCase: "Akkusativ",
daForm: "darum",
woForm: "worum",
english: "to take care of / look after",
sentenceBefore: "Keine Sorge, ich kümmere mich heute",
sentenceAfter: "Abendessen.",
nounHint: "das Abendessen (neut.)",
correctCombo: "um das",
acceptedAnswers: ["um das", "ums"],
options: ["um das", "für das", "über das", "mit dem"],
daExampleDe: "Wir brauchen noch Getränke. — Ich kümmere mich darum!",
explanation: "'sich kümmern' ALWAYS takes 'um + Akkusativ' (um das Abendessen / ums Abendessen). For things: 'darum' / 'Worum kümmerst du dich?'"
},
{
id: "prep2",
verb: "sich freuen (Zukunft)",
preposition: "auf",
grammaticalCase: "Akkusativ",
daForm: "darauf",
woForm: "worauf",
english: "to look forward to (something in the future)",
sentenceBefore: "Nach der langen Arbeitswoche freue ich mich riesig",
sentenceAfter: "Feierabend!",
nounHint: "der Feierabend (mask.)",
correctCombo: "auf den",
acceptedAnswers: ["auf den"],
options: ["auf den", "auf dem", "über den", "für den"],
daExampleDe: "Am Freitag fahren wir ans Meer — ich freue mich schon sehr darauf!",
explanation: "Future events take 'sich freuen AUF + Akkusativ'. Since 'der Feierabend' is masculine, Accusative changes 'der' to 'den' → 'auf den Feierabend'."
},
{
id: "prep3",
verb: "sich freuen (Gegenwart/Vergangenheit)",
preposition: "über",
grammaticalCase: "Akkusativ",
daForm: "darüber",
woForm: "worüber",
english: "to be happy about (something received or already happening)",
sentenceBefore: "Sie hat sich gestern sehr",
sentenceAfter: "nette Einladung gefreut.",
nounHint: "die Einladung (fem.)",
correctCombo: "über die",
acceptedAnswers: ["über die", "ueber die"],
options: ["über die", "auf die", "über der", "von der"],
daExampleDe: "Du hast mir sehr geholfen — ich habe mich wirklich darüber gefreut.",
explanation: "When something has already happened or you received a gift/invitation, use 'sich freuen ÜBER + Akkusativ' ('über die Einladung')."
},
{
id: "prep4",
verb: "teilnehmen",
preposition: "an",
grammaticalCase: "Dativ",
daForm: "daran",
woForm: "woran",
english: "to participate in / attend",
sentenceBefore: "Möchtest du nächste Woche auch",
sentenceAfter: "Deutschkurs teilnehmen?",
nounHint: "der Deutschkurs (mask.)",
correctCombo: "an dem",
acceptedAnswers: ["an dem", "am"],
options: ["an dem (am)", "in dem", "auf den", "zu dem"],
daExampleDe: "Morgen findet ein Workshop statt. Nimmst du auch daran teil?",
explanation: "'teilnehmen' takes 'an + Dativ'! Masculine 'der Deutschkurs' becomes 'dem' in Dative → 'an dem / am Deutschkurs'."
},
{
id: "prep5",
verb: "abhängen",
preposition: "von",
grammaticalCase: "Dativ",
daForm: "davon",
woForm: "wovon",
english: "to depend on",
sentenceBefore: "Ob wir im Park grillen können, hängt natürlich stark",
sentenceAfter: "Wetter ab.",
nounHint: "das Wetter (neut.)",
correctCombo: "vom",
acceptedAnswers: ["vom", "von dem"],
options: ["vom (von dem)", "auf das", "über das", "mit dem"],
daExampleDe: "Kommst du heute Abend mit? — Das hängt davon ab, wann ich Feierabend habe.",
explanation: "'abhängen' takes 'von + Dativ' (von dem Wetter = vom Wetter). Remember: 'von' is ALWAYS Dative in German!"
},
{
id: "prep6",
verb: "warten",
preposition: "auf",
grammaticalCase: "Akkusativ",
daForm: "darauf",
woForm: "worauf",
english: "to wait for",
sentenceBefore: "Auf dem Bahnsteig warten wir schon seit zwanzig Minuten",
sentenceAfter: "Zug nach Hamburg.",
nounHint: "der Zug (mask.)",
correctCombo: "auf den",
acceptedAnswers: ["auf den"],
options: ["auf den", "für den", "auf dem", "nach dem"],
daExampleDe: "Mein neuer Reisepass ist noch nicht da; ich warte schon drei Wochen darauf.",
explanation: "English says 'wait FOR', but German NEVER uses 'für' with warten! It is always 'warten AUF + Akkusativ' ('auf den Zug')."
},
{
id: "prep7",
verb: "sich gewöhnen",
preposition: "an",
grammaticalCase: "Akkusativ",
daForm: "daran",
woForm: "woran",
english: "to get used to / accustomed to",
sentenceBefore: "Nach ein paar Monaten habe ich mich schnell",
sentenceAfter: "ruhigen Sonntage in Deutschland gewöhnt.",
nounHint: "die Sonntage (Plural)",
correctCombo: "an die",
acceptedAnswers: ["an die"],
options: ["an die", "zu den", "mit den", "an den"],
daExampleDe: "Am Anfang war die deutsche Bürokratie ungewohnt, aber man gewöhnt sich daran.",
explanation: "'sich gewöhnen' takes 'an + Akkusativ' (moving toward a new habit → Accusative 'an die Sonntage')."
},
{
id: "prep8",
verb: "bitten",
preposition: "um",
grammaticalCase: "Akkusativ",
daForm: "darum",
woForm: "worum",
english: "to ask for / request politely",
sentenceBefore: "Wegen der Verspätung bitten wir alle Fahrgäste",
sentenceAfter: "Verständnis.",
nounHint: "das Verständnis (neut.)",
correctCombo: "um das",
acceptedAnswers: ["um", "um das", "um ihr"],
options: ["um das (um Ihr)", "für das", "nach dem", "über das"],
daExampleDe: "Kannst du mir beim Umzug helfen? Ich wollte dich schon gestern darum bitten.",
explanation: "'bitten UM + Akkusativ' means 'to ask FOR something' ('Wir bitten um Ihr Verständnis'). Compare with 'fragen nach + Dativ' (to ask about information)."
},
{
id: "prep9",
verb: "sich interessieren",
preposition: "für",
grammaticalCase: "Akkusativ",
daForm: "dafür",
woForm: "wofür",
english: "to be interested in",
sentenceBefore: "Bei der Besichtigung haben sich über dreißig Personen",
sentenceAfter: "Wohnung interessiert.",
nounHint: "die Wohnung (fem.)",
correctCombo: "für die",
acceptedAnswers: ["für die", "fuer die"],
options: ["für die", "in der", "an der", "über die"],
daExampleDe: "Wofür interessierst du dich besonders? — Für deutsche Geschichte und Kultur!",
explanation: "English says 'interested IN', but German says 'sich interessieren FÜR + Akkusativ' ('für die Wohnung')."
},
{
id: "prep10",
verb: "sich erinnern",
preposition: "an",
grammaticalCase: "Akkusativ",
daForm: "daran",
woForm: "woran",
english: "to remember / recall",
sentenceBefore: "Erinnerst du dich noch",
sentenceAfter: "unseren ersten Ausflug nach München?",
nounHint: "der Ausflug (mask., 'unseren' = Akk.)",
correctCombo: "an",
acceptedAnswers: ["an"],
options: ["an", "über", "von", "auf"],
daExampleDe: "Ja, natürlich erinnere ich mich noch sehr gut daran!",
explanation: "'sich erinnern AN + Akkusativ' = to remember. Notice how 'daran' replaces 'an unseren ersten Ausflug'."
}
];
+106 -22
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css?v=4" />
<link rel="stylesheet" href="styles.css?v=5" />
</head>
<body>
@@ -30,9 +30,18 @@
<span>📊</span> Übersicht
</button>
<button class="nav-tab" data-view="trainer" onclick="app.switchView('trainer')">
<span>⚡</span> Wortschatz-Trainer
<span>⚡</span> Trainer
<span class="nav-badge" id="nav-due-badge">0</span>
</button>
<button class="nav-tab" data-view="builder" onclick="app.switchView('builder')">
<span>🧱</span> Wortbaukasten
</button>
<button class="nav-tab" data-view="prepositions" onclick="app.switchView('prepositions')">
<span>🎯</span> Präpositionen
</button>
<button class="nav-tab" data-view="listening" onclick="app.switchView('listening')">
<span>🎧</span> Hörtraining
</button>
<button class="nav-tab" data-view="reader" onclick="app.switchView('reader')">
<span>📖</span> Leseecke
</button>
@@ -68,7 +77,7 @@
<p>
After 1,400 days on Duolingo, you already have a strong intuition for basic German structure.
<strong>WortKraft</strong> bridges the A2→B1 plateau by training <em>active recall with articles</em>,
<em>compound word logic</em>, and <em>real-world production</em>.
<em>compound word logic</em>, <em>verb + preposition cases</em>, and <em>native listening & production</em>.
</p>
<div class="gender-legend-bar">
<span class="gender-badge gender-der">der • Maskulin</span>
@@ -81,8 +90,8 @@
<button class="btn btn-primary" onclick="app.switchView('trainer')">
⚡ Jetzt trainieren (<span id="hero-due-count">0</span> fällig)
</button>
<button class="btn btn-light" onclick="app.switchView('reader')">
📖 Geschichte lesen
<button class="btn btn-light" onclick="app.switchView('builder')">
🧱 Wortbaukasten
</button>
</div>
</div>
@@ -99,26 +108,26 @@
<div class="stat-sub" id="stat-mastered-sub">0 gemeistert • 0 im Training</div>
</div>
<div class="stat-card">
<div class="stat-label">Artikel-Trefferquote</div>
<div class="stat-label">Artikel- & Übungsquote</div>
<div class="stat-value" id="stat-accuracy">—</div>
<div class="stat-sub">der / die / das & aktive Eingabe</div>
<div class="stat-sub">der / die / das & Präpositionen</div>
</div>
<div class="stat-card">
<div class="stat-label">Aktive Sätze geschrieben</div>
<div class="stat-label">Aktive Sätze produziert</div>
<div class="stat-value" id="stat-sentences">0</div>
<div class="stat-sub">In der Sprachpraxis produziert</div>
<div class="stat-sub">Sprachpraxis & Hörtraining</div>
</div>
</div>
<div class="dashboard-columns">
<div class="panel-card">
<div class="panel-header">
<h2 class="panel-title">Dein täglicher 3-Schritte-Workflow</h2>
<h2 class="panel-title">Deine 6 Trainings-Module (A2 → B1)</h2>
</div>
<div class="module-launcher-list">
<div class="launcher-item" onclick="app.startTrainerMode('cloze')">
<div class="launcher-left">
<div class="launcher-icon">🎯</div>
<div class="launcher-icon">⚡</div>
<div>
<div class="launcher-title">1. Aktiver Abruf im Kontext (SRS-Trainer)</div>
<div class="launcher-desc">Lückensätze, Englisch → Deutsch mit Artikel & Plural-Drill statt Multiple-Choice.</div>
@@ -127,12 +136,45 @@
<span>→</span>
</div>
<div class="launcher-item" onclick="app.switchView('builder')">
<div class="launcher-left">
<div class="launcher-icon">🧱</div>
<div>
<div class="launcher-title">2. Wortbaukasten (Komposita & Präfix-Labor)</div>
<div class="launcher-desc">Verstehe trennbare/untrennbare Verben (nehmen, stellen, kommen) & lange zusammengesetzte Nomen.</div>
</div>
</div>
<span>→</span>
</div>
<div class="launcher-item" onclick="app.switchView('prepositions')">
<div class="launcher-left">
<div class="launcher-icon">🎯</div>
<div>
<div class="launcher-title">3. Verben mit Präposition & Kasus (Rektion + Da-/Wo-)</div>
<div class="launcher-desc">Meistere warten auf (+Akk), teilnehmen an (+Dat), sich kümmern um (+Akk) & darauf/worauf.</div>
</div>
</div>
<span>→</span>
</div>
<div class="launcher-item" onclick="app.switchView('listening')">
<div class="launcher-left">
<div class="launcher-icon">🎧</div>
<div>
<div class="launcher-title">4. Hörtraining & Aussprache-Check (Diktat & Mikrofon)</div>
<div class="launcher-desc">Höre echte Alltagssätze ohne Text, tippe was du hörst, oder sprich den Satz selbst ins Mikrofon.</div>
</div>
</div>
<span>→</span>
</div>
<div class="launcher-item" onclick="app.switchView('reader')">
<div class="launcher-left">
<div class="launcher-icon">📖</div>
<div>
<div class="launcher-title">2. Interaktive Leseecke (A2/B1 Geschichten & Alltag)</div>
<div class="launcher-desc">Klicke auf jedes Wort für Artikel, Stammform, Audio & 1-Klick-Speichern ins SRS-Deck.</div>
<div class="launcher-title">5. Interaktive Leseecke (A2/B1 Geschichten & Alltag)</div>
<div class="launcher-desc">Hover-Übersetzer + Klick auf jedes Wort für Artikel, Stammform, Audio & 1-Klick-SRS-Speichern.</div>
</div>
</div>
<span>→</span>
@@ -142,7 +184,7 @@
<div class="launcher-left">
<div class="launcher-icon">💬</div>
<div>
<div class="launcher-title">3. Sprachpraxis mit Zielwörtern</div>
<div class="launcher-title">6. Sprachpraxis mit Zielwörtern</div>
<div class="launcher-desc">Löse echte Alltagssituationen (Arzt, Bürgeramt, Feierabend) und nutze gezielt 5 Zielwörter.</div>
</div>
</div>
@@ -197,7 +239,48 @@
<div id="trainer-stage-container"></div>
</section>
<!-- 3. INTERACTIVE READER (LESEECKE) -->
<!-- 3. WORTBAUKASTEN (COMPOUND WORD & PREFIX LAB) -->
<section id="view-builder" class="view-section">
<div class="trainer-controls">
<div class="mode-pills">
<button class="mode-pill active" id="builder-tab-prefixes" onclick="app.setBuilderSubmode('prefixes')">
🔀 Verb-Stämme & Präfix-Labor (trennbar vs. untrennbar)
</button>
<button class="mode-pill" id="builder-tab-compounds" onclick="app.setBuilderSubmode('compounds')">
🧱 Komposita-Knacker (Zusammengesetzte Nomen)
</button>
<button class="mode-pill" id="builder-tab-quiz" onclick="app.setBuilderSubmode('quiz')">
⚡ Präfix- & Komposita-Quiz
</button>
</div>
</div>
<div id="builder-stage-container"></div>
</section>
<!-- 4. VERBEN MIT PRÄPOSITION (REKTION & DA-/WO- TRAINER) -->
<section id="view-prepositions" class="view-section">
<div class="trainer-controls">
<div class="mode-pills">
<button class="mode-pill active" id="prep-tab-drill" onclick="app.setPrepSubmode('drill')">
🎯 Präposition + Kasus-Trainer (auf den / am / um das)
</button>
<button class="mode-pill" id="prep-tab-dawo" onclick="app.setPrepSubmode('dawo')">
🔗 Da- & Wo-Baukasten (darauf, worauf, darum, daran)
</button>
<button class="mode-pill" id="prep-tab-reference" onclick="app.setPrepSubmode('reference')">
📋 Übersichtstabelle (Akkusativ vs. Dativ)
</button>
</div>
</div>
<div id="prepositions-stage-container"></div>
</section>
<!-- 5. HÖRTRAINING & AUSSPRACHE (LISTENING DICTATION & SHADOWING) -->
<section id="view-listening" class="view-section">
<div id="listening-stage-container"></div>
</section>
<!-- 6. INTERACTIVE READER (LESEECKE) -->
<section id="view-reader" class="view-section">
<div class="reader-layout">
<aside class="story-sidebar">
@@ -216,7 +299,7 @@
</div>
</section>
<!-- 4. CONVERSATION & WRITING PRACTICE (SPRACHPRAXIS) -->
<!-- 7. CONVERSATION & WRITING PRACTICE (SPRACHPRAXIS) -->
<section id="view-practice" class="view-section">
<div class="scenario-layout">
<aside class="story-sidebar">
@@ -231,7 +314,7 @@
</div>
</section>
<!-- 5. VOCABULARY BANK (WÖRTERBUCH) -->
<!-- 8. VOCABULARY BANK (WÖRTERBUCH) -->
<section id="view-dictionary" class="view-section">
<div class="panel-header" style="margin-bottom: 1.25rem; flex-wrap: wrap;">
<div>
@@ -408,10 +491,11 @@
<div id="toast" class="toast-notification"></div>
<!-- SCRIPTS -->
<script src="data/vocabulary.js?v=4"></script>
<script src="data/stories.js?v=4"></script>
<script src="data/scenarios.js?v=4"></script>
<script src="data/hover_dict.js?v=4"></script>
<script src="app.js?v=4"></script>
<script src="data/vocabulary.js?v=5"></script>
<script src="data/stories.js?v=5"></script>
<script src="data/scenarios.js?v=5"></script>
<script src="data/hover_dict.js?v=5"></script>
<script src="data/labs.js?v=5"></script>
<script src="app.js?v=5"></script>
</body>
</html>
+137
View File
@@ -1125,3 +1125,140 @@ body.hover-translate-on .word-token:hover {
margin-top: 0.15rem;
}
/* === WORTBAUKASTEN (COMPOUND & PREFIX LAB) === */
.compound-blocks-row {
display: flex;
align-items: stretch;
gap: 0.5rem;
flex-wrap: wrap;
margin: 1rem 0;
}
.compound-block {
background: var(--bg-main);
border: 2px solid var(--border-strong);
border-radius: 12px;
padding: 0.75rem 1rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 130px;
}
.compound-block.head-word {
border-color: var(--primary);
background: #fffbeb;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.12);
}
.compound-plus {
display: flex;
align-items: center;
font-weight: 800;
font-size: 1.25rem;
color: var(--text-muted);
}
.prefix-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.prefix-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 1.15rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.sep-pill {
font-size: 0.72rem;
font-weight: 700;
padding: 0.15rem 0.55rem;
border-radius: 999px;
}
.sep-yes {
background: #ecfdf5;
color: #047857;
border: 1px solid #6ee7b7;
}
.sep-no {
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #93c5fd;
}
/* === HÖRTRAINING (LISTENING DICTATION & SHADOWING) === */
.audio-hero-box {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
color: #fff;
border-radius: var(--radius-md);
padding: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1.25rem;
}
.diff-word {
display: inline-block;
padding: 0.15rem 0.45rem;
border-radius: 6px;
margin: 0.12rem;
font-weight: 700;
}
.diff-ok {
background: #d1fae5;
color: #065f46;
}
.diff-miss {
background: #fee2e2;
color: #991b1b;
text-decoration: underline wavy;
}
.mic-btn-recording {
background: #dc2626 !important;
color: #fff !important;
animation: pulseMic 1.2s infinite;
}
@keyframes pulseMic {
0% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.55); }
70% { box-shadow: 0 0 0 10px rgba(220, 38, 38, 0); }
100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); }
}
/* === PREPOSITION & CASE TRAINER === */
.case-badge-akk {
background: #fef3c7;
color: #b45309;
border: 1px solid #fcd34d;
font-weight: 700;
font-size: 0.76rem;
padding: 0.15rem 0.55rem;
border-radius: 6px;
}
.case-badge-dat {
background: #ede9fe;
color: #6d28d9;
border: 1px solid #c4b5fd;
font-weight: 700;
font-size: 0.76rem;
padding: 0.15rem 0.55rem;
border-radius: 6px;
}