Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 56 additions & 6 deletions nextstep-backend/src/controllers/resume_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import fs from 'fs';
import path from 'path';
import { scoreResume, streamScoreResume, getResumeTemplates,
generateImprovedResume, parseResumeFields,
saveParsedResume, getResumeByOwner } from '../services/resume_service';
saveParsedResume, getResumeByOwner, updateResume } from '../services/resume_service';
import multer from 'multer';
import {getResumeBuffer, resumeExists, uploadResume} from '../services/resources_service';
import {getResumeBuffer, resumeExists} from '../services/resources_service';
import { CustomRequest } from "types/customRequest";
import { handleError } from "../utils/handle_error";

// Simple in-memory cache: { [key: string]: { scoreAndFeedback, timestamp } }
const resumeScoreCache: Record<string, { data: any, timestamp: number }> = {};
const CACHE_TTL_MS = 24* 60 * 60 * 1000; // 24 hour

const getCacheKey = (filename: string, jobDescription?: string) =>
`${filename}::${jobDescription || ''}`;

const getResumeScore = async (req: Request, res: Response) => {
try {
const { filename } = req.params;
Expand All @@ -20,7 +27,14 @@ const getResumeScore = async (req: Request, res: Response) => {
return res.status(404).send('Resume not found');
}

const cacheKey = getCacheKey(filename, jobDescription);
const cached = resumeScoreCache[cacheKey];
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
return res.status(200).send(cached.data);
}

const scoreAndFeedback = await scoreResume(resumePath, jobDescription);
resumeScoreCache[cacheKey] = { data: scoreAndFeedback, timestamp: Date.now() };
return res.status(200).send(scoreAndFeedback);
} catch (error) {
if (error instanceof TypeError) {
Expand All @@ -31,7 +45,7 @@ const getResumeScore = async (req: Request, res: Response) => {
}
};

const getStreamResumeScore = async (req: Request, res: Response) => {
const getStreamResumeScore = async (req: CustomRequest, res: Response) => {
try {
const { filename } = req.params;
const resumePath = path.resolve(config.resources.resumesDirectoryPath(), filename);
Expand All @@ -41,6 +55,18 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
return res.status(404).send('Resume not found');
}

const cacheKey = getCacheKey(filename, jobDescription);
const cached = resumeScoreCache[cacheKey];
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
// Stream cached result as SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write(`data: ${JSON.stringify({ ...cached.data, done: true })}\n\n`);
res.end();
return;
}

// Set headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
Expand All @@ -52,17 +78,24 @@ const getStreamResumeScore = async (req: Request, res: Response) => {
});

// Stream the response
const score = await streamScoreResume(
let fullChunk = '';
const [score, fullText] = await streamScoreResume(
resumePath,
jobDescription,
(chunk) => {
fullChunk += chunk;
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
}
);

// Send the final score
res.write(`data: ${JSON.stringify({ score, done: true })}\n\n`);
res.end();

// Optionally cache the result (score and fullText)
resumeScoreCache[cacheKey] = { data: { score, fullText }, timestamp: Date.now() };
await updateResume(req.user.id, jobDescription, fullText, score);

} catch (error) {
if (error instanceof TypeError) {
return res.status(400).send(error.message);
Expand Down Expand Up @@ -108,7 +141,7 @@ const parseResume = async (req: CustomRequest, res: Response) => {

const resumeFilename = req.body.resumefileName;
const parsed = await parseResumeFields(getResumeBuffer(req.body.resumefileName), resumeFilename);
const resumeData = await saveParsedResume(parsed, req.user.id, resumeFilename);
const resumeData = await saveParsedResume(parsed, req.user.id, resumeFilename, req.body.originfilename);

return res.status(200).json(parsed);
} catch (err: any) {
Expand All @@ -117,6 +150,23 @@ const parseResume = async (req: CustomRequest, res: Response) => {
}
};

const getResume = async (req: CustomRequest, res: Response) => {
try {
const ownerId = req.user.id;
const resume = await getResumeByOwner(ownerId);

if (!resume) {
return res.status(404).json({ error: 'Resume not found' });
}

return res.status(200).json(resume);
} catch (error) {
console.error('Error retrieving resume:', error);
return handleError(error, res);
}
}


const getResumeData = async (req: CustomRequest, res: Response) => {
try {
const ownerId = req.user.id;
Expand All @@ -133,4 +183,4 @@ const getResumeData = async (req: CustomRequest, res: Response) => {

export default { parseResume, getResumeScore,
getStreamResumeScore, getTemplates,
generateResume, getResumeData };
generateResume, getResumeData, getResume };
7 changes: 5 additions & 2 deletions nextstep-backend/src/models/resume_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ const ResumeSchema = new Schema({
rawContentLink: { type: String, required: true },
parsedData: {
type: {
fileName: { type: String, required: false },
aboutMe: { type: String, required: false },
skills: { type: [String], required: false },
roleMatch: { type: String, required: false },
experience: { type: [String], required: false }

experience: { type: [String], required: false },
jobDescription: { type: String, required: false },
feedback: { type: String, required: false },
score: { type: Number, required: false },
},
required: false
},
Expand Down
4 changes: 3 additions & 1 deletion nextstep-backend/src/routes/resume_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const router = express.Router();

router.get('/score/:filename', Resume.getResumeScore);

router.get('/streamScore/:filename', Resume.getStreamResumeScore);
router.get('/streamScore/:filename', (req: Request, res: Response) => Resume.getStreamResumeScore(req as CustomRequest, res));

router.get('/templates', Resume.getTemplates);

Expand All @@ -21,5 +21,7 @@ router.post('/parseResume', upload.single('resume'), (req: Request, res: Respon
// TODO - Use it in the frontend after the parse and upload resume
router.get('/resumeData/:version', (req: Request, res: Response) => Resume.getResumeData(req as CustomRequest, res))

router.get('/', (req: Request, res: Response) => Resume.getResume(req as CustomRequest, res))


export default router;
34 changes: 30 additions & 4 deletions nextstep-backend/src/services/resume_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const streamScoreResume = async (
resumePath: string,
jobDescription: string | undefined,
onChunk: (chunk: string) => void
): Promise<number> => {
): Promise<[number, string]> => {
try {
const resumeText = await parseDocument(resumePath);
if (resumeText.trim() == '') {
Expand Down Expand Up @@ -88,7 +88,7 @@ const streamScoreResume = async (
onChunk(FEEDBACK_ERROR_MESSAGE);
}

return finalScore;
return [finalScore, fullResponse];
} catch (error: any) {
if (error instanceof TypeError) {
console.error('TypeError while streaming resume score:', error);
Expand Down Expand Up @@ -343,7 +343,7 @@ const getLatestResumeByUser = async (ownerId: string): Promise<number> => {
};


const saveParsedResume = async (parsedData: ParsedResume, ownerId: string, resumeRawLink: string): Promise<ResumeData> => {
const saveParsedResume = async (parsedData: ParsedResume, ownerId: string, resumeRawLink: string, filename: string): Promise<ResumeData> => {
const lastVersion = await getLatestResumeByUser(ownerId);
const newVersion = lastVersion + 1;

Expand All @@ -352,6 +352,7 @@ const saveParsedResume = async (parsedData: ParsedResume, ownerId: string, resum
version: newVersion,
rawContentLink: resumeRawLink,
parsedData: {
fileName: filename,
aboutMe: parsedData.aboutMe,
skills: parsedData.skills,
roleMatch: parsedData.roleMatch,
Expand All @@ -363,6 +364,31 @@ const saveParsedResume = async (parsedData: ParsedResume, ownerId: string, resum
return resumeToResumeData(savedResume);
};

const updateResume = async (ownerId: string, jobDescription: string, feedback?: string, score?: number, filename?: string): Promise<void> => {
try {
const resume = await getResumeByOwner(ownerId);
if (!resume) {
throw new Error(`Resume not found`);
}
const parsedData = resume.parsedData as ParsedResume; // Ensure parsedData is of type ParsedResume
if (jobDescription !== parsedData.jobDescription) {
resume.parsedData = {
...parsedData,
jobDescription: jobDescription || parsedData.jobDescription,
feedback: feedback || parsedData.feedback || '',
score: score || parsedData.score || 0,
fileName: parsedData.fileName || filename || ''
};
resume.markModified('parsedData');
await resume.save();

}
}
catch (error) {
console.error('Error updating resume:', error);
throw new Error(`Failed to update resume`);
}
}

const getResumeByOwner = async (ownerId: string, version?: number) => {
try {
Expand Down Expand Up @@ -394,4 +420,4 @@ const getResumeByOwner = async (ownerId: string, version?: number) => {

export { scoreResume, streamScoreResume,
getResumeTemplates, generateImprovedResume, parseResumeFields,
saveParsedResume, getResumeByOwner };
saveParsedResume, getResumeByOwner, updateResume, };
4 changes: 4 additions & 0 deletions nextstep-backend/src/types/resume_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export interface ParsedResume {
roleMatch: string;
experience: string[];
education?: string[];
jobDescription?: string;
feedback?: string;
score?: number;
fileName?: string;
}

export interface ResumeDocument extends Document {
Expand Down
21 changes: 18 additions & 3 deletions nextstep-frontend/src/pages/MainDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const MainDashboard: React.FC = () => {
const [useOAuth, setUseOAuth] = useState(true);
const [showAuthOptions, setShowAuthOptions] = useState(false);

// AI-resume state
// resume state
const [parsing, setParsing] = useState(false);
const [resumeExperience, setResumeExperience] = useState<string[]>([]);
const [roleMatch, setRoleMatch] = useState<string>('');
Expand Down Expand Up @@ -101,6 +101,21 @@ const MainDashboard: React.FC = () => {
}
}, []);

// Fetch resume data on mount
useEffect(() => {
const fetchResumeData = async () => {
try {
const response = await api.get('/resume');
setResumeFileName(response.data.parsedData.fileName || '');
setResumeExperience(response.data.parsedData.experience || []);
setRoleMatch(response.data.parsedData.roleMatch || '');
} catch (err) {
console.error('Failed to fetch resume data:', err);
}
};
fetchResumeData();
}, []);

const mergeRepoLanguages = async (fetchedRepos: typeof repos) => {
const langSet = new Set(skills);
for (const repo of fetchedRepos) {
Expand Down Expand Up @@ -158,11 +173,11 @@ const MainDashboard: React.FC = () => {
const form = new FormData();
form.append('file', file);
try {
const uplaodedResume = await uploadResume(form);
const uploadedResume = await uploadResume(form);

const res = await api.post('/resume/parseResume',
{
resumefileName: uplaodedResume,
resumefileName: uploadedResume, originfilename: file.name,
}, {
headers: { 'Content-Type': 'multipart/form-data' },
});
Expand Down
Loading
Loading