'use client'
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { X, Send, Bot, User, Loader2, Settings, ChevronDown } from 'lucide-react'
import { llmService, ChatMessage, Personality } from '../services/llmService'
interface ChatbotModalProps {
isOpen: boolean
onClose: () => void
defaultModel?: string
defaultPersonality?: string
}
export default function ChatbotModal({
isOpen,
onClose,
defaultModel = '',
defaultPersonality = 'assistant'
}: ChatbotModalProps) {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [input, setInput] = useState('')
const [selectedModel, setSelectedModel] = useState(defaultModel)
const [selectedPersonality, setSelectedPersonality] = useState(defaultPersonality)
const [personalities, setPersonalities] = useState<Record<string, Personality>>({})
const [availableModels, setAvailableModels] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [showSettings, setShowSettings] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) {
loadPersonalities()
loadModels()
}
}, [isOpen])
useEffect(() => {
scrollToBottom()
}, [messages])
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
const loadPersonalities = async () => {
const pers = await llmService.getPersonalities()
setPersonalities(pers)
}
const loadModels = async () => {
const models = await llmService.listModels()
const modelIds = models.map(m => m.id)
setAvailableModels(modelIds)
if (!selectedModel && modelIds.length > 0) {
setSelectedModel(modelIds[0])
}
}
const handleSend = useCallback(async () => {
if (!input.trim() || !selectedModel || loading) return
const userMessage: ChatMessage = { role: 'user', content: input.trim() }
const newMessages = [...messages, userMessage]
setMessages(newMessages)
setInput('')
setLoading(true)
try {
const response = await llmService.chatCompletion({
model: selectedModel,
messages: newMessages,
personality: selectedPersonality
})
if (response.success && response.data) {
const assistantMessage: ChatMessage = {
role: 'assistant',
content: response.data.content || response.data.text || JSON.stringify(response.data)
}
setMessages([...newMessages, assistantMessage])
} else {
const errorMessage: ChatMessage = {
role: 'assistant',
content: `Error: ${response.error || 'Unknown error'}`
}
setMessages([...newMessages, errorMessage])
}
} catch (error) {
const errorMessage: ChatMessage = {
role: 'assistant',
content: `Error: ${String(error)}`
}
setMessages([...newMessages, errorMessage])
} finally {
setLoading(false)
}
}, [input, selectedModel, selectedPersonality, messages, loading])
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
const clearChat = () => {
setMessages([])
}
if (!isOpen) return null
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px'
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '16px',
width: '100%',
maxWidth: '800px',
height: '90vh',
maxHeight: '700px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)'
}}>
{/* Header */}
<div style={{
padding: '20px',
borderBottom: '1px solid #e2e8f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Bot style={{ width: '24px', height: '24px', color: '#2563eb' }} />
<div>
<h2 style={{ fontSize: '20px', fontWeight: '600', margin: 0, color: '#1e293b' }}>
AI Chatbot
</h2>
{selectedPersonality && personalities[selectedPersonality] && (
<p style={{ fontSize: '12px', color: '#64748b', margin: '4px 0 0 0' }}>
{personalities[selectedPersonality].name}
</p>
)}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
onClick={() => setShowSettings(!showSettings)}
style={{
padding: '8px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Settings style={{ width: '18px', height: '18px', color: '#64748b' }} />
</button>
<button
onClick={onClose}
style={{
padding: '8px',
borderRadius: '6px',
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<X style={{ width: '20px', height: '20px', color: '#64748b' }} />
</button>
</div>
</div>
{/* Settings Panel */}
{showSettings && (
<div style={{
padding: '16px 20px',
borderBottom: '1px solid #e2e8f0',
backgroundColor: '#f8fafc'
}}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', color: '#1e293b', marginBottom: '6px' }}>
Model
</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
fontSize: '14px',
color: '#1e293b'
}}
>
{availableModels.length === 0 ? (
<option value="">No models available</option>
) : (
availableModels.map(model => (
<option key={model} value={model}>{model}</option>
))
)}
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', color: '#1e293b', marginBottom: '6px' }}>
Personality
</label>
<select
value={selectedPersonality}
onChange={(e) => setSelectedPersonality(e.target.value)}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
fontSize: '14px',
color: '#1e293b'
}}
>
{Object.entries(personalities).map(([key, persona]) => (
<option key={key} value={key}>{persona.name}</option>
))}
</select>
{selectedPersonality && personalities[selectedPersonality] && (
<p style={{ fontSize: '12px', color: '#64748b', marginTop: '4px' }}>
{personalities[selectedPersonality].system_prompt.substring(0, 100)}...
</p>
)}
</div>
</div>
</div>
)}
{/* Messages */}
<div style={{
flex: 1,
overflowY: 'auto',
padding: '20px',
display: 'flex',
flexDirection: 'column',
gap: '16px'
}}>
{messages.length === 0 ? (
<div style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
color: '#94a3b8'
}}>
<Bot style={{ width: '48px', height: '48px', marginBottom: '16px', opacity: 0.5 }} />
<p style={{ fontSize: '16px', margin: 0 }}>Start a conversation...</p>
{!selectedModel && (
<p style={{ fontSize: '14px', marginTop: '8px' }}>
Select a model in settings to begin
</p>
)}
</div>
) : (
messages.map((message, index) => (
<div
key={index}
style={{
display: 'flex',
gap: '12px',
alignItems: 'flex-start',
flexDirection: message.role === 'user' ? 'row-reverse' : 'row'
}}
>
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: message.role === 'user' ? '#2563eb' : '#10b981',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
{message.role === 'user' ? (
<User style={{ width: '18px', height: '18px', color: 'white' }} />
) : (
<Bot style={{ width: '18px', height: '18px', color: 'white' }} />
)}
</div>
<div style={{
flex: 1,
backgroundColor: message.role === 'user' ? '#eff6ff' : '#f0fdf4',
padding: '12px 16px',
borderRadius: '12px',
maxWidth: '80%'
}}>
<div style={{ fontSize: '14px', color: '#1e293b', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{message.content}
</div>
</div>
</div>
))
)}
{loading && (
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start' }}>
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: '#10b981',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<Bot style={{ width: '18px', height: '18px', color: 'white' }} />
</div>
<div style={{
backgroundColor: '#f0fdf4',
padding: '12px 16px',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<Loader2 style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite', color: '#10b981' }} />
<span style={{ fontSize: '14px', color: '#64748b' }}>Thinking...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div style={{
padding: '20px',
borderTop: '1px solid #e2e8f0',
display: 'flex',
gap: '12px',
alignItems: 'flex-end'
}}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '8px' }}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={selectedModel ? "Type your message..." : "Select a model to start chatting"}
disabled={!selectedModel || loading}
style={{
width: '100%',
minHeight: '60px',
maxHeight: '120px',
padding: '12px',
borderRadius: '8px',
border: '1px solid #e2e8f0',
fontSize: '14px',
fontFamily: 'inherit',
resize: 'none',
color: '#1e293b',
backgroundColor: selectedModel && !loading ? 'white' : '#f8fafc'
}}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<button
onClick={clearChat}
style={{
padding: '6px 12px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
color: '#64748b',
fontSize: '12px',
cursor: 'pointer'
}}
>
Clear Chat
</button>
<span style={{ fontSize: '12px', color: '#94a3b8' }}>
Press Enter to send, Shift+Enter for new line
</span>
</div>
</div>
<button
onClick={handleSend}
disabled={!input.trim() || !selectedModel || loading}
style={{
padding: '12px 20px',
borderRadius: '8px',
border: 'none',
backgroundColor: (!input.trim() || !selectedModel || loading) ? '#cbd5e1' : '#2563eb',
color: 'white',
cursor: (!input.trim() || !selectedModel || loading) ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '60px'
}}
>
{loading ? (
<Loader2 style={{ width: '20px', height: '20px', animation: 'spin 1s linear infinite' }} />
) : (
<Send style={{ width: '20px', height: '20px' }} />
)}
</button>
</div>
</div>
<style>{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}</style>
</div>
)
}