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:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AskToolInput, QuizToolInput } from "@/src/agent/tools";
|
||||
import {
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
coerceCorrectAnswer,
|
||||
normalizeOptions,
|
||||
resolveCorrect,
|
||||
seededRandom,
|
||||
shuffleOptions,
|
||||
type AnswerRequest,
|
||||
type QuestionAnswerData,
|
||||
@@ -37,8 +39,9 @@ export function QuizCard({ toolName, toolCallId, input, onSubmit }: QuizCardProp
|
||||
const kind = toolName === "quiz" ? ("quiz" as const) : ("ask" as const);
|
||||
|
||||
// Build the canonical QuestionData once per question. Quiz options are
|
||||
// shuffled so position never leaks correctness; the display order is
|
||||
// snapshotted into the answer record at submit time.
|
||||
// deterministically shuffled (seeded by toolCallId) so position never leaks
|
||||
// correctness AND server/client render the same order (hydration-safe);
|
||||
// the display order is snapshotted into the answer record at submit time.
|
||||
const qd = useMemo<QuestionData>(() => {
|
||||
const quizInput = kind === "quiz" ? (input as QuizToolInput) : null;
|
||||
const askInput = kind === "ask" ? (input as AskToolInput) : null;
|
||||
@@ -50,7 +53,7 @@ export function QuizCard({ toolName, toolCallId, input, onSubmit }: QuizCardProp
|
||||
: "single-select"
|
||||
: (askInput?.mode ?? (askInput?.options?.length ? (multi ? "multi-select" : "single-select") : "text"));
|
||||
const options = normalizeOptions((quizInput?.options ?? askInput?.options ?? []) as { label: string; value?: string }[]);
|
||||
const ordered = kind === "quiz" && options.length > 0 ? shuffleOptions(options) : options;
|
||||
const ordered = kind === "quiz" && options.length > 0 ? shuffleOptions(options, seededRandom(toolCallId)) : options;
|
||||
const q: QuestionData = {
|
||||
id: toolCallId,
|
||||
kind,
|
||||
@@ -125,8 +128,14 @@ export function QuizCard({ toolName, toolCallId, input, onSubmit }: QuizCardProp
|
||||
{kind === "quiz" ? "Quiz" : "Question"}
|
||||
{qd.mode === "multi-select" && " · select all that apply"}
|
||||
</div>
|
||||
<div className="mb-3 font-medium">{qd.question}</div>
|
||||
{qd.context && <div className="mb-3 text-sm text-muted-foreground">{qd.context}</div>}
|
||||
<div className="mb-3 font-medium">
|
||||
<MessageResponse>{qd.question}</MessageResponse>
|
||||
</div>
|
||||
{qd.context && (
|
||||
<div className="mb-3 text-sm text-muted-foreground">
|
||||
<MessageResponse>{qd.context}</MessageResponse>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{qd.mode !== "text" && (
|
||||
<div className="flex flex-col gap-2" role={qd.mode === "multi-select" ? "group" : "radiogroup"}>
|
||||
@@ -154,7 +163,7 @@ export function QuizCard({ toolName, toolCallId, input, onSubmit }: QuizCardProp
|
||||
{isSel && <span className={cn(qd.mode === "multi-select" ? "text-[10px]" : "size-1.5 rounded-full bg-current")}>{qd.mode === "multi-select" ? "✓" : ""}</span>}
|
||||
</span>
|
||||
<span>
|
||||
{opt.label}
|
||||
<MessageResponse>{opt.label}</MessageResponse>
|
||||
{opt.description && <span className="block text-xs text-muted-foreground">{opt.description}</span>}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user