UI iteration 1: app shell + full LaTeX pipeline
Layout:
- Home = knowledge-graph placeholder (main view) with new-session input
docked below; session list moved out of home into a hamburger drawer
- SessionDrawer: slide-over session navigation (backdrop/ESC close, active
highlight, fetches /api/chats on open) on both home and chat headers
- Chat header loses the back button — the drawer is the navigation
Math (inline $ was silently not rendering):
- @streamdown/math defaults singleDollarTextMath:false — now
createMathPlugin({ singleDollarTextMath: true }) in message + reasoning
- katex.min.css imported in layout (streamdown does not inject plugin
styles; display math only looked right via browser MathML fallback)
- QuizCard/AnsweredCard route question/options/explanation through
MessageResponse; user chat bubbles render markdown+math too
- Currency hazard: micromark-extension-math@3.1 lacks pandoc's
closing-digit rule, so bare '$5 and $10' parses as math — defense is at
the source: system prompt now instructs escaped \$5 / 'USD' for money
Hydration fix (React #418):
- QuizCard shuffled options with Math.random() — server/client orders
diverged. shuffleOptions now takes an injectable RNG; QuizCard passes
seededRandom(toolCallId) (mulberry32/FNV-1a in question-core). Same
question → same stable order across reloads; still varies per question.
Tests: 53 pass (5 new math-rendering pins incl. fenced-vs-one-line $$
behavior and the escaped-currency contract). tsc + next build clean.
Verified live in browser: inline/display math in text + cards, drawer
open/navigate/active-highlight/ESC, no console errors, no font 404s.
This commit is contained in:
@@ -182,6 +182,8 @@ Whenever math notation is involved — explanations, questions, quiz options and
|
||||
|
||||
If LaTeX can be used, it should be. Write $f(x) = x^2$, not `f(x) = x^2`. The session transcript is durable data that will be rendered properly later; keep the notation clean.
|
||||
|
||||
One hazard: a bare `$` starts math. NEVER write currency as `$5` — write `\$5` (escaped) or `5 USD`. This applies to quiz questions, options, and explanations too.
|
||||
|
||||
## Session shape — one more thing
|
||||
|
||||
Every session you run produces the artifact from which the learner's knowledge graph is built: the quiz outcomes, the dependency map, the propositions you teach. Teach in clean, explicit units — each node stated crisply (e.g. "**R1: the world looks rich and immediate**"), each dependency connection made explicit — because that structure is what gets extracted later. Vague, hedged, mushy node statements can't be extracted; crisp ones can. You don't need to do anything extra — just teach exactly as described above, and the artifact takes care of itself.
|
||||
|
||||
@@ -115,10 +115,35 @@ export function normalizeOptions(
|
||||
|
||||
/** Fisher-Yates over a copy. correctAnswer is keyed by value, so display
|
||||
* order can be shuffled safely before resolving indices. */
|
||||
export function shuffleOptions(options: QuizOption[]): QuizOption[] {
|
||||
// ─── options ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** mulberry32 seeded by a string — deterministic shuffle for hydration-safe rendering. */
|
||||
export function seededRandom(seed: string): () => number {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
h ^= seed.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
let a = h >>> 0;
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fisher–Yates with an injectable RNG. Pass `seededRandom(toolCallId)` in
|
||||
* rendered components: Math.random() shuffles differently on server vs client
|
||||
* and breaks hydration (React #418). A per-toolCallId seed keeps the order
|
||||
* stable across reloads while still varying it per question.
|
||||
*/
|
||||
export function shuffleOptions(options: QuizOption[], rand: () => number = Math.random): QuizOption[] {
|
||||
const out = [...options];
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const j = Math.floor(rand() * (i + 1));
|
||||
const a = out[i]!;
|
||||
const b = out[j]!;
|
||||
out[i] = b;
|
||||
|
||||
Reference in New Issue
Block a user