From 7a072445f404cff6f9b600c77a7a48ca956be9f8 Mon Sep 17 00:00:00 2001 From: Kyle Belanger Date: Tue, 18 Feb 2025 16:22:27 -0500 Subject: [PATCH] update app to allow user to edit resume first --- public/index.html | 15 +++++---- public/js/script.js | 44 ++++++++++++++++--------- routes/generate.js | 78 ++++++++++++++++++++++++++++----------------- 3 files changed, 86 insertions(+), 51 deletions(-) diff --git a/public/index.html b/public/index.html index 9cd6051..258f46a 100644 --- a/public/index.html +++ b/public/index.html @@ -4,20 +4,23 @@ AI Cover Letter Generator - + + + -

Generate Your Cover Letter

+

Generate Your Cover Letter


-

+

-

Extracted Resume Text:

- +

AI-Generated Candidate Profile:

+

Displayed in JSON format. Edit as needed.

+ @@ -28,7 +31,7 @@

Generated Cover Letter:

- +
diff --git a/public/js/script.js b/public/js/script.js index cfacbee..ede7cc9 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -2,14 +2,14 @@ document.getElementById('uploadForm').addEventListener('submit', async function event.preventDefault(); const resumePreviewSection = document.getElementById('resumePreviewSection'); - const resumeTextPreview = document.getElementById("resumeTextOutput") const generateBtn = document.getElementById('generateCoverLetterBtn'); + const profileJson = document.getElementById('profileJson'); - generateBtn.disabled = true; - resumePreviewSection.value = ""; //This clear any previous generated output + generateBtn.disabled = true; resumePreviewSection.style.display = "grid"; + profileJson.textContent = "Parsing Resume....." const fileInput = document.getElementById('resume'); const file = fileInput.files[0]; @@ -28,13 +28,14 @@ document.getElementById('uploadForm').addEventListener('submit', async function body: formData, }); - const data = await response.json(); - if (data.error) { - alert("Error: " + data.error) - } else { - resumeTextPreview.value = data.extractedText; - generateBtn.disabled = false; - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to process the resume.'); + } + + const data = await response.json(); + generateBtn.disabled = false; + profileJson.textContent = JSON.stringify(data.candidateProfile, null, 2); } catch (error) { console.error('Error:', error); alert('Something went wrong. Please try again.'); @@ -45,12 +46,12 @@ document.getElementById('uploadForm').addEventListener('submit', async function // Send Resume and Job Description to Generate Cover Letter document.getElementById("generateCoverLetterBtn").addEventListener("click", async function () { - const extractedResumeText = document.getElementById("resumeTextOutput").value; + const candidateProfile = document.getElementById("profileJson").value; const jobDescription = document.getElementById("jobDescription").value; const keyPoints = document.getElementById("keyPoints").value; const generateBtn = document.getElementById("generateCoverLetterBtn") - if (!extractedResumeText.trim()) { + if (!candidateProfile.trim()) { alert("Please confirm the extracted resume text."); return; } @@ -61,7 +62,7 @@ document.getElementById("generateCoverLetterBtn").addEventListener("click", asyn } const requestData = { - extractedResumeText, + candidateProfile, jobDescription, keyPoints, }; @@ -79,7 +80,8 @@ document.getElementById("generateCoverLetterBtn").addEventListener("click", asyn if (data.error) { alert("Error: " + data.error); } else { - document.getElementById("coverLetterOutput").innerText = data.coverLetter; + const formattedText = formatCoverLetter(data.coverLetter); + document.getElementById("coverLetterOutput").value = formattedText; document.getElementById("coverLetterSection").style.display = "grid"; // Show cover letter section generateBtn.textContent = "Generate New Cover Letter" } @@ -119,4 +121,16 @@ document.getElementById('downloadBtn').addEventListener('click', async function console.error('Error:', error); alert('Failed to download document.'); } -}); \ No newline at end of file +}); + + +function formatCoverLetter(rawText) { + return rawText + .replace(/<\/header>/g, '') + .replace(/<\/greeting>/g, '') + .replace(/<\/introduction>/g, '') + .replace(/<\/body>/g, '') + .replace(/<\/conclusion>/g, '') + .replace(/<\/signature>/g, '') + .replace(/<[^>]+>/g, ''); // Remove all XML-like tags +} \ No newline at end of file diff --git a/routes/generate.js b/routes/generate.js index d8ec650..bb55c5e 100644 --- a/routes/generate.js +++ b/routes/generate.js @@ -20,10 +20,38 @@ const anthropic = new Anthropic({ // Extract resume text and return it for preview router.post('/extract-resume', upload.single('resume'), async (req, res) => { try { - const resumeBuffer = req.file.buffer; - const extractedResumeText = await pdf(resumeBuffer); + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + let extractedText = ''; + const fileBuffer = req.file.buffer; + const fileType = req.file.mimetype; + + if (fileType === 'application/pdf') { + const pdfData = await pdf(fileBuffer); + extractedText = pdfData.text; + } else if ( + fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ) { + const result = await mammoth.extractRawText({ buffer: fileBuffer }); + extractedText = result.value; + } else { + return res.status(400).json({ error: 'Unsupported file type' }); + } + // Load Resume Parser API Prompt Template + const resume_parser_api = require('../data/resume_parser_api.json'); + + // Replace placeholder with extracted resume text + resume_parser_api.messages[0].content[0].text = resume_parser_api.messages[0].content[0].text.replace('{{resume}}', extractedText); + + // Send the extracted resume text to AI model + const resumeResponse = await anthropic.messages.create(resume_parser_api); - res.json({ extractedText: extractedResumeText.text }); + // Extract JSON-formatted profile from the response + const candidateProfile = resumeResponse.content[0].text.split('```json')[1].split('```')[0].trim(); + + res.json({candidateProfile: JSON.parse(candidateProfile) }); + } catch (error) { console.error(error); res.status(500).json({ error: 'Error extracting resume text' }); @@ -33,17 +61,10 @@ router.post('/extract-resume', upload.single('resume'), async (req, res) => { // Handle Resume upload and user input for Job Description router.post('/', async (req, res) => { try { - const { extractedResumeText, jobDescription, keyPoints } = req.body; + const { candidateProfile, jobDescription, keyPoints } = req.body; - const resume_parser_api = require('../data/resume_parser_api.json'); const cover_letter_api = require('../data/cover_letter_api.json'); - // Replace placeholder in API call - resume_parser_api.messages[0].content[0].text = resume_parser_api.messages[0].content[0].text.replace("{{resume}}", extractedResumeText); - - const resumeResponse = await anthropic.messages.create(resume_parser_api); - const candidateProfile = resumeResponse.content[0].text.split('```json')[1].split('```')[0].trim(); - cover_letter_api.messages[0].content[0].text = cover_letter_api.messages[0].content[0].text .replace('{{resume_json}}', candidateProfile) .replace('{{job_description}}', jobDescription) @@ -79,27 +100,24 @@ router.post('/download', async (req, res) => { } }); -function generateCoverLetter(rawText, outputFilename) { - // Extract all sections in one go using an object - const sections = ['header', 'greeting', 'introduction', 'body', 'conclusion', 'signature'] - .reduce((acc, section) => ({ - ...acc, - [section]: rawText.match(new RegExp(`<${section}>(.*?)<\/${section}>`, 's'))[1].trim() - }), {}); +function generateCoverLetter(rawText) { + if (!rawText || typeof rawText !== "string") { + throw new Error("Invalid cover letter text provided."); + } - // Create document with all sections + // Convert text into paragraphs, splitting by double newlines + const paragraphs = rawText.split("\n\n").map(text => + new Paragraph({ + children: [ + new TextRun(text), + new TextRun("\n") // Ensures spacing between paragraphs + ] + }) + ); + + // Create the document const doc = new Document({ - sections: [{ - properties: {}, - children: Object.values(sections).map(text => - new Paragraph({ - children: [ - new TextRun(text), - new TextRun("\n\n") - ] - }) - ) - }] + sections: [{ properties: {}, children: paragraphs }] }); return Packer.toBuffer(doc);