| 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 } | |