diff --git a/backend/controllers/resumeController.js b/backend/controllers/resumeController.js index e4f8070c..cdb7c9b5 100644 --- a/backend/controllers/resumeController.js +++ b/backend/controllers/resumeController.js @@ -116,6 +116,7 @@ Return the analysis STRICTLY as a JSON object with the following exact keys and "resumeScore": (number between 0 and 100), "roleMatch": (number between 0 and 100), "missingSkills": [array of short strings, max 5], + "missingKeywords": [array of short strings, max 5], "missingProjects": [array of short strings, max 3], "atsCompatibility": { "status": "Good" | "Average" | "Poor", @@ -155,6 +156,24 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw console.error("Failed to parse Gemini JSON:", aiResponse); return res.status(500).json({ message: "AI response parsing failed.", raw: aiResponse }); } + try { + await ResumeAnalysisHistory.create({ + user: req.user._id, + targetRole, + resumeScore: jsonResult.resumeScore || 0, + roleMatch: jsonResult.roleMatch || 0, + missingSkills: jsonResult.missingSkills || [], + missingKeywords: jsonResult.missingKeywords || [], + actionVerbs: jsonResult.actionVerbs || [], + formattingIssues: jsonResult.formattingIssues || [], + missingProjects: jsonResult.missingProjects || [], + atsCompatibility: jsonResult.atsCompatibility || {}, + suggestions: jsonResult.suggestions || [], + sections: jsonResult.sections || {} + }); + } catch (dbErr) { + console.error("Failed to save analysis history:", dbErr); + } res.status(200).json(jsonResult); @@ -165,6 +184,7 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw } const Resume = require("../models/Resume"); +const ResumeAnalysisHistory = require("../models/ResumeAnalysisHistory"); /** * Save or update a user's resume record. @@ -261,4 +281,21 @@ async function deleteResume(req, res) { } } -module.exports = { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume }; +/** + * Retrieve saved resume analysis history for the authenticated user. + * @route GET /api/resume/analysis-history + */ +const getResumeAnalysisHistory = async (req, res) => { + try { + const userId = req.user._id; + const history = await ResumeAnalysisHistory.find({ user: userId }) + .sort({ createdAt: -1 }) + .limit(50); + res.status(200).json({ success: true, history }); + } catch (error) { + console.error("Get Analysis History Error:", error); + res.status(500).json({ success: false, message: "Server Error" }); + } +}; + +module.exports = { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume, getResumeAnalysisHistory }; diff --git a/backend/models/ResumeAnalysisHistory.js b/backend/models/ResumeAnalysisHistory.js new file mode 100644 index 00000000..d7a69bb2 --- /dev/null +++ b/backend/models/ResumeAnalysisHistory.js @@ -0,0 +1,24 @@ +const mongoose = require("mongoose"); + +const ResumeAnalysisHistorySchema = new mongoose.Schema( + { + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true }, + targetRole: { type: String, default: "General" }, + resumeScore: { type: Number, required: true }, + roleMatch: { type: Number, required: true }, + missingSkills: { type: [String], default: [] }, + missingKeywords: { type: [String], default: [] }, + actionVerbs: { type: [String], default: [] }, + formattingIssues: { type: [String], default: [] }, + missingProjects: { type: [String], default: [] }, + atsCompatibility: { + status: { type: String }, + remarks: { type: String } + }, + suggestions: { type: [String], default: [] }, + sections: { type: mongoose.Schema.Types.Mixed } + }, + { timestamps: true } +); + +module.exports = mongoose.model("ResumeAnalysisHistory", ResumeAnalysisHistorySchema); diff --git a/backend/routes/resumeRoutes.js b/backend/routes/resumeRoutes.js index 89c389ea..deadbe5c 100644 --- a/backend/routes/resumeRoutes.js +++ b/backend/routes/resumeRoutes.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume } = require('../controllers/resumeController'); +const { compileResume, analyzeResume, saveResume, getMyResumes, deleteResume, getResumeAnalysisHistory } = require('../controllers/resumeController'); const { protect } = require('../middlewares/authMiddleware'); const { upload, uploadResume, validateResumeMagicBytes } = require('../middlewares/uploadMiddleware'); const { aiLimiter } = require('../middlewares/rateLimiter'); @@ -28,6 +28,11 @@ router.post('/save', validateSaveResume, saveResume); // @access Private router.get('/my-resumes', getMyResumes); +// @route GET /api/resume/analysis-history +// @desc Get all saved resume analyses for logged-in user +// @access Private +router.get('/analysis-history', getResumeAnalysisHistory); + // @route DELETE /api/resume/:id // @desc Delete a saved resume by ID // @access Private diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2c79f261..b8063766 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -27,6 +27,7 @@ import { Navigate, Outlet } from "react-router-dom"; import ResumeTemplates from "./pages/ResumeBuilder/ResumeTemplates"; import ResumeEditor from "./pages/ResumeBuilder/ResumeEditor"; import ResumeAnalyzer from "./pages/ResumeBuilder/ResumeAnalyzer"; +import ResumeAnalysisHistory from "./pages/ResumeBuilder/ResumeAnalysisHistory"; import InterviewExperiences from "./pages/InterviewExperiences/InterviewExperiences"; import TermsandConditions from "./pages/Terms/TermsandConditions"; import ProjectIdeas from "./pages/ProjectIdeas/ProjectIdeas"; @@ -334,6 +335,16 @@ const App = () => { } /> + + + + + + } + /> { + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [expandedId, setExpandedId] = useState(null); + + useEffect(() => { + fetchHistory(); + }, []); + + const fetchHistory = async () => { + try { + setLoading(true); + const response = await axiosInstance.get(API_PATHS.RESUME.GET_ANALYSIS_HISTORY); + setHistory(response.data.history || []); + } catch (err) { + console.error(err); + setError("Failed to load analysis history."); + } finally { + setLoading(false); + } + }; + + const toggleExpand = (id) => { + setExpandedId(expandedId === id ? null : id); + }; + + const renderScore = (score) => { + let colorClass = "text-emerald-500 bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-500/20"; + if (score < 50) { + colorClass = "text-red-500 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-500/20"; + } else if (score < 75) { + colorClass = "text-amber-500 bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-500/20"; + } + return ( +
+ {score}% +
+ ); + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+ + {/* Header */} +
+
+
+ +
+
+

