“vinit5112”
Add all code
deb090d
raw
history blame
18.3 kB
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;