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 name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body>
<h1>Generate Your Cover Letter</h1>
<h1 class="text-2xl text-center">Generate Your Cover Letter</h1>
<br>
<form id="uploadForm" enctype="multipart/form-data">
<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>
</form>
<div id="resumePreviewSection">
<h3>Extracted Resume Text:</h3>
<textarea id="resumeTextOutput" rows="10"></textarea>
<h3>AI-Generated Candidate Profile:</h3>
<p>Displayed in JSON format. Edit as needed.</p>
<textarea id="profileJson" rows="10"></textarea>
<label for="jobDescription">Paste Job Description:</label>
<textarea id="jobDescription" rows="5" required></textarea>
<label for="keyPoints">Key Points for Letter (optional):</label>
@ -28,7 +31,7 @@
<!-- Hidden To Start With -->
<div id="coverLetterSection">
<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>
</div>
<!-- End Hidden Section -->

View file

@ -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
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"
}
@ -120,3 +122,15 @@ document.getElementById('downloadBtn').addEventListener('click', async function
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
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);
// 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) {
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);