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:
+6
-7
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport, isToolUIPart, lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
||||
import {
|
||||
@@ -26,6 +25,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { QuizCard } from "@/components/quiz-card";
|
||||
import { AnsweredCard } from "@/components/answered-card";
|
||||
import { SessionDrawer } from "@/components/session-drawer";
|
||||
import type { TeacherUIMessage } from "@/src/agent/ui-message";
|
||||
import type { QuestionAnswerData } from "@/src/core/question-core";
|
||||
import { findPendingToolCall } from "@/src/core/pending";
|
||||
@@ -46,7 +46,6 @@ export function Chat({
|
||||
initialMessages: TeacherUIMessage[];
|
||||
initialPrompt?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { messages, sendMessage, addToolOutput, status, error, stop } =
|
||||
useChat<TeacherUIMessage>({
|
||||
id: chatId,
|
||||
@@ -80,10 +79,8 @@ export function Chat({
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col">
|
||||
<header className="flex items-center gap-3 border-b px-4 py-3">
|
||||
<Button variant="ghost" size="sm" onClick={() => router.push("/")}>
|
||||
← Sessions
|
||||
</Button>
|
||||
<header className="flex items-center gap-3 border-b px-3 py-2">
|
||||
<SessionDrawer />
|
||||
<div className="min-w-0 flex-1 truncate font-medium">{title}</div>
|
||||
{busy && (
|
||||
<Button variant="outline" size="sm" onClick={() => stop()}>
|
||||
@@ -105,7 +102,9 @@ export function Chat({
|
||||
<Message key={m.id} from={m.role}>
|
||||
<MessageContent>
|
||||
{m.role === "user" ? (
|
||||
<div className="whitespace-pre-wrap">{m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")}</div>
|
||||
<MessageResponse>
|
||||
{m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")}
|
||||
</MessageResponse>
|
||||
) : (
|
||||
m.parts.map((part, pi) => {
|
||||
const isLastPart = mi === messages.length - 1 && pi === m.parts.length - 1;
|
||||
|
||||
Reference in New Issue
Block a user