update app to allow user to edit resume first

This commit is contained in:
Kyle Belanger 2025-02-18 16:22:27 -05:00
parent 1d6a5f4950
commit 7a072445f4
3 changed files with 86 additions and 51 deletions

View file

@ -4,20 +4,23 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Cover Letter Generator</title> <title>AI Cover Letter Generator</title>
<link rel="stylesheet" href="css/styles.css"> <!-- <link rel="stylesheet" href="css/styles.css"> -->
<!-- Used for Testing purposes only -->
<link rel="stylesheet" href="css/base_styles.css">
</head> </head>
<body> <body>
<h1>Generate Your Cover Letter</h1> <h1 class="text-2xl text-center">Generate Your Cover Letter</h1>
<br> <br>
<form id="uploadForm" enctype="multipart/form-data"> <form id="uploadForm" enctype="multipart/form-data">
<label for="resume">Upload Resume (pdf or docx):</label> <label for="resume">Upload Resume (pdf or docx):</label>
<input type="file" id="resume" name="resume" accept=".txt,.pdf,.docx" required><br><br> <input type="file" id="resume" name="resume" accept=".pdf,.docx,.doc" required><br><br>
<button type="submit" id="generateBtn">Read Resume</button> <button type="submit" id="generateBtn">Read Resume</button>
</form> </form>
<div id="resumePreviewSection"> <div id="resumePreviewSection">
<h3>Extracted Resume Text:</h3> <h3>AI-Generated Candidate Profile:</h3>
<textarea id="resumeTextOutput" rows="10"></textarea> <p>Displayed in JSON format. Edit as needed.</p>
<textarea id="profileJson" rows="10"></textarea>
<label for="jobDescription">Paste Job Description:</label> <label for="jobDescription">Paste Job Description:</label>
<textarea id="jobDescription" rows="5" required></textarea> <textarea id="jobDescription" rows="5" required></textarea>
<label for="keyPoints">Key Points for Letter (optional):</label> <label for="keyPoints">Key Points for Letter (optional):</label>
@ -28,7 +31,7 @@
<!-- Hidden To Start With --> <!-- Hidden To Start With -->
<div id="coverLetterSection"> <div id="coverLetterSection">
<h3>Generated Cover Letter:</h3> <h3>Generated Cover Letter:</h3>
<textarea id="coverLetterOutput" rows="10"></textarea> <textarea id="coverLetterOutput" rows="15" cols="80"></textarea>
<button id="downloadBtn">Download as DOCX</button> <button id="downloadBtn">Download as DOCX</button>
</div> </div>
<!-- End Hidden Section --> <!-- End Hidden Section -->

View file

