feat(options): letterless student/projector UI + text-on-wire submit

Both student-facing surfaces (the per-device join page and the
front-of-room projector) now render options as text only, no A/B/C/D
chips, no numeric prefixes. The letter-namespace remains internal:
canonical A..D in pool.json + submissions storage; canonical position
1..4 in the CSV export. Admin dashboard keeps letters because it is the
instructor's private console.

Why this lands now: the discussion of per-student option shuffling
flagged that A/B/C/D as discussion handles ("the answer is B") is itself
a leaky channel. Removing the labels closes that channel for the
non-shuffled case and keeps it closed if shuffling is added later.

Wire protocol: the submit message carries the option's full text
("answer": "Pipelining"). Server's submit_answer resolves text -> letter
via app.pool.resolve_option_key, which also accepts a canonical letter
so internal callers and tests stay readable. A non-matching string is
recorded as a zero-score submission with answer=NULL, locked in via the
PK + existing_submit_ack short-circuit. So an attempted UI bypass that
posts a fabricated string just produces a wrong answer; no retry.

CSV: A=1 .. D=2 .. C=3 .. D=4 in the answer column. Empty when no option
matched. Header unchanged so downstream pandas readers don't break, but
the value type is int instead of letter.

Histogram: the failsafe "submitted-but-no-match" row buckets into
"missed" alongside genuine misses — both yield zero credit and the
instructor cares about the same thing.

Tests:
- test_submit_accepts_option_text_resolves_to_canonical: production
  wire format produces correct grading + canonical-letter storage.
- test_submit_failsafe_locks_in_zero_score_on_garbage_text: a non-
  matching string is recorded at score=0 and a follow-up correct
  submission cannot overwrite it.

71/71 green.
This commit is contained in:
ameer
2026-05-04 17:31:12 +08:00
parent 464c6ee1cb
commit 168cffea8b
9 changed files with 158 additions and 30 deletions

View File

@@ -1061,7 +1061,9 @@ h2.question-text.small {
}
.answer-btn {
display: grid;
grid-template-columns: 36px 1fr;
/* Letterless: option text fills the row. Generous padding compensates
* for the missing letter chip so the button still reads as a tile. */
grid-template-columns: 1fr;
gap: 18px;
align-items: center;
text-align: left;
@@ -1127,10 +1129,22 @@ h2.question-text.small {
.answer-btn .answer-text {
font-family: var(--font-display);
font-weight: 500;
font-size: 1.1rem;
font-size: 1.18rem;
line-height: 1.35;
}
/* Student-reveal letterless variant: drop the 32px key column from the
* options row. The "Your pick" ribbon (.options.student-reveal li.yours)
* and correct/wrong tinting still work because they target the <li>. */
.options.letterless {
/* options li uses display:grid; redefine the columns when letterless. */
}
.options.letterless li {
grid-template-columns: 1fr auto;
padding-left: 18px;
padding-right: 18px;
}
.big-score {
font-family: var(--font-mono);
font-size: 4.2rem;