update app to allow user to edit resume first
This commit is contained in:
parent
1d6a5f4950
commit
7a072445f4
3 changed files with 86 additions and 51 deletions
|
@ -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 -->
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue