From cb0b7ea3054de048e5ecb0e372bd4875bdccc8b1 Mon Sep 17 00:00:00 2001 From: Vijay Janapa Reddi Date: Fri, 24 Apr 2026 16:10:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(staffml=20practice):=20restructure=20layou?= =?UTF-8?q?t=20per=20UX=20review=20=E2=80=94=20reading+answering=20on=20on?= =?UTF-8?q?e=20column,=20tools=20on=20the=20other?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Responds to a 4-reviewer UX pass (Emma, David, Chip Huyen, plus Soumith who bounced). Consensus: ship the read-and-answer vertical flow on the left, move tools to the right — but bundle in three safeguards that address each reviewer's structural concern. Layout changes: - LEFT column: title → STICKY Your-task callout → scenario → optional QuestionVisual diagram → answer textarea → reveal → post-reveal content (user's answer, model answer, rubric, self-assessment). One vertical flow — no more 500px eye-travel from reading on the left to typing on the right. - RIGHT column: tools panel — Ask Interviewer, Hardware reference, Napkin calculator. Each is a collapsible card; together they replace the previous dense stack of widgets around the textarea. Safeguards folded in: 1. Sticky 'Your task' callout (David's concern: long scenarios push the textarea below the fold, losing the ask while typing). Negative horizontal margins span full column width so the sticky bar's background cleanly covers scrolling scenario text. 2. Right-column defaults inverted per Chip Huyen's practitioner review. HardwareRef defaultOpen=true (consulted constantly during bandwidth/FLOPS calcs); NapkinCalc defaultOpen=false (invoked, not consulted). AskInterviewer inherits its own default-closed behavior so non-AI users see a thin header, not a giant empty chat panel signalling 'you're doing this wrong'. 3. Submit-gradient safeguard (Chip Huyen's most subtle warning). Removing the eye-travel friction made the Reveal button too frictionless — users would type two sentences and reveal. handleReveal now gates on (elapsed<15s && chars<50), surfacing a one-time 'Think longer?' confirm. deliberationCalibrated ref self-turns the guard OFF once the user demonstrates normal deliberation (≥20s elapsed OR ≥80 chars) on any question. Power users see the guard at most once per session. 4. Beginner scaffolding (Emma's scaffolding request). A subtle 'Stuck? Use the Ask Interviewer panel →' nudge sits beneath the textarea, pointing to the right column. Discoverable without dominating the layout — exactly the 'teach users how to use the tools, not just access to them' pattern beginners need. Post-reveal UX: - modelAnswerRef + scrollIntoView({behavior: smooth, block: start}) after showAnswer flips. On long scenarios the model answer previously rendered below the fold; users had to hunt for it. Scroll-into-view plus scroll-mt-24 keeps the sticky header from overlapping the landing position. - Chain navigation + QuestionFeedback stay in the same column as the user's answer — vertical comparison, not horizontal. Analytics: - New 'think_guard_triggered' event fires when the submit-gradient guard pops the confirm dialog. Track this alongside the existing 'answer_response_time' event to validate that the redesign did not erode deliberation depth (Chip's predicted regression). --- interviews/staffml/src/app/practice/page.tsx | 808 +++++++++++-------- 1 file changed, 483 insertions(+), 325 deletions(-) diff --git a/interviews/staffml/src/app/practice/page.tsx b/interviews/staffml/src/app/practice/page.tsx index 702fe9c72..7ff4e14ac 100644 --- a/interviews/staffml/src/app/practice/page.tsx +++ b/interviews/staffml/src/app/practice/page.tsx @@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { Target, CheckCircle2, XCircle, Terminal, SkipForward, - Calculator, SlidersHorizontal, X + Calculator, SlidersHorizontal, X, Sparkles, Lightbulb, Clock } from "lucide-react"; import clsx from "clsx"; import HardwareRef from "@/components/HardwareRef"; @@ -37,6 +37,7 @@ import { Calendar, ArrowLeft, Flag, LinkIcon } from "lucide-react"; import Link from "next/link"; import { buildReportUrl } from "@/lib/issue-url"; import QuestionFeedback from "@/components/QuestionFeedback"; +import QuestionVisual from "@/components/QuestionVisual"; import { track } from "@/lib/analytics"; /** @@ -169,6 +170,22 @@ function PracticePage() { const [dailyDone, setDailyDone] = useState(false); const [sourceTopic, setSourceTopic] = useState<{ id: string; name: string } | null>(null); const [showStarGate, setShowStarGate] = useState(false); + // Submit-gradient safeguard (per Chip Huyen's UX review). + // Rationale: with the restructured layout, the Reveal button sits + // directly below the answer textarea — removing the eye-travel + // friction that previously enforced deliberation. Without this + // guard, users type two sentences and reveal. We detect + // low-effort reveals (<15s elapsed AND <50 chars typed) and + // surface a one-time "Think longer?" confirm so the UX doesn't + // silently erode learning depth. Once confirmed (or once the user + // has demonstrated normal deliberation on any question), the guard + // stays dismissed for the session. + const [thinkConfirmOpen, setThinkConfirmOpen] = useState(false); + const deliberationCalibrated = useRef(false); + // Post-reveal model-answer anchor so we can scroll-into-view when + // showAnswer flips. Prevents the model answer from rendering below + // the fold on long scenarios. + const modelAnswerRef = useRef(null); // Chain tracking — update when current question changes const chainInfo = current ? getChainForQuestion(current.id) : null; @@ -368,7 +385,33 @@ function PracticePage() { setRubricItems([]); }, [pool, current, showAnswer]); - const handleReveal = () => { + // Submit-gradient guard: intercept reveals that look like + // "didn't really try." The restructured layout puts the Reveal + // button directly below the textarea, so the eye-travel friction + // that previously enforced deliberation is gone. Without a + // deliberate pause, median time-before-reveal drops and + // self-assessed scores inflate without actual learning. We surface + // a one-time confirm, and once the user has demonstrated normal + // deliberation on any question (≥20s OR ≥80 chars typed), the + // guard calibrates itself off for the session. `force=true` skips + // the check — used by the confirm dialog's "Reveal anyway" button. + const handleReveal = (force: boolean = false) => { + if (!force && !deliberationCalibrated.current) { + const elapsedMs = Date.now() - questionShownAt.current; + const charsTyped = userAnswer.trim().length; + if (elapsedMs < 15000 && charsTyped < 50) { + setThinkConfirmOpen(true); + track({ type: 'think_guard_triggered' }); + return; + } + // Any reveal that passes the threshold marks this user as + // deliberating normally — don't pester them again this session. + if (elapsedMs >= 20000 || charsTyped >= 80) { + deliberationCalibrated.current = true; + } + } + setThinkConfirmOpen(false); + // Star gate check if (shouldShowGate()) { setShowStarGate(true); @@ -400,6 +443,15 @@ function PracticePage() { } setShowAnswer(true); + // After the reveal state commits, smoothly scroll the model + // answer into view. This is the primary pedagogical comparison + // moment — the user's answer should still be visible above, the + // model answer enters from below. Without this scroll, on long + // scenarios the model answer renders below the fold and the + // user has to hunt for it. + setTimeout(() => { + modelAnswerRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 50); // Track answer reveal with response time and napkin grade if (current) { @@ -829,132 +881,374 @@ function PracticePage() { Filters - {/* Main content */} + {/* + Main content — restructured 2026-04-24 based on UX feedback from + Emma (beginner), David (power user), and Chip Huyen (practitioner). + Key shifts: + - LEFT column = problem + answer + reveal + post-reveal (one + vertical reading/typing flow). No more 500px eye-travel from + scenario to textarea. + - RIGHT column = tools panel (Ask Interviewer, Hardware ref, + Napkin calc) — each a collapsible card so non-AI users don't + see wasted pixels shouting "use the AI." + - Your-task callout is STICKY at the top of the left scroll + container: scroll the scenario away, the question stays + visible (David's fix for long-scenario context loss). + - Submit-gradient safeguard: reveals triggered with <15s + elapsed AND <50 chars typed pop a "Think longer?" confirm. + Once the user demonstrates normal deliberation, the guard + self-calibrates off for the session (Chip's warning about + the new layout making premature reveals too frictionless). + - Post-reveal model answer scrolls into view via modelAnswerRef + so it doesn't render below the fold. + - "Stuck? Ask the Interviewer →" nudge beneath the textarea + so beginners discover the AI without it dominating the page + (Emma's scaffolding request + Chip's "don't advertise + unused features" rule). + */}
{current ? (
- {/* Question */} + {/* ── LEFT: problem + answer + reveal + post-reveal ── */}
-
- - -
- - - {current.competency_area} - - - {current.zone} - - - {current.track} - - - {/* Copy link */} - - {/* Report issue */} - - Report - -
- {chainInfo && !showAnswer && ( -
- setChainPreviewOpen((v) => !v)} - /> +
+ + + {/* Badges + copy-link + report */} +
+ + + {current.competency_area} + + + {current.zone} + + + {current.track} + + + + + Report +
- )} -

- {current.title} -

-
- {current.scenario ? ( -

- {cleanScenario(current.scenario)} -

- ) : ( - + + {/* Chain badge (pre-reveal only) */} + {chainInfo && !showAnswer && ( +
+ setChainPreviewOpen((v) => !v)} + /> +
)} -
- {/* - Your-task callout. Three states, in priority order: - 1. `current.question` present → render it as the - primary ask (post-backfill state). - 2. Scenario already ends with `?` → render nothing - extra; the interrogative is already in the prose. - 3. Neither → render a zone-aware inferred fallback - so the reader at least knows the shape of the - expected answer. Marked "inferred" so it's - clearly distinct from an authored question. - Placed AFTER the scenario because the scenario sets - context that the question refers to ("given the - above…"). A prompt above an unread scenario would - be meaningless. - */} - {current.question ? ( -
-
- - Your task -
-

- {current.question} -

-
- ) : current.scenario && !current.scenario.trim().endsWith("?") ? ( -
-
- - - Your task (inferred) - -
-

- {inferTaskPrompt(current.zone, current.bloom_level)} -

-
- ) : null} - {chainInfo && !showAnswer && chainPreviewOpen && ( -
- -
- )} + {/* Title */} +

+ {current.title} +

-
-
+ {/* + STICKY Your-task callout. Pins to the top of the + scroll container so the question stays visible + while the user scrolls to read more scenario or + compare their answer with the model. Negative + horizontal margins + re-added padding make the + sticky bar span the full left-column width so the + background covers scrolling text cleanly. + */} +
+ {current.question ? ( +
+
+ + Your task +
+

+ {current.question} +

+
+ ) : current.scenario && !current.scenario.trim().endsWith("?") ? ( +
+
+ + + Your task (inferred) + +
+

+ {inferTaskPrompt(current.zone, current.bloom_level)} +

+
+ ) : ( + /* Scenario ends with ?; no callout needed but reserve minimal spacing */ +
+ + Your task — see scenario below +
+ )} +
+ + {/* Scenario prose */} +
+ {current.scenario ? ( +

+ {cleanScenario(current.scenario)} +

+ ) : ( + + )} +
+ + {/* Visual diagram (optional) */} + {current.visual && ( + + )} + + {/* Pre-reveal chain sibling preview (toggle from ChainBadge) */} + {chainInfo && !showAnswer && chainPreviewOpen && ( +
+ +
+ )} + + {/* ── ANSWER + REVEAL (pre-reveal) ── */} + {!showAnswer ? ( +
+
+ + + {current.details.napkin_math ? "Your napkin math" : "Your answer"} + +
+