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:
Generated
+1536
-4
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user