Spaces:
Sleeping
Sleeping
Download frontend/src/services/scoringService.js from chowdary1/ai-interviewer-version-2: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/services/scoringService.js
- Command line
-
hf download hf://spaces/chowdary1/ai-interviewer-version-2/frontend/src/services/scoringService.js
-
curl -L -o scoringService.js https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/services/scoringService.js
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(); | |
| } | |