wu981526092's picture
Add markdown rendering to chat messages with ReactMarkdown
8eb16a3
raw
history blame
6.32 kB
import * as React from 'react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Send, Square, User, Bot } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
export interface ChatProps extends React.HTMLAttributes<HTMLDivElement> {
messages: Array<{
id: string
role: 'user' | 'assistant' | 'system'
content: string
createdAt?: Date
}>
input: string
handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
isGenerating?: boolean
stop?: () => void
}
const Chat = React.forwardRef<HTMLDivElement, ChatProps>(
({ className, messages, input, handleInputChange, handleSubmit, isGenerating, stop, ...props }, ref) => {
const messagesEndRef = React.useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
React.useEffect(() => {
console.log('Chat component - messages updated:', messages.length, messages.map(m => ({ id: m.id, role: m.role, content: m.content.slice(0, 50) + '...' })))
scrollToBottom()
}, [messages])
return (
<div
className={cn('flex h-full flex-col', className)}
ref={ref}
{...props}
>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<p>No messages yet. Start a conversation!</p>
</div>
) : (
messages.map((message, index) => (
<div
key={`${message.id}-${index}`}
className={cn(
'flex gap-3 w-full',
message.role === 'user' ? 'justify-end' : 'justify-start'
)}
>
{/* Avatar for assistant */}
{message.role !== 'user' && (
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Bot className="h-4 w-4 text-primary-foreground" />
</div>
)}
{/* Message content */}
<div
className={cn(
'max-w-[75%] flex flex-col gap-2 rounded-lg px-3 py-2 text-sm',
message.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted'
)}
>
<div className="text-xs opacity-70">
{message.role === 'user' ? 'You' : 'Assistant'} • #{index + 1}
</div>
<div className="leading-relaxed prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
components={{
// Customize components for better styling
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
ul: ({ children }) => <ul className="mb-2 last:mb-0 list-disc pl-4">{children}</ul>,
ol: ({ children }) => <ol className="mb-2 last:mb-0 list-decimal pl-4">{children}</ol>,
li: ({ children }) => <li className="mb-1">{children}</li>,
code: ({ children, className }) => {
const isInline = !className;
return isInline ? (
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{children}</code>
) : (
<code className="block bg-muted p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre">{children}</code>
)
},
pre: ({ children }) => <div className="mb-2 last:mb-0">{children}</div>,
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
blockquote: ({ children }) => <blockquote className="border-l-4 border-muted pl-4 italic mb-2 last:mb-0">{children}</blockquote>,
h1: ({ children }) => <h1 className="text-lg font-bold mb-2 last:mb-0">{children}</h1>,
h2: ({ children }) => <h2 className="text-base font-bold mb-2 last:mb-0">{children}</h2>,
h3: ({ children }) => <h3 className="text-sm font-bold mb-2 last:mb-0">{children}</h3>,
}}
>
{message.content}
</ReactMarkdown>
</div>
</div>
{/* Avatar for user */}
{message.role === 'user' && (
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
<User className="h-4 w-4" />
</div>
)}
</div>
))
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="border-t p-4">
<form onSubmit={handleSubmit} className="flex gap-2">
<Textarea
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="min-h-[60px] resize-none"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit(e as any)
}
}}
/>
{isGenerating ? (
<Button type="button" onClick={stop} variant="outline" size="icon">
<Square className="h-4 w-4" />
</Button>
) : (
<Button type="submit" disabled={!input.trim()} size="icon">
<Send className="h-4 w-4" />
</Button>
)}
</form>
</div>
</div>
)
}
)
Chat.displayName = 'Chat'
export { Chat }