From b5269d284424f73d05b926ac8b588d2feaaecab1 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Mon, 12 Oct 2026 10:59:49 +0000 Subject: [PATCH] AI disclaimer: also on the cards and the results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The note now rides along with the content itself, not only with the generation screens and the chat composer: - Under every card in the study feed and in the review, mistakes and exam sessions (the activity slide's last element — below the card, and below the feedback panel once answered). - On the results slides: the study results screen and the shared session results (review, mistakes, exam). Navigation surfaces (library, deck lists, chat history, settings) still carry no note — it belongs to the moment of generation and to the card being answered, not to browsing (README decision 50 updated). The phase-2 grading check now scopes its placement (the disclaimer right under the graded box) since the same text legitimately appears several times on a session page; three new checks cover the feed, the results slide and the exam (59 checks). SW -> freezmo-v40. --- README.md | 12 +++++++----- components/SessionFeed.tsx | 9 ++++++++- components/StudyFeed.tsx | 3 +++ public/sw.js | 2 +- tests/phase2-test.mjs | 27 +++++++++++++++++++++++++-- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6e96d15..fead6c4 100644 --- a/README.md +++ b/README.md @@ -1283,11 +1283,13 @@ Things I chose while building, with the reasoning: 50. **One AI disclaimer, only where the model's words are on screen.** A shared `AiDisclaimer` (copy, style and translations in one place, `common.aiDisclaimer`) sits under the chat composer — always, since chat is pure model output — next to the generation buttons of the homework and - deck wizards, under their results, and under AI grading feedback. Stored, already-generated - content (the deck page, study cards, explanations) deliberately carries no note: the disclaimer - belongs to the moment of generation, not to browsing, and the empty states stay clean. It is a - plain `--text-subtle` paragraph (documented at 5.6:1 light / 8.6:1 dark), centred, wrapping to - two lines on a 360 px phone — not clickable, not dismissable. + deck wizards, under their results, and under AI grading feedback. It also rides along with the + content itself once it is being consumed: under every card in the study feed and in the review, + mistakes and exam sessions, and on their results slides. Navigation surfaces (the library, deck + lists, chat history, settings) deliberately carry no note: the disclaimer belongs to the moment + of generation and to the card being answered, not to browsing, and the empty states stay clean. + It is a plain `--text-subtle` paragraph (documented at 5.6:1 light / 8.6:1 dark), centred, + wrapping to two lines on a 360 px phone — not clickable, not dismissable. 36. **The mobile declutter pass reshapes, it doesn't fork.** Phones (≤639px) get a 3-step deck flow and a 2-step homework flow that share the exact state and handlers with the desktop diff --git a/components/SessionFeed.tsx b/components/SessionFeed.tsx index cd5ddd7..451803f 100644 --- a/components/SessionFeed.tsx +++ b/components/SessionFeed.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { ActivityRenderer } from "@/components/activities"; +import { AiDisclaimer } from "@/components/AiDisclaimer"; import { XIcon } from "@/components/icons"; import { ReportQuestionButton } from "@/components/ReportQuestionButton"; import { ProgressBar } from "@/components/ui"; @@ -216,6 +217,7 @@ export function SessionFeed({ isLast={itemIndex === total - 1} nextLabel={itemIndex === total - 1 ? t("review.seeResults") : undefined} /> + ))} @@ -226,7 +228,12 @@ export function SessionFeed({ >
{finished - ? renderResults({ results, items, correct, total: attemptedTotal, score }) + ? ( + <> + {renderResults({ results, items, correct, total: attemptedTotal, score })} + + + ) : (

{t("review.results")}

diff --git a/components/StudyFeed.tsx b/components/StudyFeed.tsx index eb50174..811ebb8 100644 --- a/components/StudyFeed.tsx +++ b/components/StudyFeed.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityRenderer } from "@/components/activities"; +import { AiDisclaimer } from "@/components/AiDisclaimer"; import { celebrate } from "@/lib/confetti"; import { ApiError, generateDeck } from "@/lib/client-api"; import { materialiseActivities } from "@/lib/deck-ops"; @@ -326,6 +327,7 @@ export function StudyFeed({ deck }: { deck: Deck }) { onAnswered={(correct, meta) => handleAnswered(activity, correct, meta)} onNext={() => goTo(activityIndex + 1)} /> +
))} @@ -350,6 +352,7 @@ export function StudyFeed({ deck }: { deck: Deck }) { regenerating={regenerating} regenerateError={regenerateError} /> +
diff --git a/public/sw.js b/public/sw.js index 9cb3f33..02404c3 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v39"; +const CACHE_VERSION = "freezmo-v40"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/phase2-test.mjs b/tests/phase2-test.mjs index e867858..cec0e41 100644 --- a/tests/phase2-test.mjs +++ b/tests/phase2-test.mjs @@ -119,7 +119,13 @@ const browser = await launchBrowser(); await page.getByRole("button", { name: "Submit answer" }).click(); await page.waitForTimeout(800); check("grading: AI feedback rendered", await page.getByText("Nice — scattering does the work.").isVisible()); - check("grading: AI disclaimer under the feedback", await page.getByText("Freezmo uses AI and can make mistakes").isVisible()); + // Scope it to the placement: the disclaimer directly under the graded box. + const feedbackDisclaimer = await page.evaluate(() => { + const feedback = [...document.querySelectorAll("p")].find((el) => el.textContent.includes("Nice — scattering does the work.")); + const next = feedback?.closest("div")?.nextElementSibling; + return Boolean(next?.textContent?.includes("Freezmo uses AI")); + }); + check("grading: AI disclaimer under the feedback", feedbackDisclaimer); await context.close(); } @@ -184,7 +190,6 @@ const browser = await launchBrowser(); title: "Flashcards", activities: [ { type: "flashcard", id: "f1", xp: 10, topic: "Sky", front: "Why is the sky blue?", back: "Rayleigh scattering.", explanation: "e" }, - { type: "flashcard", id: "f2", xp: 10, topic: "Sky", front: "Second question", back: "Second answer", explanation: "e" }, ], }; await seed(page, { decks: [flashDeck] }); @@ -201,6 +206,10 @@ const browser = await launchBrowser(); const atRest = await faces(); check("flashcard: front face painted at rest", atRest.front === "visible" && atRest.back === "hidden", JSON.stringify(atRest)); + check( + "feed: AI disclaimer under the card", + (await page.getByText("Freezmo uses AI and can make mistakes").count()) >= 1, + ); await page.locator("button.group").first().click(); await page.waitForTimeout(900); const flipped = await faces(); @@ -214,6 +223,19 @@ const browser = await launchBrowser(); await page.waitForTimeout(1000); const back = await faces(); check("flashcard: flipping back restores the question face", back.front === "visible" && back.back === "hidden", JSON.stringify(back)); + + // Answer it and check the results slide carries the disclaimer too. + await page.locator("button.group").first().click(); + await page.waitForTimeout(800); + await page.getByRole("button", { name: "I knew it" }).click(); + await page.waitForTimeout(500); + await page.getByRole("button", { name: "See results" }).click(); + await page.waitForTimeout(800); + const resultsDisclaimer = await page.evaluate(() => { + const slides = [...document.querySelectorAll("[data-testid='study-feed'] section")]; + return slides[slides.length - 1]?.textContent?.includes("Freezmo uses AI") ?? false; + }); + check("results: AI disclaimer on the results slide", resultsDisclaimer); await context.close(); } @@ -281,6 +303,7 @@ const browser = await launchBrowser(); (await page.getByText("What colour is a clear daytime sky?").count()) === 0 && (await page.getByText("What makes the sky blue?").count()) >= 1, ); + check("exam: AI disclaimer under the card", (await page.getByText("Freezmo uses AI and can make mistakes").count()) >= 1); await context.close(); }