Spaces:
Running
Running
import React, { useState, useRef, useEffect } from 'react'; | |
import { motion, AnimatePresence } from 'framer-motion'; | |
import { PaperAirplaneIcon, StopIcon } from '@heroicons/react/24/solid'; | |
import MessageBubble from './MessageBubble'; | |
import TypingIndicator from './TypingIndicator'; | |
import FileUploader from './FileUploader'; | |
import { sendMessage, sendMessageStream } from '../services/api'; | |
import toast from 'react-hot-toast'; | |
const ChatInterface = ({ conversationId, conversations, setConversations, darkMode }) => { | |
const [message, setMessage] = useState(''); | |
const [isLoading, setIsLoading] = useState(false); | |
const [showFileUploader, setShowFileUploader] = useState(false); | |
const messagesEndRef = useRef(null); | |
const textareaRef = useRef(null); | |
const currentConversation = conversations.find(conv => conv.id === conversationId); | |
const messages = currentConversation?.messages || []; | |
const scrollToBottom = () => { | |
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
}; | |
useEffect(() => { | |
scrollToBottom(); | |
}, [messages]); | |
const handleSubmit = async (e) => { | |
e.preventDefault(); | |
if (!message.trim() || isLoading) return; | |
const userMessage = { | |
id: Date.now().toString(), | |
role: 'user', | |
content: message.trim(), | |
timestamp: new Date(), | |
}; | |
// Add user message immediately | |
setConversations(prev => prev.map(conv => | |
conv.id === conversationId | |
? { | |
...conv, | |
messages: [...conv.messages, userMessage], | |
title: conv.messages.length === 0 ? message.slice(0, 50) + '...' : conv.title | |
} | |
: conv | |
)); | |
setMessage(''); | |
setIsLoading(true); | |
const assistantMessageId = (Date.now() + 1).toString(); | |
try { | |
let fullResponse = ''; | |
// Add a placeholder for the assistant's message | |
setConversations(prev => prev.map(conv => | |
conv.id === conversationId | |
? { ...conv, messages: [...conv.messages, { id: assistantMessageId, role: 'assistant', content: '', timestamp: new Date() }] } | |
: conv | |
)); | |
await sendMessageStream(message.trim(), (chunk) => { | |
fullResponse += chunk; | |
setConversations(prev => prev.map(conv => | |
conv.id === conversationId | |
? { | |
...conv, | |
messages: conv.messages.map(msg => | |
msg.id === assistantMessageId | |
? { ...msg, content: fullResponse } | |
: msg | |
), | |
} | |
: conv | |
)); | |
}); | |
} catch (error) { | |
toast.error('Failed to send message. Please try again.'); | |
console.error('Error sending message:', error); | |
// Optional: remove placeholder on error | |
setConversations(prev => prev.map(conv => | |
conv.id === conversationId | |
? { ...conv, messages: conv.messages.filter(msg => msg.id !== assistantMessageId) } | |
: conv | |
)); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
const handleKeyDown = (e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
handleSubmit(e); | |
} | |
}; | |
const adjustTextareaHeight = () => { | |
const textarea = textareaRef.current; | |
if (textarea) { | |
textarea.style.height = 'auto'; | |
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; | |
} | |
}; | |
useEffect(() => { | |
adjustTextareaHeight(); | |
}, [message]); | |
return ( | |
<div className="flex flex-col h-screen"> | |
{/* Messages Container */} | |
<div className="flex-1 overflow-y-auto px-4 py-6"> | |
<div className="max-w-3xl mx-auto"> | |
{/* Empty State */} | |
{messages.length === 0 && !isLoading && ( | |
<motion.div | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
transition={{ duration: 0.6 }} | |
className="flex flex-col items-center justify-center min-h-[60vh] text-center" | |
> | |
{/* CA Assistant Avatar */} | |
<motion.div | |
initial={{ scale: 0.8 }} | |
animate={{ scale: 1 }} | |
transition={{ duration: 0.5, delay: 0.2 }} | |
className={`w-20 h-20 rounded-full flex items-center justify-center mb-6 ${ | |
darkMode | |
? 'bg-gradient-to-br from-primary-600 to-purple-600' | |
: 'bg-gradient-to-br from-primary-500 to-purple-500' | |
} shadow-lg`} | |
> | |
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | |
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> | |
</svg> | |
</motion.div> | |
{/* Welcome Message */} | |
<motion.h2 | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
transition={{ delay: 0.4 }} | |
className="text-2xl md:text-3xl font-bold mb-3 gradient-text" | |
> | |
Hello! I'm your CA Study Assistant | |
</motion.h2> | |
<motion.p | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
transition={{ delay: 0.5 }} | |
className={`text-lg mb-8 ${darkMode ? 'text-gray-300' : 'text-gray-600'}`} | |
> | |
I'm here to help you with accounting, finance, taxation, and auditing concepts. | |
Ask me anything or upload your study materials! | |
</motion.p> | |
{/* Quick Start Suggestions */} | |
<motion.div | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
transition={{ delay: 0.6 }} | |
className="w-full max-w-2xl" | |
> | |
<h3 className={`text-sm font-semibold mb-4 ${ | |
darkMode ? 'text-gray-400' : 'text-gray-500' | |
}`}> | |
Try asking me about: | |
</h3> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> | |
{[ | |
{ icon: "📊", text: "Financial statement analysis", query: "Explain financial statement analysis" }, | |
{ icon: "💰", text: "Depreciation methods", query: "What are different depreciation methods?" }, | |
{ icon: "🏦", text: "Working capital management", query: "Explain working capital management" }, | |
{ icon: "📈", text: "Ratio analysis", query: "How to perform ratio analysis?" }, | |
{ icon: "📋", text: "Auditing procedures", query: "What are key auditing procedures?" }, | |
{ icon: "💼", text: "Tax planning strategies", query: "Explain tax planning strategies" } | |
].map((suggestion, index) => ( | |
<motion.button | |
key={index} | |
initial={{ opacity: 0, x: -20 }} | |
animate={{ opacity: 1, x: 0 }} | |
transition={{ delay: 0.7 + index * 0.1 }} | |
whileHover={{ scale: 1.02, y: -2 }} | |
whileTap={{ scale: 0.98 }} | |
onClick={() => setMessage(suggestion.query)} | |
className={`flex items-center p-4 rounded-xl text-left transition-all ${ | |
darkMode | |
? 'bg-gray-800 hover:bg-gray-700 border-gray-700 text-gray-300' | |
: 'bg-gray-50 hover:bg-gray-100 border-gray-200 text-gray-700' | |
} border hover:border-primary-300 hover:shadow-md`} | |
> | |
<span className="text-2xl mr-3">{suggestion.icon}</span> | |
<span className="font-medium">{suggestion.text}</span> | |
</motion.button> | |
))} | |
</div> | |
</motion.div> | |
{/* Upload Reminder */} | |
<motion.div | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
transition={{ delay: 1.2 }} | |
className={`mt-8 p-4 rounded-xl ${ | |
darkMode | |
? 'bg-primary-900/20 border-primary-700/30' | |
: 'bg-primary-50 border-primary-200' | |
} border`} | |
> | |
<div className="flex items-center justify-center"> | |
<svg className={`w-5 h-5 mr-2 ${ | |
darkMode ? 'text-primary-400' : 'text-primary-600' | |
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | |
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
</svg> | |
<span className={`text-sm ${ | |
darkMode ? 'text-primary-300' : 'text-primary-700' | |
}`}> | |
💡 Upload your study materials for more specific and detailed answers | |
</span> | |
</div> | |
</motion.div> | |
</motion.div> | |
)} | |
{/* Messages */} | |
<AnimatePresence> | |
{messages.map((msg, index) => ( | |
<MessageBubble | |
key={msg.id} | |
message={msg} | |
darkMode={darkMode} | |
isLast={index === messages.length - 1} | |
/> | |
))} | |
</AnimatePresence> | |
{isLoading && <TypingIndicator darkMode={darkMode} />} | |
<div ref={messagesEndRef} /> | |
</div> | |
</div> | |
{/* File Uploader Modal */} | |
<AnimatePresence> | |
{showFileUploader && ( | |
<motion.div | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
exit={{ opacity: 0 }} | |
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" | |
onClick={() => setShowFileUploader(false)} | |
> | |
<motion.div | |
initial={{ scale: 0.9, opacity: 0 }} | |
animate={{ scale: 1, opacity: 1 }} | |
exit={{ scale: 0.9, opacity: 0 }} | |
onClick={(e) => e.stopPropagation()} | |
className={`max-w-md w-full p-6 rounded-2xl ${ | |
darkMode ? 'bg-gray-800' : 'bg-white' | |
} shadow-2xl`} | |
> | |
<h3 className="text-lg font-semibold mb-4">Upload Document</h3> | |
<FileUploader darkMode={darkMode} onClose={() => setShowFileUploader(false)} /> | |
</motion.div> | |
</motion.div> | |
)} | |
</AnimatePresence> | |
{/* Input Area */} | |
<div className={`border-t ${ | |
darkMode ? 'border-gray-700/50 bg-gray-900/95' : 'border-gray-200/50 bg-white/95' | |
} backdrop-blur-sm p-6`}> | |
<div className="max-w-3xl mx-auto"> | |
<form onSubmit={handleSubmit} className="relative"> | |
{/* Enhanced Input Container */} | |
<div className={`relative overflow-hidden rounded-2xl border-2 transition-all duration-300 ${ | |
darkMode | |
? 'bg-gradient-to-br from-gray-800 to-gray-900 border-gray-600 focus-within:border-primary-500 focus-within:from-gray-700 focus-within:to-gray-800' | |
: 'bg-gradient-to-br from-white to-gray-50 border-gray-300 focus-within:border-primary-500 focus-within:from-blue-50 focus-within:to-white' | |
} focus-within:ring-4 focus-within:ring-primary-500/20 shadow-xl hover:shadow-2xl focus-within:shadow-2xl`}> | |
{/* Subtle Inner Glow */} | |
<div className={`absolute inset-0 opacity-0 focus-within:opacity-100 transition-opacity duration-300 ${ | |
darkMode | |
? 'bg-gradient-to-br from-primary-900/20 to-purple-900/20' | |
: 'bg-gradient-to-br from-primary-50/50 to-purple-50/50' | |
}`} /> | |
{/* Input Content */} | |
<div className="relative flex items-end space-x-4 p-4"> | |
{/* File Upload Button */} | |
<motion.button | |
type="button" | |
whileHover={{ scale: 1.05 }} | |
whileTap={{ scale: 0.95 }} | |
onClick={() => setShowFileUploader(true)} | |
className={`flex-shrink-0 p-3 rounded-xl transition-all duration-200 ${ | |
darkMode | |
? 'hover:bg-gray-700/70 text-gray-400 hover:text-primary-400 hover:shadow-lg' | |
: 'hover:bg-gray-100/70 text-gray-500 hover:text-primary-600 hover:shadow-md' | |
} relative group backdrop-blur-sm`} | |
title="Upload document" | |
> | |
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} | |
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" /> | |
</svg> | |
{/* Enhanced Tooltip */} | |
<div className={`absolute -top-14 left-1/2 transform -translate-x-1/2 px-3 py-2 rounded-lg text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-all duration-200 ${ | |
darkMode ? 'bg-gray-800 text-white shadow-xl border border-gray-700' : 'bg-gray-900 text-white shadow-xl' | |
}`}> | |
Upload documents | |
<div className={`absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent ${ | |
darkMode ? 'border-t-gray-800' : 'border-t-gray-900' | |
}`} /> | |
</div> | |
</motion.button> | |
{/* Enhanced Text Input */} | |
<div className="flex-1 relative"> | |
<textarea | |
ref={textareaRef} | |
value={message} | |
onChange={(e) => setMessage(e.target.value)} | |
onKeyDown={handleKeyDown} | |
placeholder={messages.length === 0 ? "Hi! Ask me about accounting, finance, taxation, or upload your study materials..." : "Ask a follow-up question..."} | |
className={`w-full resize-none border-none outline-none bg-transparent py-3 px-2 text-base leading-relaxed ${ | |
darkMode ? 'text-white placeholder-gray-400' : 'text-gray-900 placeholder-gray-500' | |
} placeholder:text-sm placeholder:leading-relaxed`} | |
rows={1} | |
disabled={isLoading} | |
style={{ | |
minHeight: '24px', | |
maxHeight: '120px', | |
lineHeight: '1.5' | |
}} | |
/> | |
{/* Input Focus Indicator */} | |
<div className={`absolute left-0 bottom-0 h-0.5 w-0 bg-gradient-to-r from-primary-500 to-purple-500 transition-all duration-300 ${ | |
message.trim() ? 'w-full' : 'group-focus-within:w-full' | |
}`} /> | |
</div> | |
{/* Enhanced Send Button */} | |
<motion.button | |
type="submit" | |
disabled={!message.trim() || isLoading} | |
whileHover={message.trim() && !isLoading ? { scale: 1.05 } : {}} | |
whileTap={message.trim() && !isLoading ? { scale: 0.95 } : {}} | |
className={`flex-shrink-0 p-3 rounded-xl transition-all duration-200 relative group ${ | |
message.trim() && !isLoading | |
? 'bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white shadow-lg hover:shadow-xl' | |
: darkMode | |
? 'bg-gray-600/50 text-gray-400 hover:bg-gray-600/70' | |
: 'bg-gray-300/50 text-gray-500 hover:bg-gray-300/70' | |
} disabled:cursor-not-allowed`} | |
title={isLoading ? "Stop generation" : "Send message"} | |
> | |
{isLoading ? ( | |
<div className="relative"> | |
<StopIcon className="w-5 h-5" /> | |
<div className="absolute inset-0 border-2 border-white border-t-transparent rounded-full animate-spin opacity-50"></div> | |
</div> | |
) : ( | |
<PaperAirplaneIcon className="w-5 h-5" /> | |
)} | |
{/* Enhanced Send Button Glow Effect */} | |
{message.trim() && !isLoading && ( | |
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-primary-600 to-primary-700 opacity-0 group-hover:opacity-30 transition-opacity duration-200 blur-lg -z-10"></div> | |
)} | |
</motion.button> | |
</div> | |
{/* Bottom Border Accent */} | |
<div className={`absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-primary-500 to-transparent opacity-0 focus-within:opacity-100 transition-opacity duration-300`} /> | |
</div> | |
</form> | |
{/* Footer Text */} | |
<motion.p | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
transition={{ delay: 0.3 }} | |
className={`text-xs text-center mt-3 ${ | |
darkMode ? 'text-gray-500' : 'text-gray-400' | |
}`} | |
> | |
⚡ Powered by AI • CA Study Assistant can make mistakes. Consider checking important information. | |
</motion.p> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default ChatInterface; |