| 'use client'; |
|
|
| import React, { useState } from 'react'; |
| import { motion, AnimatePresence } from 'framer-motion'; |
| import { Upload, Search, Zap, CheckCircle2, AlertCircle, Loader2, BarChart3, Users } from 'lucide-react'; |
| import { uploadCSV, runEvaluation } from '@/lib/api'; |
| import CandidateTable from '@/components/CandidateTable'; |
| import CandidateDetail from '@/components/CandidateDetail'; |
|
|
| export default function Dashboard() { |
| const [jd, setJd] = useState(''); |
| const [candidates, setCandidates] = useState([]); |
| const [evaluations, setEvaluations] = useState([]); |
| const [isUploading, setIsUploading] = useState(false); |
| const [isEvaluating, setIsEvaluating] = useState(false); |
| const [selectedCandidate, setSelectedCandidate] = useState(null); |
|
|
| const handleFileUpload = async (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
|
|
| setIsUploading(true); |
| try { |
| const data = await uploadCSV(file); |
| setCandidates(data.candidates); |
| } catch (err) { |
| console.error(err); |
| alert('Failed to upload CSV. Check console for details.'); |
| } finally { |
| setIsUploading(false); |
| } |
| }; |
|
|
| const handleEvaluate = async () => { |
| if (!jd || candidates.length === 0) { |
| alert('Please provide a JD and upload candidates.'); |
| return; |
| } |
|
|
| setIsEvaluating(true); |
| try { |
| const data = await runEvaluation(jd, candidates); |
| setEvaluations(data.shortlist); |
| |
| window.evalDetails = data.details; |
| } catch (err) { |
| console.error(err); |
| alert('Evaluation failed. Check backend/Groq keys.'); |
| } finally { |
| setIsEvaluating(false); |
| } |
| }; |
|
|
| return ( |
| <div className="container mx-auto px-6 py-12"> |
| <header className="mb-12"> |
| <motion.h2 |
| initial={{ opacity: 0, x: -20 }} |
| animate={{ opacity: 1, x: 0 }} |
| className="text-4xl font-extrabold tracking-tight" |
| > |
| Candidate <span className="gradient-text">Ranking Platform</span> |
| </motion.h2> |
| <p className="text-slate-500 mt-2 text-lg">Intelligent multi-agent assessment for your next top hire.</p> |
| </header> |
| |
| <div className="grid grid-cols-1 lg:grid-cols-12 gap-8"> |
| {/* Left Column: Input */} |
| <div className="lg:col-span-4 space-y-6"> |
| <section className="glass-card p-6 rounded-2xl"> |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> |
| <Zap className="w-5 h-5 text-indigo-500" /> Job Description |
| </h3> |
| <textarea |
| className="w-full h-64 p-4 bg-white/50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all resize-none text-sm" |
| placeholder="Paste the job description here..." |
| value={jd} |
| onChange={(e) => setJd(e.target.value)} |
| /> |
| </section> |
| |
| <section className="glass-card p-6 rounded-2xl"> |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-indigo-500"> |
| <Upload className="w-5 h-5" /> Candidate Data (CSV) |
| </h3> |
| <div className="relative group"> |
| <input |
| type="file" |
| className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" |
| onChange={handleFileUpload} |
| accept=".csv" |
| /> |
| <div className="border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-xl p-8 text-center group-hover:border-indigo-500 transition-colors"> |
| {isUploading ? ( |
| <Loader2 className="w-8 h-8 mx-auto animate-spin text-indigo-500" /> |
| ) : ( |
| <> |
| <Users className="w-8 h-8 mx-auto text-slate-400 group-hover:text-indigo-500 transition-colors mb-2" /> |
| <p className="text-sm font-medium">Click or drag CSV</p> |
| <p className="text-xs text-slate-500 mt-1">{candidates.length > 0 ? `${candidates.length} candidates loaded` : 'Supports names, skills, email...'}</p> |
| </> |
| )} |
| </div> |
| </div> |
| </section> |
| |
| <button |
| onClick={handleEvaluate} |
| disabled={isEvaluating || !jd || candidates.length === 0} |
| className={`w-full py-4 rounded-xl flex items-center justify-center gap-2 font-bold text-white transition-all shadow-lg ${ |
| isEvaluating || !jd || candidates.length === 0 |
| ? 'bg-slate-400 cursor-not-allowed' |
| : 'bg-indigo-600 hover:bg-indigo-700 hover:scale-[1.02] active:scale-[0.98]' |
| }`} |
| > |
| {isEvaluating ? ( |
| <Loader2 className="w-5 h-5 animate-spin" /> |
| ) : ( |
| <BarChart3 className="w-5 h-5" /> |
| )} |
| {isEvaluating ? 'Running AI Agents...' : 'Run Evaluation'} |
| </button> |
| </div> |
| |
| {/* Right Column: Results */} |
| <div className="lg:col-span-8"> |
| <div className="glass-card rounded-2xl min-h-[600px] relative overflow-hidden"> |
| <AnimatePresence mode="wait"> |
| {evaluations.length > 0 ? ( |
| <motion.div |
| key="results" |
| initial={{ opacity: 0 }} |
| animate={{ opacity: 1 }} |
| exit={{ opacity: 0 }} |
| className="p-6" |
| > |
| <CandidateTable |
| evaluations={evaluations} |
| onViewDetail={(c) => setSelectedCandidate(c)} |
| /> |
| </motion.div> |
| ) : ( |
| <motion.div |
| key="empty" |
| initial={{ opacity: 0 }} |
| animate={{ opacity: 1 }} |
| exit={{ opacity: 0 }} |
| className="absolute inset-0 flex flex-col items-center justify-center text-slate-400 p-12 text-center" |
| > |
| <Search className="w-16 h-16 mb-4 opacity-20" /> |
| <h3 className="text-xl font-medium">No Evaluations Yet</h3> |
| <p className="max-w-xs mt-2 text-sm">Upload a CSV, paste a JD, and hit Run to see the magic happen with Groq LLMs.</p> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </div> |
| </div> |
| </div> |
|
|
| {} |
| <AnimatePresence> |
| {selectedCandidate && ( |
| <CandidateDetail |
| evaluation={selectedCandidate} |
| onClose={() => setSelectedCandidate(null)} |
| /> |
| )} |
| </AnimatePresence> |
| </div> |
| ); |
| } |
|
|