Spaces:
Running
Running
import React, { useState, useEffect } from 'react'; | |
import { motion, AnimatePresence } from 'framer-motion'; | |
import { Upload, BookOpen, Search, File, Send, Trash2, Gem, Loader2 } from 'lucide-react'; | |
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
import { Button } from '@/components/ui/button'; | |
import { Textarea } from '@/components/ui/textarea'; | |
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; | |
import { Checkbox } from '@/components/ui/checkbox'; | |
import { toast, Toaster } from 'sonner'; | |
import GeminiResponseDisplay from './GeminiResponse'; | |
const FileTypeIcons = { | |
'.pdf': File, | |
'.docx': File, | |
'.xlsx': File, | |
'.csv': File, | |
'.txt': File, | |
'.ppt': File, | |
'.pptx': File | |
}; | |
const containerVariants = { | |
hidden: { opacity: 0 }, | |
visible: { | |
opacity: 1, | |
transition: { | |
delayChildren: 0.2, | |
staggerChildren: 0.1 | |
} | |
} | |
}; | |
const itemVariants = { | |
hidden: { y: 20, opacity: 0 }, | |
visible: { | |
y: 0, | |
opacity: 1, | |
transition: { | |
type: "spring", | |
stiffness: 300, | |
damping: 24 | |
} | |
} | |
}; | |
const chatMessageVariants = { | |
hidden: { opacity: 0, x: -20 }, | |
visible: { | |
opacity: 1, | |
x: 0, | |
transition: { | |
type: "tween", | |
duration: 0.3 | |
} | |
} | |
}; | |
const MainPage = () => { | |
// State Management | |
const [documents, setDocuments] = useState([]); | |
const [query, setQuery] = useState(''); | |
const [chatHistory, setChatHistory] = useState([]); | |
const [loading, setLoading] = useState(false); | |
const [selectedDocs, setSelectedDocs] = useState([]); | |
// Error Handling | |
const showError = (message, description = '') => { | |
toast.error(message, { | |
description, | |
duration: 4000 | |
}); | |
}; | |
const showSuccess = (message, description = '') => { | |
toast.success(message, { | |
description, | |
duration: 3000 | |
}); | |
}; | |
// Fetch Initial Data | |
useEffect(() => { | |
fetchDocuments(); | |
fetchChatHistory(); | |
}, []); | |
const fetchDocuments = async () => { | |
try { | |
const response = await fetch('http://localhost:8000/documents'); | |
const data = await response.json(); | |
setDocuments(data); | |
} catch (err) { | |
showError('Failed to fetch documents', err.message); | |
} | |
}; | |
const fetchChatHistory = async () => { | |
try { | |
const response = await fetch('http://localhost:8000/chat-history'); | |
const data = await response.json(); | |
setChatHistory(data); | |
} catch (err) { | |
showError('Failed to fetch chat history', err.message); | |
} | |
}; | |
const ALLOWED_TYPES = ['.pdf', '.docx', '.xlsx', '.csv', '.txt','.ppt', '.pptx']; | |
const handleFileUpload = async (event) => { | |
const file = event.target.files[0]; | |
if (!file) return; | |
// File Validation | |
const MAX_FILE_SIZE = 35 * 1024 * 1024; // 35MB | |
const fileExtension = '.' + file.name.split('.').pop().toLowerCase(); | |
if (file.size > MAX_FILE_SIZE) { | |
showError('File Too Large', 'Maximum file size is 35MB'); | |
return; | |
} | |
if (!ALLOWED_TYPES.includes(fileExtension)) { | |
showError('Unsupported File Type', `Supported: ${ALLOWED_TYPES.join(', ')}`); | |
return; | |
} | |
const formData = new FormData(); | |
formData.append('file', file); | |
try { | |
setLoading(true); | |
const response = await fetch('http://localhost:8000/upload', { | |
method: 'POST', | |
body: formData, | |
}); | |
if (!response.ok) throw new Error('Upload failed'); | |
const data = await response.json(); | |
setDocuments([...documents, data]); | |
showSuccess('Document Uploaded', `${file.name} processed successfully`); | |
} catch (err) { | |
showError('Failed to upload document', err.message); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
const handleClearAll = async () => { | |
try { | |
setLoading(true); | |
const response = await fetch('http://localhost:8000/clear-all', { | |
method: 'GET', | |
}); | |
if (!response.ok) throw new Error('Clear all failed'); | |
setDocuments([]); | |
setChatHistory([]); | |
setSelectedDocs([]); | |
showSuccess('Data Cleared', 'All documents and chat history removed'); | |
} catch (err) { | |
showError('Failed to clear all', err.message); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
const handleAnalyze = async () => { | |
if (!selectedDocs.length || !query) { | |
showError('Incomplete Request', 'Select documents and enter a query'); | |
return; | |
} | |
try { | |
setLoading(true); | |
const response = await fetch('http://localhost:8000/analyze', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
text: query, | |
selected_docs: selectedDocs, | |
}), | |
}); | |
if (!response.ok) throw new Error('Analysis failed'); | |
await fetchChatHistory(); | |
setQuery(''); | |
showSuccess('Analysis Complete', 'Results are available in chat history'); | |
} catch (err) { | |
showError('Failed to analyze', err.message); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
const formatTimestamp = (timestamp) => { | |
return new Date(timestamp).toLocaleString(); | |
}; | |
return ( | |
<motion.div | |
initial="hidden" | |
animate="visible" | |
variants={containerVariants} | |
className="min-h-screen bg-gray-100 p-8" | |
> | |
<Toaster position="top-right" /> | |
<motion.div | |
variants={itemVariants} | |
className="max-w-6xl mx-auto space-y-6" | |
> | |
<Card> | |
<CardHeader> | |
<motion.div | |
variants={itemVariants} | |
className="flex items-center justify-between" | |
> | |
<CardTitle className="text-2xl font-bold flex items-center gap-2"> | |
<BookOpen className="w-6 h-6" /> | |
EduScope AI | |
</CardTitle> | |
<motion.div | |
whileHover={{ scale: 1.05 }} | |
whileTap={{ scale: 0.95 }} | |
> | |
<Gem className="w-6 h-6 text-purple-600" /> | |
</motion.div> | |
</motion.div> | |
</CardHeader> | |
<CardContent> | |
<div className="grid grid-cols-12 gap-6"> | |
{/* Document Management Sidebar */} | |
<motion.div | |
variants={itemVariants} | |
className="col-span-4 space-y-4" | |
> | |
<Card> | |
<CardHeader> | |
<CardTitle className="text-lg">Documents</CardTitle> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-4"> | |
<motion.div | |
whileHover={{ scale: 1.02 }} | |
whileTap={{ scale: 0.98 }} | |
> | |
<Button | |
variant="outline" | |
onClick={() => document.getElementById('file-upload').click()} | |
className="w-full" | |
> | |
<Upload className="w-4 h-4 mr-2" /> | |
Upload Document | |
</Button> | |
</motion.div> | |
<motion.div | |
whileHover={{ scale: 1.02 }} | |
whileTap={{ scale: 0.98 }} | |
> | |
<Button | |
onClick={handleClearAll} | |
disabled={loading} | |
className="w-full" | |
> | |
{loading ? ( | |
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> | |
) : ( | |
<Trash2 className="w-4 h-4 mr-2" /> | |
)} | |
Clear All | |
</Button> | |
</motion.div> | |
<input | |
id="file-upload" | |
type="file" | |
accept={ALLOWED_TYPES.join(',')} | |
className="hidden" | |
onChange={handleFileUpload} | |
/> | |
<motion.div | |
variants={containerVariants} | |
className="space-y-2" | |
> | |
<AnimatePresence> | |
{documents.map((doc) => { | |
const FileIcon = FileTypeIcons[`.${doc.name.split('.').pop().toLowerCase()}`] || File; | |
return ( | |
<motion.div | |
key={doc.id} | |
initial={{ opacity: 0, x: -20 }} | |
animate={{ opacity: 1, x: 0 }} | |
exit={{ opacity: 0, x: 20 }} | |
transition={{ type: "spring", stiffness: 300, damping: 30 }} | |
className="flex items-center space-x-2" | |
> | |
<Checkbox | |
checked={selectedDocs.includes(doc.id)} | |
onCheckedChange={(checked) => { | |
if (checked) { | |
setSelectedDocs([...selectedDocs, doc.id]); | |
} else { | |
setSelectedDocs(selectedDocs.filter(id => id !== doc.id)); | |
} | |
}} | |
/> | |
<div className="flex items-center space-x-2"> | |
<FileIcon className="w-4 h-4" /> | |
<span className="text-sm truncate">{doc.name}</span> | |
</div> | |
</motion.div> | |
); | |
})} | |
</AnimatePresence> | |
</motion.div> | |
</div> | |
</CardContent> | |
</Card> | |
</motion.div> | |
{/* Chat Interface */} | |
<motion.div | |
variants={itemVariants} | |
className="col-span-8 space-y-4" | |
> | |
<motion.div | |
className="h-[500px] overflow-y-auto bg-white rounded-lg p-4 border" | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
> | |
<AnimatePresence> | |
{chatHistory.map((message) => ( | |
<motion.div | |
key={message.id} | |
variants={chatMessageVariants} | |
initial="hidden" | |
animate="visible" | |
exit={{ opacity: 0, x: 20 }} | |
className={`mb-4 ${message.type === 'assistant' ? 'ml-4' : 'mr-4'}`} | |
> | |
<div | |
className={`p-3 rounded-lg ${ | |
message.type === 'assistant' | |
? 'bg-blue-100' | |
: 'bg-gray-100' | |
}`} | |
> | |
<div className="text-sm text-gray-500 mb-1"> | |
{message.type === 'assistant' ? 'AI Assistant' : 'You'} •{' '} | |
{formatTimestamp(message.timestamp)} | |
</div> | |
<div className="text-gray-800"> | |
{message.content.includes('pareto_analysis') || message.content.includes('<html') | |
? <GeminiResponseDisplay responseStr={message.content} /> | |
: message.content} | |
</div> | |
{message.referenced_docs.length > 0 && ( | |
<div className="text-xs text-gray-500 mt-2"> | |
Referenced documents:{' '} | |
{message.referenced_docs | |
.map( | |
(docId) => | |
documents.find((d) => d.id === docId)?.name | |
) | |
.join(', ')} | |
</div> | |
)} | |
</div> | |
</motion.div> | |
))} | |
</AnimatePresence> | |
</motion.div> | |
<motion.div | |
variants={itemVariants} | |
className="flex gap-2" | |
> | |
<Textarea | |
value={query} | |
onChange={(e) => setQuery(e.target.value)} | |
placeholder="Ask a question about the selected documents..." | |
className="flex-1" | |
/> | |
<motion.div | |
whileHover={{ scale: 1.05 }} | |
whileTap={{ scale: 0.95 }} | |
> | |
<Button | |
onClick={handleAnalyze} | |
disabled={loading} | |
className="self-end" | |
> | |
{loading ? ( | |
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> | |
) : ( | |
<> | |
<Send className="w-4 h-4 mr-2" /> | |
Send | |
</> | |
)} | |
</Button> | |
</motion.div> | |
</motion.div> | |
</motion.div> | |
</div> | |
</CardContent> | |
</Card> | |
</motion.div> | |
</motion.div> | |
); | |
}; | |
export default MainPage; |