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"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"katex": "^0.18.4",
|
||||||
"react": "^19.0.0",
|
"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": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
|||||||
@@ -89,6 +89,88 @@
|
|||||||
color: var(--color-on-surface);
|
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 --- */
|
/* --- ACTION BUTTONS --- */
|
||||||
.message__actions {
|
.message__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
======================================== */
|
======================================== */
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
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";
|
import "./Message.css";
|
||||||
|
|
||||||
const Message = ({ message }) => {
|
const Message = ({ message }) => {
|
||||||
@@ -27,8 +31,19 @@ const Message = ({ message }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="message__bubble">
|
<div className="message__bubble">
|
||||||
<div className="message__text">
|
<div className={`message__text ${!isUser ? "markdown-body" : ""}`}>
|
||||||
{text || (streaming ? "" : null)}
|
{isUser ? (
|
||||||
|
text
|
||||||
|
) : (
|
||||||
|
text && (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkMath]}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)
|
||||||
|
)}
|
||||||
{streaming && <span className="message__typing-dots">
|
{streaming && <span className="message__typing-dots">
|
||||||
<span className="message__typing-dot"></span>
|
<span className="message__typing-dot"></span>
|
||||||
<span className="message__typing-dot"></span>
|
<span className="message__typing-dot"></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user