Report question: skip a broken card and tell the server
A generated matching card arrived on a phone with three identical definitions (the generation guards now drop those), but there was no way to get past a bad card from inside a session. The feed chrome (deck study, review, mistakes and exam) now has the red "!" control: one tap records the question, skips to the next card and shows an Undo toast. - lib/question-reports.ts: the local list (freezmo.reports.v1, zod on read, capped 200) is the skip state — reported cards leave every session type, count as "not attempted" in the results instead of wrong, and Undo takes a report back. A deck where everything is reported stays studiable so it can be regenerated. - POST /api/reports appends one JSON line per report to REPORTS_DIR/reports.jsonl (shared/freezmo/reports in production, the dev data dir in dev). Deliberately write-only: the question text is readable on the server (that is the point — prompt tuning) but never over HTTP, since the app has no auth. Best effort: offline reports stay local. - The button: a red outlined rounded square with "!", the chrome's control size, in both feed headers (hidden on the results slide). - config: REPORTS_DIR + RATE_LIMIT_REPORTS (20); the deploy creates the shared reports dir; the dev unit and AGENTS.md note the new server data location. - Suites: phase-1 (append + validation + no GET), phase-2 (button, toast, server POST, local copy, skip counter, hidden next session, Undo, exam pool). SW -> freezmo-v38.
This commit is contained in:
parent
f202653017
commit
9cea13496a
16 changed files with 496 additions and 15 deletions
44
components/ReportQuestionButton.tsx
Normal file
44
components/ReportQuestionButton.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import { haptic } from "@/lib/haptics";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { reportQuestion, unreportQuestion } from "@/lib/question-reports";
|
||||
import type { Activity, Deck } from "@/lib/schemas";
|
||||
import { showToast } from "@/lib/toast";
|
||||
|
||||
/**
|
||||
* The red "!" report control in the feed chrome: records the question, sends
|
||||
* the operator a copy and skips past it. Undo lives in the toast.
|
||||
*/
|
||||
export function ReportQuestionButton({
|
||||
deck,
|
||||
activity,
|
||||
onSkip,
|
||||
}: {
|
||||
deck: Deck;
|
||||
activity: Activity;
|
||||
onSkip: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
haptic("tap");
|
||||
reportQuestion({ deck, activity });
|
||||
showToast({
|
||||
message: t("feed.reported"),
|
||||
actionLabel: t("toast.undo"),
|
||||
onAction: () => unreportQuestion(deck.id, activity.id),
|
||||
});
|
||||
onSkip();
|
||||
}}
|
||||
aria-label={t("feed.report")}
|
||||
title={t("feed.report")}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-xl border-2 border-rose-400/80 text-[15px] font-black leading-none text-rose-600 transition hover:bg-rose-50 dark:border-rose-400/50 dark:text-rose-300 dark:hover:bg-rose-400/10"
|
||||
>
|
||||
<span aria-hidden>!</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue