chowdary1's picture
V2 Full Sync: 4-mode architecture, compiler, RAG, neon theme, round detection fix, camera toggle
7fff83a
Raw History Blame Contribute Delete
15.5 kB
/**
* Interview Scoring Service
*
* Rule-based smart scoring for interview performance.
* Uses heuristics instead of heavy ML for fast execution.
*
* Scores:
* - Speech Clarity (filler words, pauses, speed)
* - Confidence (answer length, hesitation)
* - Eye Contact (simulated from webcam availability)
* - Technical Depth (keyword density, structure)
*/
// ===========================================
// FILLER WORDS DATABASE
// ===========================================
const FILLER_WORDS = [
'um', 'uh', 'er', 'ah', 'like', 'you know', 'basically',
'actually', 'literally', 'honestly', 'right', 'so yeah',
'kind of', 'sort of', 'i mean', 'well', 'okay so',
];
const HEDGING_WORDS = [
'maybe', 'perhaps', 'probably', 'might', 'could be',
'i think', 'i guess', 'i believe', 'i suppose',
'not sure', 'possibly', 'somewhat',
];
const TECHNICAL_KEYWORDS = [
// Programming
'algorithm', 'data structure', 'complexity', 'optimization',
'scalability', 'performance', 'architecture', 'design pattern',
'microservices', 'api', 'database', 'cache', 'queue',
// Concepts
'trade-off', 'approach', 'solution', 'implementation',
'refactor', 'debug', 'testing', 'deployment', 'monitoring',
// Methods
'agile', 'scrum', 'sprint', 'iteration', 'review',
];
const STAR_METHOD_INDICATORS = [
'situation', 'task', 'action', 'result',
'challenge', 'approach', 'outcome', 'learned',
'responsibility', 'achieved', 'improved', 'delivered',
];
// ===========================================
// SCORING FUNCTIONS
// ===========================================
/**
* Calculate speech clarity score (0-100)
* Based on filler words, sentence structure, and word count
*/
export function calculateClarityScore(answer) {
if (!answer || answer.trim().length === 0) return 0;
const text = answer.toLowerCase();
const words = text.split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
if (wordCount < 5) return 20; // Too short
// Count filler words
let fillerCount = 0;
FILLER_WORDS.forEach(filler => {
const regex = new RegExp(`\\b${filler}\\b`, 'gi');
const matches = text.match(regex);
if (matches) fillerCount += matches.length;
});
// Filler ratio (lower is better)
const fillerRatio = fillerCount / wordCount;
const fillerPenalty = Math.min(40, fillerRatio * 200);
// Sentence count and average length
const sentences = answer.split(/[.!?]+/).filter(s => s.trim().length > 0);
const avgSentenceLength = wordCount / Math.max(1, sentences.length);
// Ideal sentence length is 15-25 words
let structurePenalty = 0;
if (avgSentenceLength < 8) structurePenalty = 15;
else if (avgSentenceLength > 35) structurePenalty = 20;
// Word length variety (indicates vocabulary)
const avgWordLength = words.reduce((sum, w) => sum + w.length, 0) / wordCount;
const vocabularyBonus = avgWordLength > 5 ? 10 : 0;
// Calculate final score
let score = 100 - fillerPenalty - structurePenalty + vocabularyBonus;
// Clamp to 0-100
return Math.max(0, Math.min(100, Math.round(score)));
}
/**
* Calculate confidence score (0-100)
* Based on hedging words, answer length, and assertive language
*/
export function calculateConfidenceScore(answer) {
if (!answer || answer.trim().length === 0) return 0;
const text = answer.toLowerCase();
const words = text.split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
if (wordCount < 5) return 15; // Very short = low confidence
// Count hedging words
let hedgingCount = 0;
HEDGING_WORDS.forEach(hedge => {
const regex = new RegExp(`\\b${hedge}\\b`, 'gi');
const matches = text.match(regex);
if (matches) hedgingCount += matches.length;
});
// Hedging ratio (lower is better)
const hedgingRatio = hedgingCount / wordCount;
const hedgingPenalty = Math.min(35, hedgingRatio * 250);
// Answer length bonus (longer = more confident, up to a point)
let lengthBonus = 0;
if (wordCount >= 30) lengthBonus = 10;
if (wordCount >= 60) lengthBonus = 15;
if (wordCount >= 100) lengthBonus = 20;
// Assertive language detection
const assertivePatterns = [
/\bi (did|led|managed|created|built|designed|implemented)\b/gi,
/\bmy (approach|solution|responsibility)\b/gi,
/\bsuccessfully\b/gi,
/\bachieved\b/gi,
/\bdelivered\b/gi,
];
let assertiveBonus = 0;
assertivePatterns.forEach(pattern => {
if (pattern.test(text)) assertiveBonus += 5;
});
assertiveBonus = Math.min(20, assertiveBonus);
// Calculate final score
let score = 70 - hedgingPenalty + lengthBonus + assertiveBonus;
// Clamp to 0-100
return Math.max(0, Math.min(100, Math.round(score)));
}
/**
* Calculate technical depth score (0-100)
* Based on technical keywords and structured responses
*/
export function calculateTechnicalScore(answer) {
if (!answer || answer.trim().length === 0) return 0;
const text = answer.toLowerCase();
const words = text.split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
if (wordCount < 10) return 10;
// Count technical keywords
let technicalCount = 0;
TECHNICAL_KEYWORDS.forEach(keyword => {
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
const matches = text.match(regex);
if (matches) technicalCount += matches.length;
});
// Technical density bonus
const technicalRatio = technicalCount / wordCount;
const technicalBonus = Math.min(30, technicalRatio * 500);
// STAR method indicators
let starCount = 0;
STAR_METHOD_INDICATORS.forEach(indicator => {
if (text.includes(indicator)) starCount++;
});
const starBonus = Math.min(20, starCount * 5);
// Specific example detection
const hasExample = /for example|such as|instance|specifically|when i/gi.test(text);
const exampleBonus = hasExample ? 15 : 0;
// Numbers and metrics (shows quantifiable experience)
const hasNumbers = /\d+/.test(answer);
const metricsBonus = hasNumbers ? 10 : 0;
// Base score from length
let baseScore = 40;
if (wordCount >= 50) baseScore = 50;
if (wordCount >= 80) baseScore = 55;
// Calculate final score
let score = baseScore + technicalBonus + starBonus + exampleBonus + metricsBonus;
// Clamp to 0-100
return Math.max(0, Math.min(100, Math.round(score)));
}
/**
* Calculate eye contact score (0-100)
* Simulated based on webcam availability and face detection
*/
export function calculateEyeContactScore(stats) {
const {
totalDuration = 0, // Total interview duration in seconds
cameraActiveTime = 0, // Time camera was on
faceDetectedTime = 0, // Time face was detected
lookAwayCount = 0, // Times user looked away
} = stats;
if (totalDuration === 0) return 50; // Default if no data
// Camera usage ratio
const cameraRatio = cameraActiveTime / totalDuration;
const cameraScore = cameraRatio * 40; // Max 40 points
// Face detection ratio
const faceRatio = cameraActiveTime > 0
? faceDetectedTime / cameraActiveTime
: 0;
const faceScore = faceRatio * 40; // Max 40 points
// Look away penalty (normalized per minute)
const durationMinutes = totalDuration / 60;
const lookAwayPerMinute = lookAwayCount / Math.max(1, durationMinutes);
const lookAwayPenalty = Math.min(20, lookAwayPerMinute * 4);
// Calculate final score
let score = cameraScore + faceScore + 20 - lookAwayPenalty;
// Clamp to 0-100
return Math.max(0, Math.min(100, Math.round(score)));
}
/**
* Calculate overall interview score from components
*/
export function calculateOverallScore(scores) {
const {
clarity = 50,
confidence = 50,
technical = 50,
eyeContact = 50,
} = scores;
// Weighted average
const overall = (
clarity * 0.25 +
confidence * 0.30 +
technical * 0.30 +
eyeContact * 0.15
);
return Math.round(overall);
}
/**
* Get score grade and label
*/
export function getScoreGrade(score) {
if (score >= 90) return { grade: 'A+', label: 'Exceptional', color: '#10b981' };
if (score >= 85) return { grade: 'A', label: 'Excellent', color: '#10b981' };
if (score >= 80) return { grade: 'B+', label: 'Very Good', color: '#22c55e' };
if (score >= 75) return { grade: 'B', label: 'Good', color: '#84cc16' };
if (score >= 70) return { grade: 'C+', label: 'Above Average', color: '#eab308' };
if (score >= 65) return { grade: 'C', label: 'Average', color: '#f59e0b' };
if (score >= 60) return { grade: 'D', label: 'Below Average', color: '#f97316' };
return { grade: 'F', label: 'Needs Improvement', color: '#ef4444' };
}
/**
* Generate score breakdown for analytics
*/
export function generateScoreBreakdown(answers = []) {
if (answers.length === 0) {
return {
clarity: 50,
confidence: 50,
technical: 50,
eyeContact: 50,
overall: 50,
details: [],
};
}
// Calculate scores for each answer
const details = answers.map((answer, index) => ({
questionIndex: index + 1,
clarity: calculateClarityScore(answer.text),
confidence: calculateConfidenceScore(answer.text),
technical: calculateTechnicalScore(answer.text),
wordCount: (answer.text || '').split(/\s+/).filter(w => w).length,
responseTime: answer.responseTime || 0,
}));
// Calculate averages
const avgClarity = details.reduce((sum, d) => sum + d.clarity, 0) / details.length;
const avgConfidence = details.reduce((sum, d) => sum + d.confidence, 0) / details.length;
const avgTechnical = details.reduce((sum, d) => sum + d.technical, 0) / details.length;
return {
clarity: Math.round(avgClarity),
confidence: Math.round(avgConfidence),
technical: Math.round(avgTechnical),
eyeContact: 50, // Default, updated by eye tracking
overall: calculateOverallScore({
clarity: avgClarity,
confidence: avgConfidence,
technical: avgTechnical,
eyeContact: 50,
}),
details,
};
}
// ===========================================
// INTERVIEW MODE SYSTEM - 4 MODES ONLY
// ===========================================
/**
* The ONLY 4 interview modes available.
* This is the single source of truth for modes.
*/
export const INTERVIEW_MODES = {
friendly: {
id: 'friendly',
name: 'Friendly Practice',
icon: '😊',
description: 'Relaxed practice with helpful feedback - perfect for beginners',
tier: 'free',
color: '#10b981', // Green
config: {
strictness: 0.3,
technicalWeight: 0.30,
behavioralWeight: 0.40,
communicationWeight: 0.30,
followUpDepth: 'light',
scoringThreshold: 50,
hasVideo: false,
hasVerdict: false,
hasRag: false,
usesLLM: false,
},
},
balanced: {
id: 'balanced',
name: 'Balanced Interview',
icon: 'βš–οΈ',
description: 'Standard interview experience with video recording',
tier: 'paid_entry',
color: '#3b82f6', // Blue
config: {
strictness: 0.6,
technicalWeight: 0.35,
behavioralWeight: 0.35,
communicationWeight: 0.30,
followUpDepth: 'moderate',
scoringThreshold: 65,
hasVideo: true,
hasVerdict: false,
hasRag: false,
usesLLM: true,
},
},
strict: {
id: 'strict',
name: 'Strict Interview',
icon: '🎯',
description: 'Rigorous evaluation with behavior analysis and verdicts',
tier: 'paid_advanced',
color: '#f59e0b', // Orange
config: {
strictness: 0.8,
technicalWeight: 0.40,
behavioralWeight: 0.30,
communicationWeight: 0.30,
followUpDepth: 'deep',
scoringThreshold: 75,
hasVideo: true,
hasVerdict: true,
hasRag: true,
usesLLM: true,
},
},
final_boss: {
id: 'final_boss',
name: 'Final Boss Challenge',
icon: 'πŸ‘Ή',
description: 'Maximum difficulty with industry persona - no mercy',
tier: 'premium',
color: '#ef4444', // Red
config: {
strictness: 1.0,
technicalWeight: 0.45,
behavioralWeight: 0.25,
communicationWeight: 0.30,
followUpDepth: 'deep',
scoringThreshold: 85,
hasVideo: true,
hasVerdict: true,
hasRag: true,
usesLLM: true,
hasPersonalitySelection: true, // ONLY this mode has personality sub-selection
hasRoadmap: true, // Career roadmap generation after interview
},
},
};
/**
* Final Boss mode personality options.
* These are ONLY available when mode = 'final_boss'.
*/
export const FINAL_BOSS_PERSONALITIES = {
faang_engineer: {
id: 'faang_engineer',
name: 'FAANG Engineer',
icon: '🏒',
description: 'Google/Meta-level senior engineer with deep technical focus',
},
startup_founder: {
id: 'startup_founder',
name: 'Startup Founder',
icon: 'πŸš€',
description: 'YC-backed founder who values speed and ownership',
},
senior_staff: {
id: 'senior_staff',
name: 'Senior Staff Engineer',
icon: 'βš™οΈ',
description: 'Principal-level architect with 15+ years experience',
},
hr_director: {
id: 'hr_director',
name: 'HR Director',
icon: 'πŸ‘”',
description: 'Fortune 500 HR leader focused on culture and leadership',
},
hiring_manager: {
id: 'hiring_manager',
name: 'Hiring Manager',
icon: 'πŸ“‹',
description: 'Experienced team lead making final hiring decisions',
},
};
export function getInterviewMode(modeId) {
return INTERVIEW_MODES[modeId] || INTERVIEW_MODES.friendly;
}
export function getInterviewModeList() {
return Object.values(INTERVIEW_MODES);
}
export function getFinalBossPersonality(personalityId) {
return FINAL_BOSS_PERSONALITIES[personalityId] || FINAL_BOSS_PERSONALITIES.faang_engineer;
}
export function getFinalBossPersonalityList() {
return Object.values(FINAL_BOSS_PERSONALITIES);
}
/**
* Check if personality selection is allowed for a mode.
* ONLY final_boss mode allows personality selection.
*/
export function modeAllowsPersonality(modeId) {
return modeId === 'final_boss';
}
// ===========================================
// LEGACY COMPATIBILITY - DEPRECATE SOON
// ===========================================
/** @deprecated Use INTERVIEW_MODES instead */
export const COMPANY_MODES = INTERVIEW_MODES;
/** @deprecated Use getInterviewMode instead */
export function getCompanyMode(modeId) {
return getInterviewMode(modeId);
}
/** @deprecated Use getInterviewModeList instead */
export function getCompanyModeList() {
return getInterviewModeList();
}