ai-interviewer-version-2 / frontend /src /components /InterviewModeSelector.jsx
chowdary1's picture
V2 Full Sync: 4-mode architecture, compiler, RAG, neon theme, round detection fix, camera toggle
7fff83a
Raw History Blame Contribute Delete
30.4 kB
/**
* Interview Mode Selector
*
* FOUR MODES ONLY:
* 1. Friendly - Database questions only
* 2. Balanced - API + video
* 3. Strict - RAG + verdicts
* 4. Final Boss - Full power + personality selection
*
* This component enforces the 4-mode architecture.
* Personality selection only appears for Final Boss mode.
*
* NOTE: All modes currently UNLOCKED. Payment integration pending.
*/
import React, { useState, useEffect } from 'react';
import { ChevronDown, Check, Lock, Zap, Crown, Skull, Video, Scale, Brain, User, Sparkles, Map } from 'lucide-react';
import {
INTERVIEW_MODES,
getInterviewModeList,
FINAL_BOSS_PERSONALITIES,
getFinalBossPersonalityList,
modeAllowsPersonality
} from '../services/scoringService';
// Mode tier badges (visual only - all modes unlocked)
const TIER_BADGES = {
free: { label: 'Free', color: '#10b981', icon: null },
paid_entry: { label: 'Basic', color: '#3b82f6', icon: <Zap size={12} /> },
paid_advanced: { label: 'Advanced', color: '#f59e0b', icon: <Crown size={12} /> },
premium: { label: 'Premium', color: '#ef4444', icon: <Skull size={12} /> },
};
// Feature configs with colors and icons
const FEATURE_CONFIG = {
video: { label: 'Video', icon: Video, color: '#3b82f6' },
verdict: { label: 'Verdict', icon: Scale, color: '#8b5cf6' },
rag: { label: 'RAG', icon: Brain, color: '#f59e0b' },
persona: { label: 'Persona', icon: User, color: '#ef4444' },
llm: { label: 'LLM', icon: Sparkles, color: '#22d3ee' },
roadmap: { label: 'Roadmap', icon: Map, color: '#a855f7' },
};
/**
* Main Mode Card Component
*/
function ModeCard({ mode, isSelected, onSelect, disabled, userTier = 'premium', isDark }) {
const tierInfo = TIER_BADGES[mode.tier] || TIER_BADGES.free;
// Check if user can access this mode
const tierOrder = ['free', 'paid_entry', 'paid_advanced', 'premium'];
const userTierIndex = tierOrder.indexOf(userTier);
const requiredTierIndex = tierOrder.indexOf(mode.tier);
const canAccess = userTierIndex >= requiredTierIndex;
const isLocked = !canAccess;
// Dark mode colors
const cardBg = isDark ? '#1e293b' : '#ffffff';
const cardBorder = isDark ? '#475569' : '#e2e8f0';
const textPrimary = isDark ? '#ffffff' : '#1e293b';
const textSecondary = isDark ? '#cbd5e1' : '#64748b';
return (
<div
className={`mode-card ${isSelected ? 'selected' : ''} ${isLocked ? 'locked' : ''} ${disabled ? 'disabled' : ''}`}
onClick={() => !disabled && !isLocked && onSelect(mode.id)}
style={{
'--mode-color': mode.color,
background: isSelected
? (isDark
? `linear-gradient(145deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.9))`
: `linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.95))`)
: (isDark
? 'linear-gradient(145deg, rgba(30, 41, 59, 0.8), rgba(15, 23, 42, 0.7))'
: 'linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.85))'),
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
borderColor: isSelected ? mode.color : (isDark ? 'rgba(148, 163, 184, 0.15)' : 'rgba(203, 213, 225, 0.5)'),
borderWidth: isSelected ? '2px' : '1px',
boxShadow: isSelected
? `0 8px 32px ${mode.color}30, 0 4px 16px rgba(0, 0, 0, 0.15), inset 0 1px 0 ${isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.9)'}`
: `0 4px 20px rgba(0, 0, 0, ${isDark ? '0.25' : '0.08'}), inset 0 1px 0 ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.7)'}`,
transform: isSelected ? 'scale(1.02)' : 'scale(1)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
<div className="mode-header">
<div style={{
width: '72px',
height: '72px',
borderRadius: '16px',
background: isDark
? `linear-gradient(145deg, ${mode.color}25, ${mode.color}10)`
: `linear-gradient(145deg, ${mode.color}20, ${mode.color}08)`,
boxShadow: `0 4px 16px ${mode.color}30`,
border: `2px solid ${mode.color}40`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
}}>
<span style={{
fontSize: '2.5rem',
lineHeight: 1,
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))',
}}>{mode.icon}</span>
</div>
<div className="mode-tier-badge" style={{
background: isDark
? `linear-gradient(135deg, ${tierInfo.color}50, ${tierInfo.color}30)`
: `linear-gradient(135deg, ${tierInfo.color}25, ${tierInfo.color}15)`,
color: isDark ? '#fff' : tierInfo.color,
fontWeight: 700,
padding: '8px 16px',
fontSize: '0.7rem',
letterSpacing: '0.5px',
textTransform: 'uppercase',
textShadow: 'none',
border: `1.5px solid ${tierInfo.color}${isDark ? '60' : '40'}`,
borderRadius: '12px',
boxShadow: `0 2px 8px ${tierInfo.color}30`,
}}>
{tierInfo.icon}
<span>{tierInfo.label}</span>
</div>
</div>
<div className="mode-content">
<h3 className="mode-name" style={{ color: textPrimary, fontWeight: 700, fontSize: '1.15rem', marginBottom: '8px' }}>{mode.name}</h3>
<p className="mode-description" style={{
color: textSecondary,
lineHeight: 1.5,
fontSize: '0.85rem',
margin: 0,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
minHeight: '2.55em',
}}>{mode.description}</p>
</div>
<div className="mode-features" style={{
display: 'flex',
flexWrap: 'wrap',
gap: '6px',
marginTop: '12px',
minHeight: '32px',
}}>
{mode.config.usesLLM && (
<span className="feature-tag" style={{
background: isDark ? `${FEATURE_CONFIG.llm.color}30` : `${FEATURE_CONFIG.llm.color}15`,
color: isDark ? '#67e8f9' : FEATURE_CONFIG.llm.color,
border: `1px solid ${FEATURE_CONFIG.llm.color}${isDark ? '60' : '30'}`,
padding: '4px 10px',
borderRadius: '20px',
fontSize: '0.7rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
}}>
<Sparkles size={11} /> LLM
</span>
)}
{mode.config.hasVideo && (
<span className="feature-tag" style={{
background: isDark ? `${FEATURE_CONFIG.video.color}30` : `${FEATURE_CONFIG.video.color}15`,
color: isDark ? '#93c5fd' : FEATURE_CONFIG.video.color,
border: `1px solid ${FEATURE_CONFIG.video.color}${isDark ? '60' : '30'}`,
padding: '4px 10px',
borderRadius: '20px',
fontSize: '0.7rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
}}>
<Video size={11} /> Video
</span>
)}
{mode.config.hasVerdict && (
<span className="feature-tag" style={{
background: isDark ? `${FEATURE_CONFIG.verdict.color}30` : `${FEATURE_CONFIG.verdict.color}15`,
color: isDark ? '#c4b5fd' : FEATURE_CONFIG.verdict.color,
border: `1px solid ${FEATURE_CONFIG.verdict.color}${isDark ? '60' : '30'}`,
padding: '4px 10px',
borderRadius: '20px',
fontSize: '0.7rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
}}>
<Scale size={11} /> Verdict
</span>
)}
{mode.config.hasRag && (
<span className="feature-tag" style={{
background: isDark ? `${FEATURE_CONFIG.rag.color}30` : `${FEATURE_CONFIG.rag.color}15`,
color: isDark ? '#fcd34d' : FEATURE_CONFIG.rag.color,
border: `1px solid ${FEATURE_CONFIG.rag.color}${isDark ? '60' : '30'}`,
padding: '4px 10px',
borderRadius: '20px',
fontSize: '0.7rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
}}>
<Brain size={11} /> RAG
</span>
)}
{mode.config.hasPersonalitySelection && (
<span className="feature-tag" style={{
background: isDark ? `${FEATURE_CONFIG.persona.color}30` : `${FEATURE_CONFIG.persona.color}15`,
color: isDark ? '#fca5a5' : FEATURE_CONFIG.persona.color,
border: `1px solid ${FEATURE_CONFIG.persona.color}${isDark ? '60' : '30'}`,
padding: '4px 10px',
borderRadius: '20px',
fontSize: '0.7rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
}}>
<User size={11} /> Persona
</span>
)}
{mode.config.hasRoadmap && (
<span className="feature-tag" style={{
background: isDark ? `${FEATURE_CONFIG.roadmap.color}30` : `${FEATURE_CONFIG.roadmap.color}15`,
color: isDark ? '#d8b4fe' : FEATURE_CONFIG.roadmap.color,
border: `1px solid ${FEATURE_CONFIG.roadmap.color}${isDark ? '60' : '30'}`,
padding: '4px 10px',
borderRadius: '20px',
fontSize: '0.7rem',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
whiteSpace: 'nowrap',
}}>
<Map size={11} /> Roadmap
</span>
)}
</div>
{isSelected && !isLocked && (
<div className="mode-selected-indicator">
<Check size={18} />
</div>
)}
{isLocked && (
<div className="mode-locked-overlay">
<Lock size={24} />
<span>Upgrade to {tierInfo.label}</span>
</div>
)}
</div>
);
}
/**
* Final Boss Personality Selector
* Only shown when mode = 'final_boss'
*/
function FinalBossPersonalitySelector({ value, onChange, disabled }) {
const [isOpen, setIsOpen] = useState(false);
const personalities = getFinalBossPersonalityList();
const selected = FINAL_BOSS_PERSONALITIES[value] || FINAL_BOSS_PERSONALITIES.faang_engineer;
return (
<div className="personality-subselector">
<label className="subselector-label">
<Skull size={16} />
Final Boss Persona
</label>
<p className="subselector-hint">
Choose your interviewer persona for maximum challenge
</p>
<div className="subselector-container">
<button
type="button"
className={`subselector-trigger ${isOpen ? 'open' : ''}`}
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
>
<span className="persona-icon">{selected.icon}</span>
<div className="persona-info">
<span className="persona-name">{selected.name}</span>
<span className="persona-desc">{selected.description}</span>
</div>
<ChevronDown size={18} className={`chevron ${isOpen ? 'rotated' : ''}`} />
</button>
{isOpen && (
<>
<div className="subselector-backdrop" onClick={() => setIsOpen(false)} />
<div className="subselector-dropdown">
{personalities.map((persona) => (
<button
key={persona.id}
type="button"
className={`dropdown-option ${persona.id === value ? 'selected' : ''}`}
onClick={() => {
onChange(persona.id);
setIsOpen(false);
}}
>
<span className="persona-icon">{persona.icon}</span>
<div className="persona-info">
<span className="persona-name">{persona.name}</span>
<span className="persona-desc">{persona.description}</span>
</div>
{persona.id === value && <Check size={18} className="check-icon" />}
</button>
))}
</div>
</>
)}
</div>
</div>
);
}
/**
* Main Interview Mode Selector Component
*/
function InterviewModeSelector({
mode = 'friendly',
onModeChange,
personality = 'faang_engineer',
onPersonalityChange,
disabled = false,
userTier = 'premium' // All modes unlocked - payment integration pending
}) {
const modes = getInterviewModeList();
const showPersonalitySelector = modeAllowsPersonality(mode);
// Detect dark mode
const [isDark, setIsDark] = useState(() => {
// Initialize from current theme
if (typeof document !== 'undefined') {
return document.documentElement.getAttribute('data-theme') === 'dark';
}
return false;
});
useEffect(() => {
if (typeof document === 'undefined') return;
const checkTheme = () => {
try {
const theme = document.documentElement.getAttribute('data-theme');
setIsDark(theme === 'dark');
} catch (e) {
console.warn('Theme check error:', e);
}
};
checkTheme();
// Watch for theme changes
let observer = null;
try {
observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
} catch (e) {
console.warn('MutationObserver error:', e);
}
return () => {
if (observer) observer.disconnect();
};
}, []);
// Theme-aware colors
const headerColor = isDark ? '#ffffff' : '#1e293b';
const subheaderColor = isDark ? '#94a3b8' : '#64748b';
return (
<div className="interview-mode-selector">
<div className="mode-selector-header">
<h3 style={{ color: headerColor }}>Choose Interview Mode</h3>
<p style={{ color: subheaderColor }}>Select the interview difficulty and style that matches your preparation level</p>
</div>
<div className="mode-grid">
{modes.map((m) => (
<ModeCard
key={m.id}
mode={m}
isSelected={mode === m.id}
onSelect={onModeChange}
disabled={disabled}
userTier={userTier}
isDark={isDark}
/>
))}
</div>
{/* Final Boss Personality Sub-Selector */}
{showPersonalitySelector && (
<FinalBossPersonalitySelector
value={personality}
onChange={onPersonalityChange}
disabled={disabled}
/>
)}
<style jsx>{`
.interview-mode-selector {
margin-bottom: var(--space-6);
}
.mode-selector-header {
margin-bottom: var(--space-4);
}
.mode-selector-header h3 {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0 0 var(--space-1) 0;
}
[data-theme="dark"] .mode-selector-header h3 {
color: #f8fafc;
}
.mode-selector-header p {
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0;
}
[data-theme="dark"] .mode-selector-header p {
color: #94a3b8;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-4);
}
@media (max-width: 768px) {
.mode-grid {
grid-template-columns: 1fr;
}
}
.mode-card {
position: relative;
padding: var(--space-4);
background: var(--bg-card, #ffffff);
border: 2px solid var(--border-color, #e2e8f0);
border-radius: var(--radius-xl);
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 200px;
}
.mode-card:hover:not(.locked):not(.disabled) {
border-color: var(--mode-color);
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
[data-theme="dark"] .mode-card:hover:not(.locked):not(.disabled) {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4), 0 0 20px var(--mode-color);
}
.mode-card.selected {
border-color: var(--mode-color);
border-width: 2px;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mode-color) 20%, transparent);
}
[data-theme="dark"] .mode-card.selected {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mode-color) 30%, transparent), 0 0 30px var(--mode-color);
}
.mode-card.locked {
opacity: 0.6;
cursor: not-allowed;
}
.mode-card.disabled {
opacity: 0.5;
pointer-events: none;
}
.mode-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--space-3);
gap: var(--space-2);
}
.mode-icon {
font-size: 2.5rem;
flex-shrink: 0;
}
.mode-tier-badge {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: var(--font-bold);
flex-shrink: 0;
}
.mode-content {
margin-bottom: var(--space-3);
flex: 1;
min-height: 0;
}
.mode-name {
font-size: 1.1rem;
font-weight: var(--font-bold);
color: var(--text-primary, #1e293b);
margin: 0 0 var(--space-2) 0;
}
.mode-description {
font-size: 0.85rem;
color: var(--text-secondary, #64748b);
margin: 0;
line-height: 1.5;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.mode-features {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: auto;
padding-top: var(--space-2);
}
.feature-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.3px;
white-space: nowrap;
}
.mode-selected-indicator {
position: absolute;
top: var(--space-3);
right: var(--space-3);
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--mode-color);
color: white;
border-radius: var(--radius-full);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.mode-locked-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
background: rgba(0, 0, 0, 0.7);
color: white;
font-size: var(--text-sm);
font-weight: var(--font-medium);
backdrop-filter: blur(4px);
}
.feature-tag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
}
.mode-selected-indicator {
position: absolute;
top: var(--space-3);
right: var(--space-3);
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--mode-color);
color: white;
border-radius: var(--radius-full);
}
.mode-locked-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
background: rgba(0, 0, 0, 0.6);
color: white;
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
/* Personality Sub-Selector */
.personality-subselector {
margin-top: var(--space-5);
padding: var(--space-4);
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05));
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: var(--radius-xl);
}
.subselector-label {
display: flex;
align-items: center;
gap: var(--space-2);
font-weight: var(--font-semibold);
font-size: var(--text-sm);
color: #ef4444;
margin-bottom: var(--space-1);
}
.subselector-hint {
font-size: var(--text-xs);
color: var(--text-secondary);
margin: 0 0 var(--space-3) 0;
}
.subselector-container {
position: relative;
}
.subselector-trigger {
display: flex;
align-items: center;
gap: var(--space-3);
width: 100%;
padding: var(--space-3) var(--space-4);
background: var(--bg-card);
border: 2px solid rgba(239, 68, 68, 0.3);
border-radius: var(--radius-lg);
text-align: left;
cursor: pointer;
transition: all var(--transition-fast);
}
.subselector-trigger:hover:not(:disabled) {
border-color: #ef4444;
}
.subselector-trigger.open {
border-color: #ef4444;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.persona-icon {
font-size: 1.5rem;
}
.persona-info {
flex: 1;
display: flex;
flex-direction: column;
}
.persona-name {
font-weight: var(--font-semibold);
font-size: var(--text-sm);
color: var(--text-primary);
}
.persona-desc {
font-size: var(--text-xs);
color: var(--text-secondary);
}
.chevron {
transition: transform var(--transition-fast);
color: var(--text-tertiary);
}
.chevron.rotated {
transform: rotate(180deg);
}
.subselector-backdrop {
position: fixed;
inset: 0;
z-index: 10;
}
.subselector-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-card);
border: 2px solid #ef4444;
border-top: none;
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
z-index: 20;
max-height: 300px;
overflow-y: auto;
}
.dropdown-option {
display: flex;
align-items: center;
gap: var(--space-3);
width: 100%;
padding: var(--space-3) var(--space-4);
background: transparent;
border: none;
text-align: left;
cursor: pointer;
transition: background var(--transition-fast);
}
.dropdown-option:hover {
background: rgba(239, 68, 68, 0.1);
}
.dropdown-option.selected {
background: rgba(239, 68, 68, 0.15);
}
.check-icon {
color: #ef4444;
}
`}</style>
</div>
);
}
export default InterviewModeSelector;