+ Analysis History +

+

+ Track your progress and revisit past resume improvements. +

+
+
+ + New Analysis + +
+ + {error && ( +
+ {error} +
+ )} + + {history.length === 0 && !error ? ( +
+
+ +
+

No Analysis History Yet

+

+ You haven't analyzed any resumes yet. Upload a resume to get instant ATS parsing and AI feedback. +

+ + Get Started + +
+ ) : ( +
+ {history.map((item) => ( +
+ + {/* Summary Row */} + + + {/* Expanded Details */} + {expandedId === item._id && ( +
+ +
+
+ {/* Missing Skills */} +
+

+ Missing Skills +

+ {item.missingSkills?.length > 0 ? ( +
+ {item.missingSkills.map((skill, i) => ( + + {skill} + + ))} +
+ ) : ( +

None detected

+ )} +
+ + {/* Missing Keywords */} + {item.missingKeywords?.length > 0 && ( +
+

+ Missing Keywords +

+
+ {item.missingKeywords.map((keyword, i) => ( + + {keyword} + + ))} +
+
+ )} + + {/* Formatting Issues */} + {item.formattingIssues?.length > 0 && ( +
+

+ Formatting Issues +

+
    + {item.formattingIssues.map((issue, i) => ( +
  • + + {issue} +
  • + ))} +
+
+ )} +
+ +
+ {/* Suggestions */} +
+

+ Suggestions +

+
+ {item.suggestions?.length > 0 ? item.suggestions.map((sug, i) => ( +
+ +

+ {sug} +

+
+ )) : ( +

No suggestions available.

+ )} +
+
+
+
+ +
+ )} +
+ ))} +
+ )} + +
+
+ ); +}; + +export default ResumeAnalysisHistory; diff --git a/frontend/src/pages/ResumeBuilder/ResumeAnalyzer.jsx b/frontend/src/pages/ResumeBuilder/ResumeAnalyzer.jsx index 6a7597b1..1e46cb76 100644 --- a/frontend/src/pages/ResumeBuilder/ResumeAnalyzer.jsx +++ b/frontend/src/pages/ResumeBuilder/ResumeAnalyzer.jsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from "react"; -import { Upload, FileText, Briefcase, Zap, CheckCircle2, AlertTriangle, AlertCircle, X, ChevronRight, RefreshCw, Target } from "lucide-react"; +import { Upload, FileText, Briefcase, Zap, CheckCircle2, AlertTriangle, AlertCircle, X, ChevronRight, RefreshCw, Target, History } from "lucide-react"; import axiosInstance from "../../utils/axiosinstance"; import { API_PATHS } from "../../utils/apiPaths"; +import { Link } from "react-router-dom"; const ResumeAnalyzer = () => { const [file, setFile] = useState(null); @@ -128,18 +129,26 @@ const ResumeAnalyzer = () => {
{/* Header */} -
-
- -
-
-

- AI Resume Analyzer -

-

- Upload your resume for real-time ATS parsing, scoring, and role-matched AI suggestions. -

+
+
+
+ +
+
+

+ AI Resume Analyzer +

+

+ Upload your resume for real-time ATS parsing, scoring, and role-matched AI suggestions. +

+
+ + View History +
{/* Dynamic Display */} diff --git a/frontend/src/utils/apiPaths.js b/frontend/src/utils/apiPaths.js index 23f46dd6..abc3847a 100644 --- a/frontend/src/utils/apiPaths.js +++ b/frontend/src/utils/apiPaths.js @@ -47,6 +47,7 @@ export const API_PATHS = { ANALYZE: "/api/resume/analyze", // AI Resume Analyzer via Gemini SAVE: "/api/resume/save", // Save resume to backend GET_ALL: "/api/resume/my-resumes", // Get all user's saved resumes + GET_ANALYSIS_HISTORY: "/api/resume/analysis-history", // Get resume analysis history }, NOTES_SUMMARY: { SUMMARIZE: "/api/notes-summary/summarize", // AI PDF notes summarizer via Gemini