Merge pull request #529 from erzabytyci/user-tiers

improve Satzmix exercise
This commit is contained in:
erzabytyci
2026-05-31 15:59:57 +03:00
committed by GitHub
4 changed files with 77 additions and 22 deletions
@@ -9,9 +9,21 @@ import { withBasePath } from "@/shared/utils/basePath";
import useQuery from "@/shared/hooks/useQuery"; import useQuery from "@/shared/hooks/useQuery";
import styles from "./jumbled-sentence.module.css"; import styles from "./jumbled-sentence.module.css";
function toWordPerLine(value) { function toElementPerLine(value) {
if (typeof value !== "string") return ""; if (typeof value !== "string") return "";
return value.trim().split(/\s+/).filter(Boolean).join("\n");
const trimmedValue = value.trim();
if (!trimmedValue) return "";
if (trimmedValue.includes("\n")) {
return trimmedValue
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
}
return trimmedValue.split(/\s+/).filter(Boolean).join("\n");
} }
export default function CreateJumbledSentenceExercise() { export default function CreateJumbledSentenceExercise() {
@@ -50,10 +62,12 @@ export default function CreateJumbledSentenceExercise() {
setSentences( setSentences(
(loadedExercise.sentences || []).length (loadedExercise.sentences || []).length
? loadedExercise.sentences.map((item) => ({ ? loadedExercise.sentences.map((item) => ({
sentence: toWordPerLine(item.sentence || ""), sentence: toElementPerLine(item.sentence || ""),
alternates: alternates:
item.alternates && item.alternates.length > 0 item.alternates && item.alternates.length > 0
? item.alternates ? item.alternates.map((alternate) =>
toElementPerLine(alternate)
)
: [""], : [""],
alternateFeedbacks: alternateFeedbacks:
item.alternateFeedbacks && item.alternateFeedbacks &&
@@ -67,7 +81,9 @@ export default function CreateJumbledSentenceExercise() {
correctSentenceFeedback: item.correctSentenceFeedback || "", correctSentenceFeedback: item.correctSentenceFeedback || "",
incorrectAlternates: incorrectAlternates:
item.incorrectAlternates && item.incorrectAlternates.length > 0 item.incorrectAlternates && item.incorrectAlternates.length > 0
? item.incorrectAlternates ? item.incorrectAlternates.map((alternate) =>
toElementPerLine(alternate)
)
: [""], : [""],
incorrectFeedbacks: incorrectFeedbacks:
item.incorrectFeedbacks && item.incorrectFeedbacks &&
@@ -205,10 +221,10 @@ export default function CreateJumbledSentenceExercise() {
const normalizedSentences = sentences.map((item) => ({ const normalizedSentences = sentences.map((item) => ({
...item, ...item,
sentence: toWordPerLine(item.sentence), sentence: toElementPerLine(item.sentence),
alternates: item.alternates alternates: item.alternates
.map((alt, index) => ({ .map((alt, index) => ({
alt: typeof alt === "string" ? alt.trim() : "", alt: toElementPerLine(alt),
feedback: feedback:
typeof item.alternateFeedbacks?.[index] === "string" typeof item.alternateFeedbacks?.[index] === "string"
? item.alternateFeedbacks[index].trim() ? item.alternateFeedbacks[index].trim()
@@ -232,7 +248,7 @@ export default function CreateJumbledSentenceExercise() {
: "", : "",
incorrectAlternates: (item.incorrectAlternates || []) incorrectAlternates: (item.incorrectAlternates || [])
.map((alt, index) => ({ .map((alt, index) => ({
alt: typeof alt === "string" ? alt.trim() : "", alt: toElementPerLine(alt),
feedback: feedback:
typeof item.incorrectFeedbacks?.[index] === "string" typeof item.incorrectFeedbacks?.[index] === "string"
? item.incorrectFeedbacks[index].trim() ? item.incorrectFeedbacks[index].trim()
@@ -346,15 +362,20 @@ export default function CreateJumbledSentenceExercise() {
</label> </label>
<div> <div>
<div className={styles.sectionTitle}>Alternativ-Sätze:</div> <div className={styles.sectionTitle}>Alternativ-Sätze:</div>
<div className={styles.sentenceHint}>
Schreiben Sie auch hier jedes Element in eine neue Zeile.
</div>
{s.alternates.map((alt, altIdx) => ( {s.alternates.map((alt, altIdx) => (
<div key={altIdx} className={styles.alternateGroup}> <div key={altIdx} className={styles.alternateGroup}>
<div className={styles.alternateRow}> <div className={styles.alternateRow}>
<input <textarea
value={alt} value={alt}
onChange={(e) => onChange={(e) =>
handleAlternateChange(idx, altIdx, e.target.value) handleAlternateChange(idx, altIdx, e.target.value)
} }
className={`${styles.fieldInput} ${styles.alternateInput}`} className={`${styles.fieldInput} ${styles.sentenceTextarea}`}
rows={4}
placeholder="Jeden Tag\nlerne ich\nDeutsch"
/> />
<Button <Button
type="button" type="button"
@@ -410,10 +431,13 @@ export default function CreateJumbledSentenceExercise() {
<div className={styles.sectionTitle}> <div className={styles.sectionTitle}>
Alternativ-Sätze (Falsche): Alternativ-Sätze (Falsche):
</div> </div>
<div className={styles.sentenceHint}>
Schreiben Sie auch hier jedes Element in eine neue Zeile.
</div>
{s.incorrectAlternates.map((alt, altIdx) => ( {s.incorrectAlternates.map((alt, altIdx) => (
<div key={altIdx} className={styles.alternateGroup}> <div key={altIdx} className={styles.alternateGroup}>
<div className={styles.alternateRowNoGapTop}> <div className={styles.alternateRowNoGapTop}>
<input <textarea
value={alt} value={alt}
onChange={(e) => onChange={(e) =>
handleIncorrectAlternateChange( handleIncorrectAlternateChange(
@@ -422,8 +446,9 @@ export default function CreateJumbledSentenceExercise() {
e.target.value e.target.value
) )
} }
className={`${styles.fieldInput} ${styles.alternateInput}`} className={`${styles.fieldInput} ${styles.sentenceTextarea}`}
placeholder="Falsche Alternative (optional)" rows={4}
placeholder="Deutsch\nlerne ich\njeden Tag"
/> />
<Button <Button
type="button" type="button"
@@ -16,11 +16,11 @@ describe("admin jumbled sentence exercises API", () => {
title: "Satzreihenfolge A1", title: "Satzreihenfolge A1",
sentences: [ sentences: [
{ {
sentence: "Ich lerne jeden Tag Deutsch", sentence: "Ich\nlerne jeden Tag\nDeutsch",
alternates: ["Jeden Tag lerne ich Deutsch"], alternates: ["lerne jeden Tag\nIch\nDeutsch"],
alternateFeedbacks: ["Auch richtig."], alternateFeedbacks: ["Auch richtig."],
correctSentenceFeedback: "Perfekt!", correctSentenceFeedback: "Perfekt!",
incorrectAlternates: ["Deutsch ich lerne jeden Tag"], incorrectAlternates: ["Deutsch\nIch\nlerne jeden Tag"],
incorrectFeedbacks: ["Subjekt und Verb bitte prüfen."], incorrectFeedbacks: ["Subjekt und Verb bitte prüfen."],
}, },
], ],
@@ -33,8 +33,23 @@ export default function JumbledSentenceExercisePage() {
const cleanTokenForComparison = (token) => const cleanTokenForComparison = (token) =>
stripIgnoredPunctuation(token).toLowerCase(); stripIgnoredPunctuation(token).toLowerCase();
const splitElements = (value) => {
const trimmedValue = value.trim();
if (!trimmedValue) return [];
if (trimmedValue.includes("\n")) {
return trimmedValue
.split(/\r?\n/)
.map((element) => element.trim())
.filter(Boolean);
}
return trimmedValue.split(/\s+/).filter(Boolean);
};
const tokenizeSentenceForDisplay = (value) => const tokenizeSentenceForDisplay = (value) =>
value.trim().split(/\s+/).map(stripIgnoredPunctuation).filter(Boolean); splitElements(value).map(stripIgnoredPunctuation).filter(Boolean);
const normalizeSentence = (value) => const normalizeSentence = (value) =>
tokenizeSentenceForDisplay(value) tokenizeSentenceForDisplay(value)
@@ -11,8 +11,23 @@ function cleanToken(token) {
return token.replace(/[\p{P}]/gu, "").toLowerCase(); return token.replace(/[\p{P}]/gu, "").toLowerCase();
} }
function normalizeTokens(sentence) { function splitElements(sentence) {
return sentence.trim().split(/\s+/).map(cleanToken).filter(Boolean).sort(); const trimmedSentence = sentence.trim();
if (!trimmedSentence) return [];
if (trimmedSentence.includes("\n")) {
return trimmedSentence
.split(/\r?\n/)
.map((element) => element.trim())
.filter(Boolean);
}
return trimmedSentence.split(/\s+/).filter(Boolean);
}
function normalizeElements(sentence) {
return splitElements(sentence).map(cleanToken).filter(Boolean).sort();
} }
function sameWordSet(a, b) { function sameWordSet(a, b) {
@@ -52,7 +67,7 @@ export const jumbledSentenceSchema = z
}, z.array(z.string())), }, z.array(z.string())),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
const baseTokens = normalizeTokens(data.sentence); const baseTokens = normalizeElements(data.sentence);
if (data.alternateFeedbacks.length !== data.alternates.length) { if (data.alternateFeedbacks.length !== data.alternates.length) {
ctx.addIssue({ ctx.addIssue({
@@ -64,7 +79,7 @@ export const jumbledSentenceSchema = z
for (let index = 0; index < data.alternates.length; index++) { for (let index = 0; index < data.alternates.length; index++) {
const alternate = data.alternates[index]; const alternate = data.alternates[index];
const alternateTokens = normalizeTokens(alternate); const alternateTokens = normalizeElements(alternate);
if (!sameWordSet(baseTokens, alternateTokens)) { if (!sameWordSet(baseTokens, alternateTokens)) {
ctx.addIssue({ ctx.addIssue({
@@ -78,7 +93,7 @@ export const jumbledSentenceSchema = z
for (let index = 0; index < data.incorrectAlternates.length; index++) { for (let index = 0; index < data.incorrectAlternates.length; index++) {
const incorrectAlternate = data.incorrectAlternates[index]; const incorrectAlternate = data.incorrectAlternates[index];
const incorrectTokens = normalizeTokens(incorrectAlternate); const incorrectTokens = normalizeElements(incorrectAlternate);
if (!sameWordSet(baseTokens, incorrectTokens)) { if (!sameWordSet(baseTokens, incorrectTokens)) {
ctx.addIssue({ ctx.addIssue({