File size: 6,318 Bytes
6a50e97 8eb16a3 6a50e97 8eb16a3 6a50e97 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
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 }
|