-
diff --git a/app/globals.css b/app/globals.css
index b13f9f7..9c03607 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -982,6 +982,18 @@ html.dark body {
color: var(--foreground);
}
+.note-editor .note-inline-math-block math-field,
+.note-renderer .note-inline-math-block math-field {
+ width: auto;
+ max-width: 100%;
+ min-width: 4rem;
+}
+
+.note-editor .note-inline-math-block math-field::part(container),
+.note-renderer .note-inline-math-block math-field::part(container) {
+ padding: 0;
+}
+
.note-renderer .katex-display {
margin: 0.25rem 0;
overflow-x: auto;
@@ -1019,15 +1031,3 @@ html.dark body {
user-select: none;
pointer-events: none;
}
-
-/* ---- Inline math spans ---- */
-.note-inline-math {
- display: inline;
- font-family: var(--font-mono), monospace;
- font-size: 0.9em;
- color: var(--color-accent);
- background: var(--color-accent-soft);
- border-radius: 3px;
- padding: 0.05em 0.3em;
- white-space: nowrap;
-}
diff --git a/app/quizzes/quizzes-client.test.tsx b/app/quizzes/quizzes-client.test.tsx
index cabf33c..74c8263 100644
--- a/app/quizzes/quizzes-client.test.tsx
+++ b/app/quizzes/quizzes-client.test.tsx
@@ -1,6 +1,6 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { afterEach, describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import { QuizzesClient } from "@/app/quizzes/quizzes-client";
import type { QuizQuestion } from "@/lib/quizzes/gemini";
import type { SavedQuiz } from "@/lib/quizzes/types";
@@ -44,16 +44,25 @@ const savedQuiz: SavedQuiz = {
updatedAt: "2026-05-29T18:00:00.000Z",
};
+function jsonResponse(payload: unknown, init?: ResponseInit) {
+ return {
+ ok: init?.status ? init.status < 400 : true,
+ status: init?.status ?? 200,
+ json: async () => payload,
+ } as Response;
+}
+
describe("QuizzesClient", () => {
afterEach(() => {
cleanup();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
});
it("renders library as quiz dashboard with stats and empty state", () => {
render(
);
expect(screen.getByTestId("quizzes-one-page")).toHaveClass("h-full", "overflow-hidden");
- expect(screen.getByRole("heading", { name: "My Quizzes" })).toBeInTheDocument();
expect(screen.getByText("0 quizzes")).toBeInTheDocument();
expect(screen.getByText("0 questions")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /create quiz/i })).toBeInTheDocument();
@@ -67,7 +76,6 @@ describe("QuizzesClient", () => {
await user.click(screen.getByRole("button", { name: /create quiz/i }));
- expect(screen.getByRole("heading", { name: "Quizzes" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Create Quiz" })).toBeInTheDocument();
expect(screen.getByText("Context")).toBeInTheDocument();
expect(screen.getByText("Preview")).toBeInTheDocument();
@@ -83,7 +91,8 @@ describe("QuizzesClient", () => {
const user = userEvent.setup();
render(
);
- await user.click(screen.getByRole("button", { name: /edit/i }));
+ await user.click(screen.getByRole("button", { name: /Open actions for Loop quiz/ }));
+ await user.click(screen.getByRole("button", { name: "Edit" }));
expect(screen.getByRole("heading", { name: "Preview Quiz" })).toBeInTheDocument();
expect(screen.getByLabelText(/quiz name/i)).toHaveValue("Loop quiz");
@@ -94,4 +103,35 @@ describe("QuizzesClient", () => {
expect(screen.getAllByLabelText("Prompt")).toHaveLength(1);
expect(screen.getAllByText("Question 2").length).toBeGreaterThan(0);
});
+
+ it("normalizes edited quiz math to LaTeX before saving", async () => {
+ const user = userEvent.setup();
+ const normalizedQuiz: SavedQuiz = {
+ ...savedQuiz,
+ questions: [
+ {
+ ...question,
+ prompt: "Area $\\pi r^2$",
+ },
+ ],
+ };
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse({ quiz: normalizedQuiz })));
+
+ render(
);
+
+ await user.click(screen.getByRole("button", { name: /Open actions for Loop quiz/ }));
+ await user.click(screen.getByRole("button", { name: "Edit" }));
+ await user.clear(screen.getByLabelText("Prompt"));
+ await user.type(screen.getByLabelText("Prompt"), "Area $πr²$");
+ await user.click(screen.getByRole("button", { name: /save quiz/i }));
+
+ expect(fetch).toHaveBeenCalledTimes(1);
+ expect(JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body))).toMatchObject({
+ questions: [
+ {
+ prompt: "Area $\\pi r^2$",
+ },
+ ],
+ });
+ });
});
diff --git a/app/quizzes/quizzes-client.tsx b/app/quizzes/quizzes-client.tsx
index 1a14528..642e441 100644
--- a/app/quizzes/quizzes-client.tsx
+++ b/app/quizzes/quizzes-client.tsx
@@ -11,17 +11,13 @@ import {
Loader2,
MoreHorizontal,
Pencil,
- Play,
Plus,
RotateCcw,
Save,
Trash2,
XCircle,
} from "lucide-react";
-import ReactMarkdown from "react-markdown";
-import rehypeKatex from "rehype-katex";
-import remarkGfm from "remark-gfm";
-import remarkMath from "remark-math";
+import { LatexMarkdown } from "@/components/math/latex-markdown";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -37,6 +33,7 @@ import {
gradeObjectiveAnswer,
summarizeAttempt,
} from "@/lib/quizzes/attempts";
+import { normalizeTextMathToLatex } from "@/lib/math/latex";
import type {
QuizDifficulty,
QuizQuestion,
@@ -123,61 +120,6 @@ async function readJsonResponse
(response: Response): Promise {
return payload;
}
-function MarkdownText({
- markdown,
- className,
-}: {
- markdown: string;
- className?: string;
-}) {
- return (
-
-
{children}
,
- ul: ({ children }) => (
-
- ),
- ol: ({ children }) => (
- {children}
- ),
- li: ({ children }) => {children},
- strong: ({ children }) => (
-
- {children}
-
- ),
- em: ({ children }) => {children},
- code: ({ children, className: codeClassName }) => (
-
- {children}
-
- ),
- pre: ({ children }) => (
-
- {children}
-
- ),
- blockquote: ({ children }) => (
-
- {children}
-
- ),
- }}
- >
- {markdown}
-
-
- );
-}
-
function StepPill({
active,
children,
@@ -207,6 +149,16 @@ function StatPill({ children }: { children: ReactNode }) {
);
}
+function normalizeQuestionMath(question: QuizQuestion): QuizQuestion {
+ return {
+ ...question,
+ prompt: normalizeTextMathToLatex(question.prompt),
+ correctAnswer: normalizeTextMathToLatex(question.correctAnswer),
+ explanation: normalizeTextMathToLatex(question.explanation),
+ choices: question.choices?.map(normalizeTextMathToLatex),
+ };
+}
+
function makeDraftQuestion(sourceNoteTitles: string[]): QuizQuestion {
return {
id: crypto.randomUUID(),
@@ -456,6 +408,24 @@ export function QuizzesClient({
);
}
+ function normalizeDraftQuestionField(
+ questionIndex: number,
+ field: "prompt" | "correctAnswer" | "explanation",
+ value: string,
+ ) {
+ updateDraftQuestion(questionIndex, {
+ [field]: normalizeTextMathToLatex(value),
+ });
+ }
+
+ function normalizeDraftChoice(
+ questionIndex: number,
+ choiceIndex: number,
+ value: string,
+ ) {
+ updateDraftChoice(questionIndex, choiceIndex, normalizeTextMathToLatex(value));
+ }
+
function updateDraftQuestionType(index: number, type: QuizQuestionType) {
setDraftQuestions((current) =>
current.map((question, questionIndex) => {
@@ -505,8 +475,9 @@ export function QuizzesClient({
setError(null);
setIsSaving(true);
try {
+ const normalizedQuestions = draftQuestions.map(normalizeQuestionMath);
const sourceNoteTitles = getSourceTitles(
- draftQuestions,
+ normalizedQuestions,
notes,
selectedNoteIds,
);
@@ -520,7 +491,7 @@ export function QuizzesClient({
title: draftTitle,
sourceNoteIds: selectedNoteIds,
sourceNoteTitles,
- questions: draftQuestions,
+ questions: normalizedQuestions,
difficulty,
mode,
questionTypes,
@@ -632,11 +603,12 @@ export function QuizzesClient({
}
function updateAnswer(questionId: string, value: string) {
+ const normalizedAnswer = normalizeTextMathToLatex(value);
setAnswers((current) => ({
...current,
[questionId]: {
questionId,
- answer: value,
+ answer: normalizedAnswer,
evaluation: current[questionId]?.evaluation,
},
}));
@@ -849,6 +821,7 @@ export function QuizzesClient({
-