mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-19 20:03:08 +00:00
feat: add full markdown rendering to chat messages
Replace the custom BOLD_REGEX parser in ChatMessage.tsx with react-markdown + remark-gfm for proper rendering of headers, tables, lists, code blocks, blockquotes, links, and horizontal rules in all chat UIs (AssistantChat, SpecCreationChat, ExpandProjectChat). Changes: - Add react-markdown and remark-gfm dependencies - Add vendor-markdown chunk to Vite manual chunks for code splitting - Add .chat-prose CSS class with styles for all markdown elements - Add .chat-prose-user modifier for contrast on primary-colored bubbles - Replace line-splitting + regex logic with ReactMarkdown component - Links open in new tabs via custom component override - System messages remain plain text (unchanged) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
|
||||
import { memo } from 'react'
|
||||
import { Bot, User, Info } from 'lucide-react'
|
||||
import ReactMarkdown, { type Components } from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import type { ChatMessage as ChatMessageType } from '../lib/types'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
@@ -14,8 +16,16 @@ interface ChatMessageProps {
|
||||
message: ChatMessageType
|
||||
}
|
||||
|
||||
// Module-level regex to avoid recreating on each render
|
||||
const BOLD_REGEX = /\*\*(.*?)\*\*/g
|
||||
// Stable references for memo — avoids re-renders
|
||||
const remarkPlugins = [remarkGfm]
|
||||
|
||||
const markdownComponents: Components = {
|
||||
a: ({ children, href, ...props }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}
|
||||
|
||||
export const ChatMessage = memo(function ChatMessage({ message }: ChatMessageProps) {
|
||||
const { role, content, attachments, timestamp, isStreaming } = message
|
||||
@@ -86,39 +96,11 @@ export const ChatMessage = memo(function ChatMessage({ message }: ChatMessagePro
|
||||
)}
|
||||
|
||||
<Card className={`${config.bgColor} px-4 py-3 border ${isStreaming ? 'animate-pulse' : ''}`}>
|
||||
{/* Parse content for basic markdown-like formatting */}
|
||||
{content && (
|
||||
<div className={`whitespace-pre-wrap text-sm leading-relaxed ${config.textColor}`}>
|
||||
{content.split('\n').map((line, i) => {
|
||||
// Bold text - use module-level regex, reset lastIndex for each line
|
||||
BOLD_REGEX.lastIndex = 0
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
let match
|
||||
|
||||
while ((match = BOLD_REGEX.exec(line)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(line.slice(lastIndex, match.index))
|
||||
}
|
||||
parts.push(
|
||||
<strong key={`bold-${i}-${match.index}`} className="font-bold">
|
||||
{match[1]}
|
||||
</strong>
|
||||
)
|
||||
lastIndex = match.index + match[0].length
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
parts.push(line.slice(lastIndex))
|
||||
}
|
||||
|
||||
return (
|
||||
<span key={i}>
|
||||
{parts.length > 0 ? parts : line}
|
||||
{i < content.split('\n').length - 1 && '\n'}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
<div className={`text-sm leading-relaxed ${config.textColor} chat-prose${role === 'user' ? ' chat-prose-user' : ''}`}>
|
||||
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user