feat: render assistant replies as markdown

Add react-markdown + remark-math + rehype-katex to render the AI's
markdown (headings, bold, lists, code, blockquotes, tables) and LaTeX
math ($H_2O$ etc.). ReactMarkdown escapes raw HTML, keeping the stored
message rendering safe from XSS. Styled via .markdown-body in Message.css.
Applied to assistant messages only; user text stays plain.
This commit is contained in:
2026-08-19 09:55:25 -04:00
parent 00a181134d
commit 56d6f355d1
4 changed files with 1640 additions and 7 deletions
+1536 -4
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -9,8 +9,12 @@
"preview": "vite preview"
},
"dependencies": {
"katex": "^0.18.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
@@ -89,6 +89,88 @@
color: var(--color-on-surface);
}
/* --- MARKDOWN RENDERED CONTENT (assistant) --- */
.message__text.markdown-body {
white-space: normal;
}
.markdown-body > *:first-child { margin-top: 0; }
.markdown-body > *:last-child { margin-bottom: 0; }
.markdown-body p { margin: 0 0 12px; }
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4 {
margin: 20px 0 10px;
font-family: var(--font-heading);
font-weight: 700;
line-height: 1.3;
}
.markdown-body h1 { font-size: 22px; }
.markdown-body h2 { font-size: 19px; }
.markdown-body h3 { font-size: 16px; }
.markdown-body h4 { font-size: 15px; }
.markdown-body ul,
.markdown-body ol {
margin: 0 0 12px;
padding-left: 22px;
}
.markdown-body li { margin-bottom: 4px; }
.markdown-body blockquote {
margin: 0 0 12px;
padding: 6px 14px;
border-left: 4px solid var(--color-primary);
background: var(--color-surface-lowest);
border-radius: 0 8px 8px 0;
color: var(--color-on-surface-variant);
}
.markdown-body code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9em;
background: rgba(0, 0, 0, 0.06);
padding: 1px 5px;
border-radius: 4px;
}
.markdown-body pre {
margin: 0 0 12px;
padding: 12px 14px;
background: #111827;
color: #e5e7eb;
border-radius: 8px;
overflow-x: auto;
white-space: pre;
}
.markdown-body pre code {
background: transparent;
padding: 0;
color: inherit;
}
.markdown-body hr {
margin: 18px 0;
border: none;
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.markdown-body table {
border-collapse: collapse;
margin: 0 0 12px;
width: 100%;
}
.markdown-body th,
.markdown-body td {
border: 1px solid rgba(0, 0, 0, 0.12);
padding: 6px 10px;
text-align: left;
}
.markdown-body th {
background: rgba(0, 0, 0, 0.04);
font-weight: 600;
}
.markdown-body a {
color: var(--color-primary);
text-decoration: underline;
}
/* KaTeX math inherits font size */
.markdown-body .katex { font-size: 1.05em; }
/* --- ACTION BUTTONS --- */
.message__actions {
display: flex;
+17 -2
View File
@@ -3,6 +3,10 @@
======================================== */
import { useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import "./Message.css";
const Message = ({ message }) => {
@@ -27,8 +31,19 @@ const Message = ({ message }) => {
</div>
)}
<div className="message__bubble">
<div className="message__text">
{text || (streaming ? "" : null)}
<div className={`message__text ${!isUser ? "markdown-body" : ""}`}>
{isUser ? (
text
) : (
text && (
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
>
{text}
</ReactMarkdown>
)
)}
{streaming && <span className="message__typing-dots">
<span className="message__typing-dot"></span>
<span className="message__typing-dot"></span>