Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 16 additions & 38 deletions backend/src/routes/resume.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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';

const router = express.Router();

Expand Down Expand Up @@ -576,48 +577,25 @@ ${text}`;
}));

router.post('/score', asyncHandler(async (req, res) => {
const { resumeText } = req.body;
const { resumeText, jobRole } = req.body;

if (!resumeText || !resumeText.trim()) {
return res.status(400).json({
success: false,
message: 'Resume text is required'
});
throw new ApiError(400, 'Resume text is required');
}
Comment thread
Gurkaran18 marked this conversation as resolved.
Outdated

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 = getDefaultProvider();
Comment thread
Gurkaran18 marked this conversation as resolved.
Outdated
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.');
}
}));
Comment thread
Gurkaran18 marked this conversation as resolved.
Outdated


Expand Down
75 changes: 75 additions & 0 deletions backend/src/services/resumeService.js
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
Gurkaran18 marked this conversation as resolved.
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, 'Raw text:', text);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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;
};
121 changes: 97 additions & 24 deletions frontend/src/components/ResumeSectionStrengthAnalyzer.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,96 @@
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 fetchScore = async () => {
setLoading(true);
setError("");
try {
const text = resume.enhancedText || resume.originalText;
const res = await resumeApi.score(text, resume.jobRole || 'Software Engineer');

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);
}
} catch (err) {
console.error("Error scoring resume:", err);
setError("Failed to analyze resume strength.");
} finally {
setLoading(false);
}
};

fetchScore();
Comment thread
Gurkaran18 marked this conversation as resolved.
}, [resume]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!resume) {
return (
<div className="rounded-2xl bg-card border border-border p-6 shadow-sm">
<div className="flex items-center gap-3 mb-6">
<TrendingUp className="w-6 h-6 text-primary" />
<h2 className="text-xl font-black">
Resume Section Strength Analyzer
</h2>
</div>
<div className="p-6 rounded-xl border border-dashed border-primary/20 bg-primary/5 text-center">
<FileText className="w-10 h-10 text-primary mx-auto mb-3 opacity-60" />
<p className="text-sm font-bold text-foreground mb-1">Upload a Resume</p>
<p className="text-xs text-muted-foreground max-w-xs mx-auto mb-4">
Upload your resume to get an AI-powered section strength analysis.
</p>
</div>
</div>
);
}

if (loading) {
return (
<div className="rounded-2xl bg-card border border-border p-6 shadow-sm">
<div className="flex items-center gap-3 mb-6">
<TrendingUp className="w-6 h-6 text-primary" />
<h2 className="text-xl font-black">
Resume Section Strength Analyzer
</h2>
</div>
<div className="flex flex-col items-center justify-center p-8 text-center gap-4 text-muted-foreground">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<p className="text-sm font-bold">Analyzing resume sections with AI...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="rounded-2xl bg-card border border-border p-6 shadow-sm">
<div className="flex items-center gap-3 mb-6">
<TrendingUp className="w-6 h-6 text-primary" />
<h2 className="text-xl font-black">
Resume Section Strength Analyzer
</h2>
</div>
<div className="flex items-center gap-2 p-4 rounded-xl bg-destructive/10 text-destructive text-sm font-bold">
<AlertCircle className="w-5 h-5" />
<p>{error}</p>
</div>
</div>
);
}

return (
<div className="rounded-2xl bg-card border border-border p-6 shadow-sm">
Expand All @@ -31,6 +99,11 @@ export default function ResumeSectionStrengthAnalyzer() {
<h2 className="text-xl font-black">
Resume Section Strength Analyzer
</h2>
{overallScore !== null && (
<span className="ml-auto px-3 py-1 bg-primary/10 text-primary rounded-full text-sm font-bold border border-primary/20">
Overall Score: {overallScore}%
</span>
)}
</div>

<div className="space-y-4">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ export default function Dashboard() {
</motion.div>

<motion.div variants={itemVariants} className="mb-10">
<ResumeSectionStrengthAnalyzer />
<ResumeSectionStrengthAnalyzer resume={resumes[0]} />
</motion.div>

<motion.div variants={itemVariants} className="mb-10">
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,17 @@ export const resumeApi = {
body: JSON.stringify(data)
})
return handleResponse(response)
},

// Score resume with AI
async score(resumeText, jobRole = 'Software Engineer') {
const headers = await getAuthHeaders()
const response = await fetch(`${API_BASE}/resumes/score`, {
method: 'POST',
headers,
body: JSON.stringify({ resumeText, jobRole })
})
return handleResponse(response)
}
}

Expand Down
Loading