Fix ordering giveaways, the stuck false label, add Regenerate deck
Ordering cards - Generation prompt now forbids dates, years, times, step numbers and ordinals in ordering items (spec + hard rules), and the server-side validation drops any ordering card whose items still contain them instead of shipping a giveaway. True/False - The 'false ✕' swipe label was permanently visible on the card: its opacity curve was inverted, so the clamp at rest was 1. Flipped the range (and tightened both ranges for the elastic drag) — each label now only fades in while swiping in that direction. Regenerate deck - Results screen gains a Regenerate deck button: rebuilds the activities from the deck's stored source notes, replaces the deck in storage (attempts/best score reset) and starts the fresh run from slide 1. - StudyFeed tracks the current deck so the header, export and messages use the regenerated title/activities. - Fixed a spurious 0% attempt recorded when a regenerated deck swapped in: scroll anchoring held the old results position, so the feed briefly reported 5/5 with zero answers. The scroll now resets before the new slides render, overflow-anchor is disabled on the feed, and a run with no answers is never recorded. Tests: new deck-fixes suite 15/15 (TF overlay at rest/drag, regenerate with loading state + storage reset, ordering guard drop/keep); retry 14/14, deck chrome 15/15, pwa 16/16, final-ux 12/12, security 8/8, contrast 20/20.
This commit is contained in:
parent
1bacf43ed2
commit
33967f9454
6 changed files with 122 additions and 16 deletions
18
lib/ai.ts
18
lib/ai.ts
|
|
@ -250,6 +250,13 @@ const VALID_TYPES = [
|
|||
"short_answer",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Giveaways that make an ordering card solvable without knowing the material:
|
||||
* years, clock times, "first/next/finally", "step 2", "1.".
|
||||
*/
|
||||
const ORDERING_GIVEAWAY =
|
||||
/\b(1[0-9]{3}|20[0-9]{2})\b|\b\d{1,2}[:.]\d{2}\b|\b(first|second|third|fourth|fifth|sixth|seventh|eighth|1st|2nd|3rd|4th|5th|6th|7th|8th)\b|\b(step|stage|phase)\s*\d+\b|^\s*\d+[.)]\s/i;
|
||||
|
||||
/**
|
||||
* Be forgiving: validate each activity on its own and keep the good ones.
|
||||
* Only if too few survive does the step get retried.
|
||||
|
|
@ -280,7 +287,16 @@ function validateDeck(parsed: unknown, minimum: number, requested: number) {
|
|||
rawActivities.slice(0, 24).forEach((item, index) => {
|
||||
const result = generatedActivitySchema.safeParse(item);
|
||||
if (result.success) {
|
||||
activities.push(sanitiseActivity(result.data));
|
||||
const activity = sanitiseActivity(result.data);
|
||||
// An ordering card with dates/ordinals on it gives the answer away —
|
||||
// drop it rather than ship a card the learner can solve without knowing.
|
||||
if (activity.type === "ordering" && activity.items.some((entry) => ORDERING_GIVEAWAY.test(entry))) {
|
||||
rejected.push(
|
||||
`#${index + 1} (ordering): items must not contain dates, years, times or ordinals — the order has to come from the content.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
activities.push(activity);
|
||||
} else {
|
||||
const type = (item as { type?: unknown } | null)?.type;
|
||||
rejected.push(`#${index + 1} (${typeof type === "string" ? type : "no type"}): ${summariseZodError(result.error)}`);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,9 @@ export const ACTIVITY_JSON_SPEC = `Each activity is one JSON object with a "type
|
|||
{ "type": "matching", "topic": "short label", "instruction": "Match each term to its definition.", "pairs": [{ "term": "Osmosis", "definition": "diffusion of water across a membrane" }], "explanation": "..." }
|
||||
|
||||
6. ordering - 3 to 7 items given in the CORRECT order (the app shuffles them)
|
||||
{ "type": "ordering", "topic": "short label", "instruction": "Put the stages of mitosis in order.", "items": ["Prophase", "Metaphase", "Anaphase", "Telophase"], "explanation": "..." }
|
||||
Items must NEVER contain dates, years, clock times, step numbers or ordinals ("first", "step 2") —
|
||||
the learner has to work out the order from what each item says, not read it off.
|
||||
{ "type": "ordering", "topic": "short label", "instruction": "Put the events in the order they happened.", "items": ["The Allies land in Normandy", "Paris is liberated", "Germany surrenders"], "explanation": "..." }
|
||||
|
||||
7. short_answer - typed answer, graded by AI
|
||||
{ "type": "short_answer", "topic": "short label", "question": "...", "referenceAnswer": "a model answer in one or two sentences", "keyPoints": ["idea that must appear", "second idea"], "explanation": "..." }
|
||||
|
|
@ -102,6 +104,8 @@ Hard rules:
|
|||
- Vary the activity types. Use a healthy mix of all seven types below, and never put more than two activities of the same type in a row.
|
||||
- Cover the whole notes, not just the first paragraph. Spread activities across the different topics, definitions, formulas and facts.
|
||||
- Make the wrong options plausible (common mix-ups from the same notes), never silly filler.
|
||||
- Ordering items must never reveal their position: no dates, years, times, step numbers or ordinals
|
||||
("first", "next", "finally", "step 2"). Describe the event or stage itself.
|
||||
- Keep questions short enough to read on a phone screen. No walls of text.
|
||||
- Never reference "the notes", "the text above" or "the student". Speak directly to the learner.
|
||||
- Match the requested difficulty: easy = recall and simple recognition; medium = understanding and application; hard = analysis, edge cases and multi-step reasoning.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue