From d4dea3b552f7236014ac26169c1ba8c151236b68 Mon Sep 17 00:00:00 2001 From: Kmadhav824 Date: Fri, 1 May 2026 23:52:53 +0530 Subject: [PATCH 1/5] Added Resume Section with Chars limit for Interview, added browser default Resume parser for client side --- Frontend/src/utils/resumeParser.js | 106 ++++++++++++++++++ backend/src/api/interview/interview-schema.ts | 1 + .../src/api/interview/interview-service.ts | 9 ++ backend/src/api/interview/interview-types.ts | 2 + 4 files changed, 118 insertions(+) create mode 100644 Frontend/src/utils/resumeParser.js diff --git a/Frontend/src/utils/resumeParser.js b/Frontend/src/utils/resumeParser.js new file mode 100644 index 0000000..3f63b93 --- /dev/null +++ b/Frontend/src/utils/resumeParser.js @@ -0,0 +1,106 @@ +/** + * resumeParser.js + * Client-side resume text extraction. + * Supports PDF (via pdf.js) and plain-text (.txt) files. + * All processing is in-browser — nothing is uploaded to a server. + * + * Token-efficiency strategy: + * 1. Extract raw text from each page. + * 2. Collapse excessive whitespace / blank lines. + * 3. Hard-cap at MAX_CHARS characters (≈ 750–1 000 tokens) so + * the context window impact is bounded and predictable. + */ + +const MAX_CHARS = 3500; // ~875 tokens – enough to capture key resume content + +/** + * Collapses runs of blank lines, trims each line, and removes noise. + * @param {string} raw + * @returns {string} + */ +function cleanText(raw) { + return raw + .split('\n') + .map((l) => l.trim()) + .filter((l, i, arr) => { + // Allow at most one consecutive blank line + if (l === '') return arr[i - 1] !== ''; + return true; + }) + .join('\n') + .trim(); +} + +/** + * Extract text from a PDF File object using pdf.js (pdfjs-dist). + * @param {File} file + * @returns {Promise} + */ +async function extractFromPdf(file) { + // Dynamic import keeps pdf.js out of the critical bundle path + const pdfjsLib = await import('pdfjs-dist'); + + // Point the worker to the bundled worker file shipped with pdfjs-dist + pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.mjs', + import.meta.url + ).href; + + const arrayBuffer = await file.arrayBuffer(); + const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer }); + const pdf = await loadingTask.promise; + + const pageTexts = []; + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const content = await page.getTextContent(); + // Join items with a space; preserve newlines from transform data + const pageText = content.items + .map((item) => ('str' in item ? item.str : '')) + .join(' '); + pageTexts.push(pageText); + } + + return pageTexts.join('\n'); +} + +/** + * Extract text from a plain-text File object. + * @param {File} file + * @returns {Promise} + */ +function extractFromTxt(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => resolve(e.target.result || ''); + reader.onerror = () => reject(new Error('Could not read file.')); + reader.readAsText(file); + }); +} + +/** + * Parse a resume file and return cleaned, capped plain text. + * + * @param {File} file – PDF or TXT + * @returns {Promise<{ text: string; truncated: boolean; charCount: number }>} + */ +export async function parseResume(file) { + if (!file) throw new Error('No file provided.'); + + const ext = file.name.split('.').pop()?.toLowerCase(); + let raw = ''; + + if (ext === 'pdf') { + raw = await extractFromPdf(file); + } else if (ext === 'txt') { + raw = await extractFromTxt(file); + } else { + throw new Error('Unsupported file type. Please upload a PDF or TXT file.'); + } + + const cleaned = cleanText(raw); + const truncated = cleaned.length > MAX_CHARS; + const text = truncated ? cleaned.slice(0, MAX_CHARS) : cleaned; + + return { text, truncated, charCount: text.length }; +} diff --git a/backend/src/api/interview/interview-schema.ts b/backend/src/api/interview/interview-schema.ts index 831390c..1df1ec8 100644 --- a/backend/src/api/interview/interview-schema.ts +++ b/backend/src/api/interview/interview-schema.ts @@ -4,6 +4,7 @@ export const generateInterviewQuestionsSchema = yup.object({ role: yup.string().required("Role is required"), experience: yup.string().required("Experience is required"), customRequirements: yup.string().optional(), + resumeContext: yup.string().optional().max(4000, "Resume context too long"), questionCount: yup.number().required("Question count is required"), interviewLevel: yup.string().required("Interview level is required"), round: yup.string().required("Round is required"), diff --git a/backend/src/api/interview/interview-service.ts b/backend/src/api/interview/interview-service.ts index 4fae0f9..1a284ba 100644 --- a/backend/src/api/interview/interview-service.ts +++ b/backend/src/api/interview/interview-service.ts @@ -56,6 +56,7 @@ async function generateQuestions( role, experience, customRequirements, + resumeContext, questionCount, interviewLevel, round, @@ -70,6 +71,12 @@ async function generateQuestions( ? `\nPROGRAMMING LANGUAGE: The candidate selected "${codingLanguage}" for their editor. Phrase each question so a solution is naturally written in that language (syntax may vary; keep the problem language-agnostic but mention they may implement in ${codingLanguage}).` : ''; + // Build the resume section only when provided; cap defensively to avoid runaway tokens. + const MAX_RESUME_CHARS = 3500; + const resumeLine = resumeContext?.trim() + ? `\nCANDIDATE RESUME PROFILE:\n${resumeContext.slice(0, MAX_RESUME_CHARS)}\n\nIMPORTANT: Use the resume profile above to personalise the questions. Reference specific technologies, projects, or experiences mentioned. Prioritise gaps or depth opportunities visible in the resume.` + : ''; + const prompt = ` You are an expert interviewer for software development roles. Generate a set of high-quality interview questions tailored to the candidate's role, experience, round type, and difficulty. @@ -83,6 +90,7 @@ ROUND AND LEVEL: ${roundLine} ${levelLine} ${langLine} +${resumeLine} CUSTOM REQUIREMENTS FROM CANDIDATE: ${customRequirements?.trim() ? customRequirements : "No extra requirements provided."} @@ -92,6 +100,7 @@ INSTRUCTIONS: - Every question must fit the selected round type above; do not blend unrelated round types. - Calibrate depth to the experience level and the interview level (${interviewLevel}). - Prioritize custom requirements when provided. +- When a resume profile is present, tailor at least half the questions to the candidate's actual experience, tech stack, or projects from their resume. - Avoid generic filler; be specific and practical for the role. - Do not repeat questions. Do not include answers. diff --git a/backend/src/api/interview/interview-types.ts b/backend/src/api/interview/interview-types.ts index c262c99..6afad6d 100644 --- a/backend/src/api/interview/interview-types.ts +++ b/backend/src/api/interview/interview-types.ts @@ -2,6 +2,8 @@ export interface createInterviewQuestion { role:string; experience:string; customRequirements?:string; + /** Cleaned plain-text extracted from the candidate's resume (client-side). */ + resumeContext?:string; questionCount:number; interviewLevel:string; round:string; From 87e273fe91c5d626c506ff3d3178132c6c7756bd Mon Sep 17 00:00:00 2001 From: Kmadhav824 Date: Fri, 1 May 2026 23:54:43 +0530 Subject: [PATCH 2/5] Updated Interview components for Resume support --- .../Interview/InterviewReadyStep.jsx | 8 + .../Interview/InterviewSelectionStep.jsx | 25 ++- .../Interview/ResumeUploadField.jsx | 177 ++++++++++++++++++ .../Interview/useInterviewSession.js | 9 + 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 Frontend/src/components/Interview/ResumeUploadField.jsx diff --git a/Frontend/src/components/Interview/InterviewReadyStep.jsx b/Frontend/src/components/Interview/InterviewReadyStep.jsx index 79b07fc..872d3d1 100644 --- a/Frontend/src/components/Interview/InterviewReadyStep.jsx +++ b/Frontend/src/components/Interview/InterviewReadyStep.jsx @@ -9,11 +9,13 @@ export function InterviewReadyStep({ customRequirements, codingLanguageName, isCodeRound, + resumeText, isLoading, startError, onGoBack, onStartInterview, }) { + const hasResume = Boolean(resumeText && resumeText.trim()); return (
@@ -46,6 +48,12 @@ export function InterviewReadyStep({ Focus: {customRequirements}

)} + {hasResume && ( +
+ + Resume loaded — questions will be tailored to your profile +
+ )}