|
|
import React from 'react' |
|
|
import ReactMarkdown from 'react-markdown' |
|
|
import { Button } from '@/components/ui/button' |
|
|
import { Textarea } from '@/components/ui/textarea' |
|
|
import { Card } from '@/components/ui/card' |
|
|
import { Badge } from '@/components/ui/badge' |
|
|
import { Message } from '@/types/chat' |
|
|
import { Send, Square, Eye, EyeOff, Brain, User, Bot } from 'lucide-react' |
|
|
|
|
|
interface ChatContainerProps { |
|
|
messages: Message[] |
|
|
input: string |
|
|
setInput: (value: string) => void |
|
|
onSubmit: () => void |
|
|
onStop: () => void |
|
|
isLoading: boolean |
|
|
disabled?: boolean |
|
|
placeholder?: string |
|
|
} |
|
|
|
|
|
export function ChatContainer({ |
|
|
messages, |
|
|
input, |
|
|
setInput, |
|
|
onSubmit, |
|
|
onStop, |
|
|
isLoading, |
|
|
disabled = false, |
|
|
placeholder = "Type your message..." |
|
|
}: ChatContainerProps) { |
|
|
const [showThinking, setShowThinking] = React.useState<{ [key: string]: boolean }>({}) |
|
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => { |
|
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault() |
|
|
if (!isLoading && !disabled) { |
|
|
onSubmit() |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const toggleThinking = (messageId: string) => { |
|
|
setShowThinking(prev => ({ |
|
|
...prev, |
|
|
[messageId]: !prev[messageId] |
|
|
})) |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="flex flex-col h-full"> |
|
|
{/* 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"> |
|
|
<div className="text-center"> |
|
|
<Bot className="h-12 w-12 mx-auto text-muted-foreground mb-4" /> |
|
|
<h3 className="text-lg font-medium mb-2">Start a conversation</h3> |
|
|
<p className="text-muted-foreground"> |
|
|
Ask me anything and I'll help you out! |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
) : ( |
|
|
messages.map((message) => ( |
|
|
<div key={message.id} className="space-y-3"> |
|
|
<div className={`flex items-start gap-3 ${ |
|
|
message.role === 'user' ? 'justify-end' : 'justify-start' |
|
|
}`}> |
|
|
{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> |
|
|
)} |
|
|
|
|
|
<Card className={`max-w-[80%] ${ |
|
|
message.role === 'user' |
|
|
? 'bg-primary text-primary-foreground' |
|
|
: 'bg-muted' |
|
|
}`}> |
|
|
<div className="p-4"> |
|
|
<div className="flex items-center gap-2 mb-2"> |
|
|
{message.role === 'user' ? ( |
|
|
<User className="h-4 w-4" /> |
|
|
) : ( |
|
|
<Bot className="h-4 w-4" /> |
|
|
)} |
|
|
<span className="text-sm font-medium capitalize"> |
|
|
{message.role === 'user' ? 'You' : 'Assistant'} |
|
|
</span> |
|
|
{message.model_used && ( |
|
|
<Badge variant="outline" className="text-xs"> |
|
|
{message.model_used} |
|
|
</Badge> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Thinking content */} |
|
|
{message.thinking_content && message.supports_thinking && ( |
|
|
<div className="mb-3"> |
|
|
<Button |
|
|
variant="ghost" |
|
|
size="sm" |
|
|
onClick={() => toggleThinking(message.id)} |
|
|
className="p-1 h-auto" |
|
|
> |
|
|
<Brain className="h-3 w-3 mr-1" /> |
|
|
{showThinking[message.id] ? ( |
|
|
<> |
|
|
<EyeOff className="h-3 w-3 mr-1" /> |
|
|
Hide thinking |
|
|
</> |
|
|
) : ( |
|
|
<> |
|
|
<Eye className="h-3 w-3 mr-1" /> |
|
|
Show thinking |
|
|
</> |
|
|
)} |
|
|
</Button> |
|
|
|
|
|
{showThinking[message.id] && ( |
|
|
<div className="mt-2 p-3 bg-background/50 rounded border-l-4 border-blue-500"> |
|
|
<div className="text-xs text-muted-foreground mb-1">Thinking process:</div> |
|
|
<ReactMarkdown |
|
|
components={{ |
|
|
p: ({ children }) => <p className="text-sm prose prose-sm max-w-none">{children}</p> |
|
|
}} |
|
|
> |
|
|
{message.thinking_content} |
|
|
</ReactMarkdown> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{/* Main content */} |
|
|
<ReactMarkdown |
|
|
components={{ |
|
|
p: ({ children }) => <p className="prose prose-sm max-w-none">{children}</p> |
|
|
}} |
|
|
> |
|
|
{message.content} |
|
|
</ReactMarkdown> |
|
|
</div> |
|
|
</Card> |
|
|
|
|
|
{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> |
|
|
)) |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{} |
|
|
<div className="border-t p-4"> |
|
|
<div className="flex gap-2"> |
|
|
<Textarea |
|
|
value={input} |
|
|
onChange={(e) => setInput(e.target.value)} |
|
|
onKeyPress={handleKeyPress} |
|
|
placeholder={placeholder} |
|
|
disabled={disabled} |
|
|
className="min-h-[60px] resize-none" |
|
|
/> |
|
|
{isLoading ? ( |
|
|
<Button onClick={onStop} variant="outline" size="icon"> |
|
|
<Square className="h-4 w-4" /> |
|
|
</Button> |
|
|
) : ( |
|
|
<Button |
|
|
onClick={onSubmit} |
|
|
disabled={disabled || !input.trim()} |
|
|
size="icon" |
|
|
> |
|
|
<Send className="h-4 w-4" /> |
|
|
</Button> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
|