AI disclaimer: also on the cards and the results
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.
This commit is contained in:
parent
396da6528a
commit
b5269d2844
5 changed files with 44 additions and 9 deletions
12
README.md
12
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`
|
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
|
(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
|
— 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
|
deck wizards, under their results, and under AI grading feedback. It also rides along with the
|
||||||
content (the deck page, study cards, explanations) deliberately carries no note: the disclaimer
|
content itself once it is being consumed: under every card in the study feed and in the review,
|
||||||
belongs to the moment of generation, not to browsing, and the empty states stay clean. It is a
|
mistakes and exam sessions, and on their results slides. Navigation surfaces (the library, deck
|
||||||
plain `--text-subtle` paragraph (documented at 5.6:1 light / 8.6:1 dark), centred, wrapping to
|
lists, chat history, settings) deliberately carry no note: the disclaimer belongs to the moment
|
||||||
two lines on a 360 px phone — not clickable, not dismissable.
|
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
|
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
|
flow and a 2-step homework flow that share the exact state and handlers with the desktop
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { ActivityRenderer } from "@/components/activities";
|
import { ActivityRenderer } from "@/components/activities";
|
||||||
|
import { AiDisclaimer } from "@/components/AiDisclaimer";
|
||||||
import { XIcon } from "@/components/icons";
|
import { XIcon } from "@/components/icons";
|
||||||
import { ReportQuestionButton } from "@/components/ReportQuestionButton";
|
import { ReportQuestionButton } from "@/components/ReportQuestionButton";
|
||||||
import { ProgressBar } from "@/components/ui";
|
import { ProgressBar } from "@/components/ui";
|
||||||
|
|
@ -216,6 +217,7 @@ export function SessionFeed({
|
||||||
isLast={itemIndex === total - 1}
|
isLast={itemIndex === total - 1}
|
||||||
nextLabel={itemIndex === total - 1 ? t("review.seeResults") : undefined}
|
nextLabel={itemIndex === total - 1 ? t("review.seeResults") : undefined}
|
||||||
/>
|
/>
|
||||||
|
<AiDisclaimer />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|
@ -226,7 +228,12 @@ export function SessionFeed({
|
||||||
>
|
>
|
||||||
<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]">
|
||||||
{finished
|
{finished
|
||||||
? renderResults({ results, items, correct, total: attemptedTotal, score })
|
? (
|
||||||
|
<>
|
||||||
|
{renderResults({ results, items, correct, total: attemptedTotal, score })}
|
||||||
|
<AiDisclaimer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
: (
|
: (
|
||||||
<div className="card space-y-2 p-5 text-center">
|
<div className="card space-y-2 p-5 text-center">
|
||||||
<p className="text-sm font-semibold text-[var(--text-muted)]">{t("review.results")}</p>
|
<p className="text-sm font-semibold text-[var(--text-muted)]">{t("review.results")}</p>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Link from "next/link";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { ActivityRenderer } from "@/components/activities";
|
import { ActivityRenderer } from "@/components/activities";
|
||||||
|
import { AiDisclaimer } from "@/components/AiDisclaimer";
|
||||||
import { celebrate } from "@/lib/confetti";
|
import { celebrate } from "@/lib/confetti";
|
||||||
import { ApiError, generateDeck } from "@/lib/client-api";
|
import { ApiError, generateDeck } from "@/lib/client-api";
|
||||||
import { materialiseActivities } from "@/lib/deck-ops";
|
import { materialiseActivities } from "@/lib/deck-ops";
|
||||||
|
|
@ -326,6 +327,7 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
onAnswered={(correct, meta) => handleAnswered(activity, correct, meta)}
|
onAnswered={(correct, meta) => handleAnswered(activity, correct, meta)}
|
||||||
onNext={() => goTo(activityIndex + 1)}
|
onNext={() => goTo(activityIndex + 1)}
|
||||||
/>
|
/>
|
||||||
|
<AiDisclaimer />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|
@ -350,6 +352,7 @@ export function StudyFeed({ deck }: { deck: Deck }) {
|
||||||
regenerating={regenerating}
|
regenerating={regenerating}
|
||||||
regenerateError={regenerateError}
|
regenerateError={regenerateError}
|
||||||
/>
|
/>
|
||||||
|
<AiDisclaimer />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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-v39";
|
const CACHE_VERSION = "freezmo-v40";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,13 @@ 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());
|
||||||
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();
|
await context.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,7 +190,6 @@ const browser = await launchBrowser();
|
||||||
title: "Flashcards",
|
title: "Flashcards",
|
||||||
activities: [
|
activities: [
|
||||||
{ type: "flashcard", id: "f1", xp: 10, topic: "Sky", front: "Why is the sky blue?", back: "Rayleigh scattering.", explanation: "e" },
|
{ 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] });
|
await seed(page, { decks: [flashDeck] });
|
||||||
|
|
@ -201,6 +206,10 @@ const browser = await launchBrowser();
|
||||||
|
|
||||||
const atRest = await faces();
|
const atRest = await faces();
|
||||||
check("flashcard: front face painted at rest", atRest.front === "visible" && atRest.back === "hidden", JSON.stringify(atRest));
|
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.locator("button.group").first().click();
|
||||||
await page.waitForTimeout(900);
|
await page.waitForTimeout(900);
|
||||||
const flipped = await faces();
|
const flipped = await faces();
|
||||||
|
|
@ -214,6 +223,19 @@ const browser = await launchBrowser();
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
const back = await faces();
|
const back = await faces();
|
||||||
check("flashcard: flipping back restores the question face", back.front === "visible" && back.back === "hidden", JSON.stringify(back));
|
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();
|
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 colour is a clear daytime sky?").count()) === 0 &&
|
||||||
(await page.getByText("What makes the sky blue?").count()) >= 1,
|
(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();
|
await context.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue