Vertical.ai / frontend /src /components /FlashcardModal.jsx
Abhisingh-18's picture
Mirror of github.com/Abhisingh18/Vertical.ai
1f7ead8 verified
Raw
History Blame Contribute Delete
12.7 kB
import React, { useState } from 'react';
import { X, BookOpen, Layers, ArrowRight, ArrowLeft, RotateCw, Loader2, Check } from 'lucide-react';
import { generateFlashcards } from '../api';
const FlashcardModal = ({ isOpen, onClose, notebookId }) => {
const [status, setStatus] = useState('setup'); // setup, loading, studying
const [difficulty, setDifficulty] = useState('medium');
const [numCards, setNumCards] = useState(10);
const [cards, setCards] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [isFlipped, setIsFlipped] = useState(false);
if (!isOpen) return null;
const handleGenerate = async () => {
setStatus('loading');
try {
const data = await generateFlashcards(notebookId, numCards, difficulty);
setCards(data.flashcards || []);
setStatus('studying');
setCurrentIndex(0);
setIsFlipped(false);
} catch (error) {
console.error(error);
// Handle error state?
setStatus('setup'); // Reset for now
}
};
const handleNext = () => {
if (currentIndex < cards.length - 1) {
setIsFlipped(false);
setTimeout(() => setCurrentIndex(prev => prev + 1), 150);
}
};
const handlePrev = () => {
if (currentIndex > 0) {
setIsFlipped(false);
setTimeout(() => setCurrentIndex(prev => prev - 1), 150);
}
};
const handleFlip = () => setIsFlipped(!isFlipped);
// Progress
const progress = cards.length > 0 ? ((currentIndex + 1) / cards.length) * 100 : 0;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl flex flex-col h-[600px] animate-in fade-in zoom-in duration-200">
{/* Header */}
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-cyan-100 flex items-center justify-center text-cyan-600">
<Layers size={20} fill="currentColor" />
</div>
<div>
<h2 className="text-lg font-bold text-slate-900">Flashcards</h2>
<p className="text-sm text-slate-500">Master concepts through active recall</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full text-slate-400 transition-colors">
<X size={20} />
</button>
</div>
{/* Content */}
<div className="flex-1 bg-slate-50 p-8 flex flex-col overflow-hidden relative">
{status === 'setup' && (
<div className="flex flex-col items-center justify-center h-full space-y-8 animate-in fade-in slide-in-from-bottom-4">
<div className="text-center">
<h3 className="text-2xl font-bold text-slate-800 mb-2">Configure Your Session</h3>
<p className="text-slate-500">Customize how you want to review this notebook.</p>
</div>
<div className="grid grid-cols-2 gap-8 w-full max-w-lg">
{/* Difficulty */}
<div className="space-y-3">
<label className="text-sm font-bold text-slate-700 uppercase tracking-wide">Difficulty Level</label>
<div className="flex flex-col gap-2">
{['basic', 'medium', 'advanced'].map(level => (
<button
key={level}
onClick={() => setDifficulty(level)}
className={`px-4 py-3 rounded-lg border-2 font-medium capitalize text-left transition-all ${difficulty === level
? 'border-cyan-500 bg-cyan-50 text-cyan-800 shadow-sm'
: 'border-slate-200 hover:border-cyan-200 bg-white text-slate-600'
}`}
>
{level}
</button>
))}
</div>
</div>
{/* Card Count */}
<div className="space-y-3">
<label className="text-sm font-bold text-slate-700 uppercase tracking-wide">Number of Cards</label>
<div className="flex flex-col gap-2">
{[5, 10, 15].map(count => (
<button
key={count}
onClick={() => setNumCards(count)}
className={`px-4 py-3 rounded-lg border-2 font-medium text-left transition-all ${numCards === count
? 'border-cyan-500 bg-cyan-50 text-cyan-800 shadow-sm'
: 'border-slate-200 hover:border-cyan-200 bg-white text-slate-600'
}`}
>
{count} Cards
</button>
))}
</div>
</div>
</div>
<button
onClick={handleGenerate}
className="px-10 py-4 bg-cyan-600 hover:bg-cyan-700 text-white font-bold rounded-xl shadow-lg shadow-cyan-200 flex items-center gap-2 hover:scale-105 transition-transform"
>
<BookOpen size={20} /> Generate Flashcards
</button>
</div>
)}
{status === 'loading' && (
<div className="flex flex-col items-center justify-center h-full text-slate-500">
<Loader2 size={48} className="animate-spin text-cyan-500 mb-6" />
<h3 className="text-xl font-medium text-slate-800">Reviewing Documents...</h3>
<p className="mt-2">Identifying core concepts and crafting questions.</p>
</div>
)}
{status === 'studying' && cards.length > 0 && (
<div className="flex flex-col h-full max-w-2xl mx-auto w-full">
{/* Progress Bar */}
<div className="flex items-center justify-between mb-2 text-sm font-medium text-slate-500">
<span>Card {currentIndex + 1} of {cards.length}</span>
<span>{difficulty}</span>
</div>
<div className="h-2 bg-slate-200 rounded-full mb-8 overflow-hidden">
<div
className="h-full bg-cyan-500 transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
{/* Card Container - Perspective */}
<div className="flex-1 relative perspective-1000 cursor-pointer" onClick={handleFlip}>
<div className={`relative w-full h-full transition-transform duration-500 transform-style-3d ${isFlipped ? 'rotate-y-180' : ''}`}>
{/* Front */}
<div className="absolute inset-0 bg-white rounded-2xl shadow-xl flex flex-col items-center justify-center p-8 text-center backface-hidden border-2 border-slate-100 group hover:border-cyan-200 transition-colors">
<span className="text-xs font-bold text-cyan-600 mb-4 bg-cyan-50 px-3 py-1 rounded-full uppercase tracking-wider">Concept</span>
<h3 className="text-3xl font-bold text-slate-800 leading-tight">
{cards[currentIndex].front}
</h3>
<p className="mt-8 text-slate-400 text-sm font-medium flex items-center gap-2">
<RotateCw size={14} /> Click to flip
</p>
</div>
{/* Back */}
<div className="absolute inset-0 bg-gradient-to-br from-cyan-50 to-blue-50 rounded-2xl shadow-xl flex flex-col items-center justify-center p-8 text-center backface-hidden rotate-y-180 border-2 border-cyan-100">
<span className="text-xs font-bold text-slate-500 mb-4 px-3 py-1 rounded-full uppercase tracking-wider">Explanation</span>
<p className="text-xl text-slate-800 leading-relaxed font-medium">
{cards[currentIndex].back}
</p>
<p className="mt-8 text-cyan-600 text-sm font-medium flex items-center gap-2">
<Check size={14} /> Swiped
</p>
</div>
</div>
</div>
{/* Nav */}
<div className="mt-8 flex justify-between items-center px-4">
<button
onClick={(e) => { e.stopPropagation(); handlePrev(); }}
disabled={currentIndex === 0}
className="p-4 rounded-full bg-white text-slate-700 shadow-md border border-slate-100 disabled:opacity-50 hover:bg-slate-50 transition-colors"
>
<ArrowLeft size={24} />
</button>
<span className="text-sm font-bold text-slate-400 uppercase tracking-widest text-[10px]">
Use Space to Flip
</span>
<button
onClick={(e) => { e.stopPropagation(); handleNext(); }}
disabled={currentIndex === cards.length - 1}
className="p-4 rounded-full bg-cyan-600 text-white shadow-lg shadow-cyan-200 disabled:opacity-50 disabled:bg-slate-300 disabled:shadow-none hover:bg-cyan-700 transition-colors"
>
<ArrowRight size={24} />
</button>
</div>
</div>
)}
</div>
</div>
{/* Inline Styles for 3D flip if Tailwind plugins missing */}
<style>{`
.perspective-1000 { perspective: 1000px; }
.transform-style-3d { transform-style: preserve-3d; }
.backface-hidden { backface-visibility: hidden; }
.rotate-y-180 { transform: rotateY(180deg); }
`}</style>
</div>
);
};
export default FlashcardModal;