eduscope / old /MainPage copy.jsx
merasabkuch's picture
Upload 10 files
e8b2588 verified
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;