Fix the clipped answer field + the doubled AI note; ordering only where it fits
UI bug from a phone screenshot (short answer, Czech): - The textarea was flex-shrunk inside the slide's column (58px instead of ~82px), clipping the typed answer mid-line. Every direct child of a study/review/guide slide is now shrink-0 and the slide scrolls instead. - Two AI disclaimers were visible at once: the activity's own after the grading box plus the feed's row. The activity-level copy is gone; the feed/guide row (which also carries the report control) is the single one. The phase-2 check now asserts the note sits below the graded feedback. - The mobile suite's backdrop-tap check is deterministic too (the spring animation made the measured box mid-flight). Ordering activities: the deck prompt now allows them only when the notes hold a real sequence (chronological events, process stages, procedure steps, a lifecycle, a ranked scale) and points at matching / multiple choice / true-false when they do not - "sort these" was landing on decks with no order at all. Version 0.2.4, SW -> freezmo-v57.
This commit is contained in:
parent
f4026d3e55
commit
9b0354fb11
9 changed files with 34 additions and 15 deletions
15
README.md
15
README.md
|
|
@ -1398,6 +1398,21 @@ Things I chose while building, with the reasoning:
|
||||||
phase-1 suite (43 checks, including the real route mapping against a stub Ollama returning a
|
phase-1 suite (43 checks, including the real route mapping against a stub Ollama returning a
|
||||||
monthly-limit body, a two-device sync round-trip, and the generation guards below).
|
monthly-limit body, a two-device sync round-trip, and the generation guards below).
|
||||||
|
|
||||||
|
65. **Slide content never flex-shrinks, and the AI note appears once.** A long typed answer in a
|
||||||
|
short-answer activity shrank the textarea inside the slide's flex column (58px instead of its
|
||||||
|
~82px), clipping the user's text mid-line on a phone; every direct child of a study/review/guide
|
||||||
|
slide is now `shrink-0` and the slide scrolls instead. The same screenshot showed **two** AI
|
||||||
|
disclaimers — the activity's own after the grading box plus the feed's row. The activity-level
|
||||||
|
copy is gone: the feed/guide row (which also carries the report control) is the single one, and
|
||||||
|
the phase-2 suite asserts the note sits below the graded feedback rather than right after it.
|
||||||
|
|
||||||
|
64. **Ordering cards only where an order exists.** "Sort these" was landing on decks whose notes
|
||||||
|
have no sequence at all (a list of works, authors or terms), which the learner cannot reason
|
||||||
|
about. The deck prompt now allows `ordering` only for real sequences — chronological events, the
|
||||||
|
stages of a process, steps of a procedure, a lifecycle, a ranked scale — and points at matching,
|
||||||
|
multiple choice or true/false when there is none, with one or two ordering cards per deck as the
|
||||||
|
usual ceiling.
|
||||||
|
|
||||||
63. **Frutiger Aero is the third theme.** Settings → Theme offers Light, Dark and Frutiger Aero.
|
63. **Frutiger Aero is the third theme.** Settings → Theme offers Light, Dark and Frutiger Aero.
|
||||||
Aero is not a dark/light variant — it replaces the palette and the scene: the wallpaper is
|
Aero is not a dark/light variant — it replaces the palette and the scene: the wallpaper is
|
||||||
*“The S Aesthetic That You Definitely Recognize Frutiger Aero”* via WallpaperSafari
|
*“The S Aesthetic That You Definitely Recognize Frutiger Aero”* via WallpaperSafari
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ export function SessionFeed({
|
||||||
aria-label={t("feed.activityOf", { n: itemIndex + 1, total })}
|
aria-label={t("feed.activityOf", { n: itemIndex + 1, total })}
|
||||||
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center]">
|
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center] [&>*]:shrink-0">
|
||||||
<ActivityRenderer
|
<ActivityRenderer
|
||||||
activity={item.activity}
|
activity={item.activity}
|
||||||
active={index === itemIndex}
|
active={index === itemIndex}
|
||||||
|
|
@ -242,7 +242,7 @@ export function SessionFeed({
|
||||||
aria-label={t("review.results")}
|
aria-label={t("review.results")}
|
||||||
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center]">
|
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center] [&>*]:shrink-0">
|
||||||
{finished
|
{finished
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
aria-label={t("feed.activityOf", { n: activityIndex + 1, total })}
|
aria-label={t("feed.activityOf", { n: activityIndex + 1, total })}
|
||||||
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center]">
|
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center] [&>*]:shrink-0">
|
||||||
<ActivityRenderer
|
<ActivityRenderer
|
||||||
activity={activity}
|
activity={activity}
|
||||||
active={index === activityIndex}
|
active={index === activityIndex}
|
||||||
|
|
@ -353,7 +353,7 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
aria-label={t("feed.results")}
|
aria-label={t("feed.results")}
|
||||||
className="h-full snap-start snap-always overflow-y-auto overscroll-y-contain"
|
className="h-full snap-start snap-always overflow-y-auto overscroll-y-contain"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center]">
|
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center] [&>*]:shrink-0">
|
||||||
<ResultsScreen
|
<ResultsScreen
|
||||||
deck={currentDeck}
|
deck={currentDeck}
|
||||||
xp={xp}
|
xp={xp}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { useI18n } from "@/lib/i18n";
|
||||||
import { modelErrorCopy } from "@/lib/model-errors";
|
import { modelErrorCopy } from "@/lib/model-errors";
|
||||||
import type { Activity } from "@/lib/schemas";
|
import type { Activity } from "@/lib/schemas";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AiDisclaimer } from "@/components/AiDisclaimer";
|
|
||||||
import { Button, Spinner } from "@/components/ui";
|
import { Button, Spinner } from "@/components/ui";
|
||||||
import { ModelErrorActions } from "@/components/ModelErrorActions";
|
import { ModelErrorActions } from "@/components/ModelErrorActions";
|
||||||
|
|
||||||
|
|
@ -186,8 +185,6 @@ export function ShortAnswer({ activity, active, language, onAnswered, onNext, is
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AiDisclaimer />
|
|
||||||
|
|
||||||
<FeedbackPanel
|
<FeedbackPanel
|
||||||
correct={state.correct}
|
correct={state.correct}
|
||||||
explanation={activity.explanation}
|
explanation={activity.explanation}
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ export function HomeworkGuide({
|
||||||
aria-label={t("homework.guide.stepOf", { n: stepIndex + 1, total: steps.length })}
|
aria-label={t("homework.guide.stepOf", { n: stepIndex + 1, total: steps.length })}
|
||||||
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
className="flex h-full snap-start snap-always items-stretch justify-center overflow-y-auto overscroll-y-contain"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center]">
|
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center] [&>*]:shrink-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="tint-sky inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ring-1 ring-white/50 backdrop-blur-sm">
|
<span className="tint-sky inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ring-1 ring-white/50 backdrop-blur-sm">
|
||||||
{t("homework.guide.stepOf", { n: stepIndex + 1, total: steps.length })}
|
{t("homework.guide.stepOf", { n: stepIndex + 1, total: steps.length })}
|
||||||
|
|
@ -291,7 +291,7 @@ export function HomeworkGuide({
|
||||||
aria-label={t("homework.guide.solution")}
|
aria-label={t("homework.guide.solution")}
|
||||||
className="h-full snap-start snap-always overflow-y-auto overscroll-y-contain"
|
className="h-full snap-start snap-always overflow-y-auto overscroll-y-contain"
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center]">
|
<div className="mx-auto flex min-h-full w-full max-w-xl flex-col gap-4 px-4 pb-24 pt-24 [justify-content:safe_center] [&>*]:shrink-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="tint-mint inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ring-1 ring-white/50 backdrop-blur-sm">
|
<span className="tint-mint inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ring-1 ring-white/50 backdrop-blur-sm">
|
||||||
{t("homework.guide.solution")}
|
{t("homework.guide.solution")}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,11 @@ 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": "..." }
|
{ "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)
|
6. ordering - 3 to 7 items given in the CORRECT order (the app shuffles them)
|
||||||
|
Use it ONLY when the notes contain a real sequence the learner can reason about: historical or
|
||||||
|
chronological events, the stages of a process, steps of a procedure, a lifecycle, a ranked scale.
|
||||||
|
If the notes only list unrelated things (works, authors, terms, places, values), do NOT invent an
|
||||||
|
order for them — use matching, multiple choice or true/false instead. One or two ordering cards
|
||||||
|
per deck is usually plenty.
|
||||||
Items must NEVER contain dates, years, clock times, step numbers or ordinals ("first", "step 2") —
|
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.
|
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": "..." }
|
{ "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": "..." }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "freezmo",
|
"name": "freezmo",
|
||||||
"version": "0.2.2",
|
"version": "0.2.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
*
|
*
|
||||||
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
||||||
*/
|
*/
|
||||||
const CACHE_VERSION = "freezmo-v56";
|
const CACHE_VERSION = "freezmo-v57";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,13 +142,15 @@ const browser = await launchBrowser();
|
||||||
await page.getByRole("button", { name: "Submit answer" }).click();
|
await page.getByRole("button", { name: "Submit answer" }).click();
|
||||||
await page.waitForTimeout(800);
|
await page.waitForTimeout(800);
|
||||||
check("grading: AI feedback rendered", await page.getByText("Nice — scattering does the work.").isVisible());
|
check("grading: AI feedback rendered", await page.getByText("Nice — scattering does the work.").isVisible());
|
||||||
// Scope it to the placement: the disclaimer directly under the graded box.
|
// The disclaimer sits below the graded feedback (the feed's row carries it;
|
||||||
|
// the activity no longer duplicates it right after the grading box).
|
||||||
const feedbackDisclaimer = await page.evaluate(() => {
|
const feedbackDisclaimer = await page.evaluate(() => {
|
||||||
const feedback = [...document.querySelectorAll("p")].find((el) => el.textContent.includes("Nice — scattering does the work."));
|
const feedback = [...document.querySelectorAll("p")].find((el) => el.textContent.includes("Nice — scattering does the work."));
|
||||||
const next = feedback?.closest("div")?.nextElementSibling;
|
const note = [...document.querySelectorAll("p")].find((el) => el.textContent.includes("Freezmo uses AI"));
|
||||||
return Boolean(next?.textContent?.includes("Freezmo uses AI"));
|
if (!feedback || !note) return false;
|
||||||
|
return note.getBoundingClientRect().top > feedback.getBoundingClientRect().bottom - 4;
|
||||||
});
|
});
|
||||||
check("grading: AI disclaimer under the feedback", feedbackDisclaimer);
|
check("grading: AI disclaimer below the feedback", feedbackDisclaimer);
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue