Spaces:
Sleeping
Sleeping
Download src/app/interview/page.tsx from viraj-421/InterviewAI: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/viraj-421/InterviewAI/resolve/main/src/app/interview/page.tsx
- Command line
-
hf download hf://spaces/viraj-421/InterviewAI/src/app/interview/page.tsx
-
curl -L -o page.tsx https://huggingface.co/spaces/viraj-421/InterviewAI/resolve/main/src/app/interview/page.tsx
13.8 kB
| 'use client'; | |
| import { useState } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { useRouter } from 'next/navigation'; | |
| import { useInterview } from '@/context/InterviewContext'; | |
| import { INTERVIEW_ROLES } from '@/lib/roles'; | |
| import { InterviewRole } from '@/types/interview'; | |
| import { | |
| ArrowRight, | |
| Gauge, | |
| Hash, | |
| Sparkles, | |
| ChevronRight, | |
| Clock, | |
| Timer, | |
| } from 'lucide-react'; | |
| type Difficulty = 'beginner' | 'intermediate' | 'advanced'; | |
| const difficultyConfig: Record<Difficulty, { label: string; description: string; color: string; icon: string }> = { | |
| beginner: { | |
| label: 'Beginner', | |
| description: 'Fundamentals & basic concepts', | |
| color: 'var(--success)', | |
| icon: '🌱', | |
| }, | |
| intermediate: { | |
| label: 'Intermediate', | |
| description: 'Practical application & problem-solving', | |
| color: 'var(--warning)', | |
| icon: '⚡', | |
| }, | |
| advanced: { | |
| label: 'Advanced', | |
| description: 'System design & deep expertise', | |
| color: 'var(--danger)', | |
| icon: '🔥', | |
| }, | |
| }; | |
| const questionCounts = [5, 8, 10, 15]; | |
| const timeLimits = [ | |
| { value: 0, label: 'No Limit', icon: '♾️' }, | |
| { value: 15, label: '15 min', icon: '⏱️' }, | |
| { value: 29, label: '29 min', icon: '🔥' }, | |
| { value: 45, label: '45 min', icon: '⏰' }, | |
| ]; | |
| export default function InterviewPage() { | |
| const router = useRouter(); | |
| const { startSession } = useInterview(); | |
| const [selectedRole, setSelectedRole] = useState<InterviewRole | null>(null); | |
| const [difficulty, setDifficulty] = useState<Difficulty>('intermediate'); | |
| const [questionCount, setQuestionCount] = useState(5); | |
| const [timeLimit, setTimeLimit] = useState(29); | |
| const [step, setStep] = useState<1 | 2>(1); | |
| const handleStartInterview = () => { | |
| if (!selectedRole) return; | |
| startSession({ | |
| role: selectedRole, | |
| difficulty, | |
| questionCount, | |
| timeLimit, | |
| }); | |
| router.push('/interview/session'); | |
| }; | |
| return ( | |
| <div className="min-h-screen py-12 px-6"> | |
| <div className="max-w-6xl mx-auto"> | |
| {/* Header */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="text-center mb-12" | |
| > | |
| <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-4" | |
| style={{ background: 'rgba(108, 92, 231, 0.1)', border: '1px solid rgba(108, 92, 231, 0.2)' }} | |
| > | |
| <Sparkles className="w-4 h-4" style={{ color: 'var(--primary-light)' }} /> | |
| <span className="text-sm font-medium" style={{ color: 'var(--primary-light)' }}> | |
| Step {step} of 2 | |
| </span> | |
| </div> | |
| <h1 className="text-4xl font-bold mb-3"> | |
| {step === 1 ? ( | |
| <>Choose Your <span className="text-gradient">Interview Role</span></> | |
| ) : ( | |
| <>Configure Your <span className="text-gradient">Interview</span></> | |
| )} | |
| </h1> | |
| <p style={{ color: 'var(--muted-light)' }}> | |
| {step === 1 | |
| ? 'Select the position you want to practice interviewing for' | |
| : 'Set your difficulty level and number of questions'} | |
| </p> | |
| </motion.div> | |
| <AnimatePresence mode="wait"> | |
| {step === 1 ? ( | |
| /* Step 1: Role Selection */ | |
| <motion.div | |
| key="step1" | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: -20 }} | |
| className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4" | |
| > | |
| {INTERVIEW_ROLES.map((role, i) => ( | |
| <motion.button | |
| key={role.id} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: i * 0.05 }} | |
| onClick={() => { | |
| setSelectedRole(role); | |
| setStep(2); | |
| }} | |
| className="group text-left p-5 rounded-2xl transition-all duration-300 cursor-pointer" | |
| style={{ | |
| background: selectedRole?.id === role.id ? `${role.color}10` : 'var(--card)', | |
| border: `1px solid ${selectedRole?.id === role.id ? role.color : 'var(--border)'}`, | |
| }} | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.borderColor = role.color; | |
| e.currentTarget.style.background = `${role.color}08`; | |
| e.currentTarget.style.transform = 'translateY(-4px)'; | |
| e.currentTarget.style.boxShadow = `0 8px 30px ${role.color}20`; | |
| }} | |
| onMouseLeave={(e) => { | |
| e.currentTarget.style.borderColor = selectedRole?.id === role.id ? role.color : 'var(--border)'; | |
| e.currentTarget.style.background = selectedRole?.id === role.id ? `${role.color}10` : 'var(--card)'; | |
| e.currentTarget.style.transform = 'translateY(0)'; | |
| e.currentTarget.style.boxShadow = 'none'; | |
| }} | |
| > | |
| <div className="flex items-start justify-between mb-3"> | |
| <span className="text-3xl">{role.icon}</span> | |
| <ChevronRight | |
| className="w-5 h-5 opacity-0 group-hover:opacity-100 transition-all duration-300 group-hover:translate-x-1" | |
| style={{ color: role.color }} | |
| /> | |
| </div> | |
| <h3 className="font-semibold text-base mb-1">{role.title}</h3> | |
| <p className="text-xs leading-relaxed mb-3" style={{ color: 'var(--muted-light)' }}> | |
| {role.description} | |
| </p> | |
| <div className="flex flex-wrap gap-1.5"> | |
| {role.topics.slice(0, 3).map((topic) => ( | |
| <span | |
| key={topic} | |
| className="px-2 py-0.5 rounded-md text-xs" | |
| style={{ | |
| background: `${role.color}10`, | |
| color: role.color, | |
| }} | |
| > | |
| {topic} | |
| </span> | |
| ))} | |
| {role.topics.length > 3 && ( | |
| <span className="px-2 py-0.5 rounded-md text-xs" style={{ color: 'var(--muted)' }}> | |
| +{role.topics.length - 3} | |
| </span> | |
| )} | |
| </div> | |
| </motion.button> | |
| ))} | |
| </motion.div> | |
| ) : ( | |
| /* Step 2: Configuration */ | |
| <motion.div | |
| key="step2" | |
| initial={{ opacity: 0, x: 20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: 20 }} | |
| className="max-w-2xl mx-auto space-y-8" | |
| > | |
| {/* Selected Role Card */} | |
| {selectedRole && ( | |
| <div | |
| className="flex items-center gap-4 p-5 rounded-2xl cursor-pointer" | |
| style={{ | |
| background: `${selectedRole.color}08`, | |
| border: `1px solid ${selectedRole.color}40`, | |
| }} | |
| onClick={() => setStep(1)} | |
| > | |
| <span className="text-3xl">{selectedRole.icon}</span> | |
| <div className="flex-1"> | |
| <h3 className="font-semibold">{selectedRole.title}</h3> | |
| <p className="text-sm" style={{ color: 'var(--muted-light)' }}> | |
| {selectedRole.description} | |
| </p> | |
| </div> | |
| <span className="text-sm" style={{ color: 'var(--primary-light)' }}> | |
| Change → | |
| </span> | |
| </div> | |
| )} | |
| {/* Difficulty Selection */} | |
| <div> | |
| <label className="flex items-center gap-2 text-sm font-medium mb-3" style={{ color: 'var(--muted-light)' }}> | |
| <Gauge className="w-4 h-4" /> | |
| Difficulty Level | |
| </label> | |
| <div className="grid grid-cols-3 gap-3"> | |
| {(Object.entries(difficultyConfig) as [Difficulty, typeof difficultyConfig.beginner][]).map(([key, config]) => ( | |
| <button | |
| key={key} | |
| onClick={() => setDifficulty(key)} | |
| className="p-4 rounded-xl text-left transition-all duration-300 cursor-pointer" | |
| style={{ | |
| background: difficulty === key ? `${config.color}10` : 'var(--card)', | |
| border: `1px solid ${difficulty === key ? config.color : 'var(--border)'}`, | |
| boxShadow: difficulty === key ? `0 0 20px ${config.color}20` : 'none', | |
| }} | |
| > | |
| <span className="text-xl mb-2 block">{config.icon}</span> | |
| <span className="font-semibold text-sm block" style={{ color: difficulty === key ? config.color : 'var(--foreground)' }}> | |
| {config.label} | |
| </span> | |
| <span className="text-xs block mt-1" style={{ color: 'var(--muted)' }}> | |
| {config.description} | |
| </span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Question Count */} | |
| <div> | |
| <label className="flex items-center gap-2 text-sm font-medium mb-3" style={{ color: 'var(--muted-light)' }}> | |
| <Hash className="w-4 h-4" /> | |
| Number of Questions | |
| </label> | |
| <div className="flex gap-3"> | |
| {questionCounts.map((count) => ( | |
| <button | |
| key={count} | |
| onClick={() => setQuestionCount(count)} | |
| className="flex-1 py-3 rounded-xl font-semibold text-center transition-all duration-300 cursor-pointer" | |
| style={{ | |
| background: questionCount === count ? 'var(--primary)' : 'var(--card)', | |
| border: `1px solid ${questionCount === count ? 'var(--primary)' : 'var(--border)'}`, | |
| color: questionCount === count ? 'white' : 'var(--foreground)', | |
| boxShadow: questionCount === count ? '0 0 20px rgba(108, 92, 231, 0.3)' : 'none', | |
| }} | |
| > | |
| {count} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Exam Timer */} | |
| <div> | |
| <label className="flex items-center gap-2 text-sm font-medium mb-3" style={{ color: 'var(--muted-light)' }}> | |
| <Clock className="w-4 h-4" /> | |
| Exam Timer | |
| </label> | |
| <div className="grid grid-cols-4 gap-3"> | |
| {timeLimits.map((tl) => ( | |
| <button | |
| key={tl.value} | |
| onClick={() => setTimeLimit(tl.value)} | |
| className="p-3 rounded-xl text-center transition-all duration-300 cursor-pointer" | |
| style={{ | |
| background: timeLimit === tl.value ? 'rgba(0, 206, 201, 0.1)' : 'var(--card)', | |
| border: `1px solid ${timeLimit === tl.value ? 'var(--secondary)' : 'var(--border)'}`, | |
| boxShadow: timeLimit === tl.value ? '0 0 20px rgba(0, 206, 201, 0.2)' : 'none', | |
| }} | |
| > | |
| <span className="text-lg block mb-1">{tl.icon}</span> | |
| <span className="text-xs font-semibold block" style={{ color: timeLimit === tl.value ? 'var(--secondary)' : 'var(--foreground)' }}> | |
| {tl.label} | |
| </span> | |
| </button> | |
| ))} | |
| </div> | |
| {timeLimit > 0 && ( | |
| <p className="text-xs mt-2 flex items-center gap-1" style={{ color: 'var(--warning)' }}> | |
| <Timer className="w-3 h-3" /> | |
| Interview will auto-submit after {timeLimit} minutes | |
| </p> | |
| )} | |
| </div> | |
| {/* Start Button */} | |
| <motion.button | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| onClick={handleStartInterview} | |
| className="w-full py-4 rounded-xl font-bold text-lg flex items-center justify-center gap-3 cursor-pointer" | |
| style={{ | |
| background: 'var(--gradient-primary)', | |
| color: 'white', | |
| boxShadow: '0 8px 30px rgba(108, 92, 231, 0.3)', | |
| }} | |
| > | |
| <Sparkles className="w-5 h-5" /> | |
| Start Interview | |
| <ArrowRight className="w-5 h-5" /> | |
| </motion.button> | |
| {/* Back button */} | |
| <button | |
| onClick={() => setStep(1)} | |
| className="w-full py-3 rounded-xl text-sm font-medium transition-all duration-300 cursor-pointer" | |
| style={{ | |
| color: 'var(--muted)', | |
| border: '1px solid var(--border)', | |
| }} | |
| > | |
| ← Back to Role Selection | |
| </button> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| ); | |
| } | |