diff --git a/backend/src/routes/resume.js b/backend/src/routes/resume.js index 79f80b375..1dd60d44f 100644 --- a/backend/src/routes/resume.js +++ b/backend/src/routes/resume.js @@ -17,6 +17,8 @@ import { import { scrapeLinkedInProfile, profileToResumeText } from '../services/linkedinImporter.js'; import { fetchGitHubProfile, convertGitHubToResumeText } from '../services/githubImporter.js'; import { getDefaultProvider } from '../config/aiProviders.js'; +import { scoreResumeText } from '../services/resumeService.js'; +import { extractAIProvider } from '../middleware/aiKey.js'; const router = express.Router(); @@ -575,49 +577,26 @@ ${text}`; } })); -router.post('/score', asyncHandler(async (req, res) => { - const { resumeText } = req.body; +router.post('/score', extractAIProvider, asyncHandler(async (req, res) => { + const { resumeText, jobRole } = req.body; - if (!resumeText || !resumeText.trim()) { - return res.status(400).json({ - success: false, - message: 'Resume text is required' - }); + if (typeof resumeText !== 'string' || !resumeText.trim()) { + throw new ApiError(400, 'Resume text is required and must be a string'); } - res.json({ - success: true, - data: { - overallScore: 82, - sections: { - summary: { - score: 80, - feedback: "Good professional summary" - }, - skills: { - score: 85, - feedback: "Skills are relevant" - }, - experience: { - score: 78, - feedback: "Add more quantified achievements" - }, - education: { - score: 88, - feedback: "Education section is clear" - }, - projects: { - score: 79, - feedback: "Projects need more impact metrics" - } - }, - topSuggestions: [ - "Add measurable achievements", - "Improve project descriptions", - "Use stronger action verbs" - ] - } - }); + try { + const provider = req.aiProvider; + const scoreData = await scoreResumeText(resumeText, jobRole || 'Software Engineer', provider); + + res.json({ + success: true, + data: scoreData + }); + } catch (error) { + console.error('Resume scoring error:', error); + if (error instanceof ApiError) throw error; + throw new ApiError(500, 'Failed to score resume with AI.'); + } })); diff --git a/backend/src/services/resumeService.js b/backend/src/services/resumeService.js new file mode 100644 index 000000000..b2940c9fc --- /dev/null +++ b/backend/src/services/resumeService.js @@ -0,0 +1,75 @@ +import { computeATSScore } from './atsScorer.js'; +import { ApiError } from '../middleware/errorHandler.js'; + +export const scoreResumeText = async (resumeText, targetRole = 'Software Engineer', provider) => { + // 1. Get deterministic scores + const deterministicScoring = computeATSScore(resumeText, targetRole); + + // 2. Get qualitative feedback via AI + const prompt = `Analyze this resume for a ${targetRole} position and return a JSON object with EXACTLY these fields: +- sections: object with keys "summary", "skills", "experience", "education", "projects" — each containing: + - feedback (string, one concise sentence of constructive feedback) +- topSuggestions: array of exactly 3 strings, each a specific actionable improvement tip + +Resume: +${resumeText} + +Return ONLY valid JSON. No markdown fences, no extra text.`; + + const result = await provider.generateContent(prompt); + let text = result.text.trim(); + + // Strip markdown fences + if (text.startsWith('```')) { + text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim(); + } + + // Attempt extra extraction + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) text = jsonMatch[0]; + + let qualitativeData; + try { + qualitativeData = JSON.parse(text); + } catch (parseErr) { + console.error('Resume score JSON parse error:', parseErr.message); + throw new ApiError( + 502, + 'AI service returned an invalid response. Please try again in a moment.' + ); + } + + // 3. Map into the format expected by the frontend + const scoreData = { + overallScore: deterministicScoring.overallScore, + sections: { + summary: { + score: deterministicScoring.breakdown.formatting, + feedback: qualitativeData.sections?.summary?.feedback || 'Good formatting.' + }, + skills: { + score: deterministicScoring.breakdown.skills, + feedback: qualitativeData.sections?.skills?.feedback || 'Include more role-specific skills.' + }, + experience: { + score: deterministicScoring.breakdown.experience, + feedback: qualitativeData.sections?.experience?.feedback || 'Add metrics.' + }, + education: { + score: 80, // Default good score for education + feedback: qualitativeData.sections?.education?.feedback || 'Good.' + }, + projects: { + score: deterministicScoring.breakdown.keywordMatch, + feedback: qualitativeData.sections?.projects?.feedback || 'Good.' + } + }, + topSuggestions: qualitativeData.topSuggestions || [ + 'Add more quantifiable metrics to your experience.', + 'Tailor keywords to the specific job role.', + 'Ensure formatting is clean and easy to read.' + ] + }; + + return scoreData; +}; diff --git a/frontend/src/components/ResumeSectionStrengthAnalyzer.jsx b/frontend/src/components/ResumeSectionStrengthAnalyzer.jsx index a936f373e..dcbdef59e 100644 --- a/frontend/src/components/ResumeSectionStrengthAnalyzer.jsx +++ b/frontend/src/components/ResumeSectionStrengthAnalyzer.jsx @@ -1,28 +1,110 @@ -import { FileText, TrendingUp } from "lucide-react"; +import { useState, useEffect } from "react"; +import { FileText, TrendingUp, AlertCircle, Loader2 } from "lucide-react"; +import { resumeApi } from "../services/api"; -export default function ResumeSectionStrengthAnalyzer() { - const sections = [ - { - name: "Education", - score: 90, - suggestion: "Strong section with relevant academic details.", - }, - { - name: "Skills", - score: 75, - suggestion: "Add more industry-relevant technical skills.", - }, - { - name: "Projects", - score: 85, - suggestion: "Include measurable outcomes and achievements.", - }, - { - name: "Experience", - score: 60, - suggestion: "Add internship or volunteer experience.", - }, - ]; +export default function ResumeSectionStrengthAnalyzer({ resume }) { + const [sections, setSections] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [overallScore, setOverallScore] = useState(null); + + useEffect(() => { + if (!resume) return; + + const abortController = new AbortController(); + + const fetchScore = async () => { + setLoading(true); + setError(""); + try { + const text = resume.enhancedText || resume.originalText; + // In a real app we might pass abortController.signal to the fetch call inside resumeApi + const res = await resumeApi.score(text, resume.jobRole || 'Software Engineer'); + + if (abortController.signal.aborted) return; + + if (res?.data?.sections) { + const formattedSections = Object.entries(res.data.sections).map(([key, value]) => ({ + name: key.charAt(0).toUpperCase() + key.slice(1), + score: value.score, + suggestion: value.feedback + })); + setSections(formattedSections); + setOverallScore(res.data.overallScore); + } else { + setError("No section data found in the response."); + } + } catch (err) { + if (abortController.signal.aborted) return; + console.error("Error scoring resume:", err); + setError("Failed to analyze resume strength."); + } finally { + if (!abortController.signal.aborted) { + setLoading(false); + } + } + }; + + fetchScore(); + + return () => { + abortController.abort(); + }; + }, [resume]); + + if (!resume) { + return ( +
Upload a Resume
++ Upload your resume to get an AI-powered section strength analysis. +
+Analyzing resume sections with AI...
+{error}
+