@ -2,14 +2,14 @@ document.getElementById('uploadForm').addEventListener('submit', async function
event.preventDefault(); event.preventDefault();
const resumePreviewSection = document.getElementById('resumePreviewSection'); const resumePreviewSection = document.getElementById('resumePreviewSection');
const resumeTextPreview = document.getElementById("resumeTextOutput")
const generateBtn = document.getElementById('generateCoverLetterBtn'); const generateBtn = document.getElementById('generateCoverLetterBtn');
const profileJson = document.getElementById('profileJson');
generateBtn.disabled = true; generateBtn.disabled = true;
resumePreviewSection.value = ""; //This clear any previous generated output
resumePreviewSection.style.display = "grid"; resumePreviewSection.style.display = "grid";
profileJson.textContent = "Parsing Resume....."
const fileInput = document.getElementById('resume'); const fileInput = document.getElementById('resume');
const file = fileInput.files[0]; const file = fileInput.files[0];
@ -28,13 +28,14 @@ document.getElementById('uploadForm').addEventListener('submit', async function
body: formData, body: formData,
}); });
const data = await response.json(); if (!response.ok) {
if (data.error) { const errorData = await response.json();
alert("Error: " + data.error) throw new Error(errorData.error || 'Failed to process the resume.');
} else {
resumeTextPreview.value = data.extractedText;
generateBtn.disabled = false;
} }
const data = await response.json();
generateBtn.disabled = false;
profileJson.textContent = JSON.stringify(data.candidateProfile, null, 2);
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Something went wrong. Please try again.'); 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 // Send Resume and Job Description to Generate Cover Letter
document.getElementById("generateCoverLetterBtn").addEventListener("click", async function () { 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 jobDescription = document.getElementById("jobDescription").value;
const keyPoints = document.getElementById("keyPoints").value; const keyPoints = document.getElementById("keyPoints").value;
const generateBtn = document.getElementById("generateCoverLetterBtn") const generateBtn = document.getElementById("generateCoverLetterBtn")
if (!extractedResumeText.trim()) { if (!candidateProfile.trim()) {
alert("Please confirm the extracted resume text."); alert("Please confirm the extracted resume text.");
return; return;
} }
@ -61,7 +62,7 @@ document.getElementById("generateCoverLetterBtn").addEventListener("click", asyn
} }
const requestData = { const requestData = {
extractedResumeText, candidateProfile,
jobDescription, jobDescription,
keyPoints, keyPoints,
}; };
@ -79,7 +80,8 @@ document.getElementById("generateCoverLetterBtn").addEventListener("click", asyn
if (data.error) { if (data.error) {
alert("Error: " + data.error); alert("Error: " + data.error);
} else { } 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 document.getElementById("coverLetterSection").style.display = "grid"; // Show cover letter section
generateBtn.textContent = "Generate New Cover Letter" generateBtn.textContent = "Generate New Cover Letter"
} }
@ -120,3 +122,15 @@ document.getElementById('downloadBtn').addEventListener('click', async function
alert('Failed to download document.'); alert('Failed to download document.');
} }
}); });
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
}

View file

@ -20,10 +20,38 @@ const anthropic = new Anthropic({
// Extract resume text and return it for preview // Extract resume text and return it for preview
router.post('/extract-resume', upload.single('resume'), async (req, res) => { router.post('/extract-resume', upload.single('resume'), async (req, res) => {
try { try {
const resumeBuffer = req.file.buffer; if (!req.file) {
const extractedResumeText = await pdf(resumeBuffer); 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);
// 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) });
res.json({ extractedText: extractedResumeText.text });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ error: 'Error extracting resume text' }); 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 // Handle Resume upload and user input for Job Description
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { 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'); 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 cover_letter_api.messages[0].content[0].text = cover_letter_api.messages[0].content[0].text
.replace('{{resume_json}}', candidateProfile) .replace('{{resume_json}}', candidateProfile)
.replace('{{job_description}}', jobDescription) .replace('{{job_description}}', jobDescription)
@ -79,27 +100,24 @@ router.post('/download', async (req, res) => {
} }
}); });
function generateCoverLetter(rawText, outputFilename) { function generateCoverLetter(rawText) {
// Extract all sections in one go using an object if (!rawText || typeof rawText !== "string") {
const sections = ['header', 'greeting', 'introduction', 'body', 'conclusion', 'signature'] throw new Error("Invalid cover letter text provided.");
.reduce((acc, section) => ({ }
...acc,
[section]: rawText.match(new RegExp(`<${section}>(.*?)<\/${section}>`, 's'))[1].trim()
}), {});
// Create document with all sections // Convert text into paragraphs, splitting by double newlines
const doc = new Document({ const paragraphs = rawText.split("\n\n").map(text =>
sections: [{
properties: {},
children: Object.values(sections).map(text =>
new Paragraph({ new Paragraph({
children: [ children: [
new TextRun(text), new TextRun(text),
new TextRun("\n\n") new TextRun("\n") // Ensures spacing between paragraphs
] ]
}) })
) );
}]
// Create the document
const doc = new Document({
sections: [{ properties: {}, children: paragraphs }]
}); });
return Packer.toBuffer(doc); return Packer.toBuffer(doc);