Compare commits

...

4 commits

Author SHA1 Message Date
Kyle Belanger
7a072445f4 update app to allow user to edit resume first 2025-02-18 16:22:27 -05:00
Kyle Belanger
1d6a5f4950 update styles.css 2025-02-18 16:22:10 -05:00
Kyle Belanger
651cb015b3 update base styles 2025-02-18 16:21:50 -05:00
Kyle Belanger
3e6fe42135 update packages 2025-02-18 12:39:13 -05:00
7 changed files with 241 additions and 56 deletions

131
package-lock.json generated
View file

@ -14,6 +14,7 @@
"docx": "^9.1.1",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"mammoth": "^1.9.0",
"multer": "^1.4.5-lts.1",
"pdf-parse": "^1.1.1",
"tailwindcss": "^4.0.6"
@ -579,6 +580,15 @@
"form-data": "^4.0.0"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -648,6 +658,15 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -660,6 +679,32 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -995,6 +1040,12 @@
"node": ">=0.10"
}
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/docx": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.1.1.tgz",
@ -1039,6 +1090,15 @@
"url": "https://dotenvx.com"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -1816,6 +1876,41 @@
"dev": true,
"license": "MIT"
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/mammoth": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.9.0.tgz",
"integrity": "sha512-F+0NxzankQV9XSUAuVKvkdQK0GbtGGuqVnND9aVf9VSeUA82LQa29GjLqYU6Eez8LHqSJG3eGiDW3224OKdpZg==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -2069,6 +2164,12 @@
"node": ">= 0.8"
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@ -2084,6 +2185,15 @@
"node": ">= 0.8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@ -2392,6 +2502,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -2540,6 +2656,12 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@ -2640,6 +2762,15 @@
"xml-js": "bin/cli.js"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View file

@ -5,6 +5,7 @@
"docx": "^9.1.1",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"mammoth": "^1.9.0",
"multer": "^1.4.5-lts.1",
"pdf-parse": "^1.1.1",
"tailwindcss": "^4.0.6"

View file

@ -10,3 +10,9 @@
#coverLetterSection {
display: none;
}
@layer base {
textarea {
resize: auto;
}
}

View file

@ -521,8 +521,15 @@
.static {
position: static;
}
.block {
display: block;
.grid {
display: grid;
}
.text-center {
text-align: center;
}
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.outline {
outline-style: var(--tw-outline-style);
@ -535,6 +542,11 @@
#coverLetterSection {
display: none;
}
@layer base {
textarea {
resize: auto;
}
}
@keyframes spin {
to {
transform: rotate(360deg);

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
const doc = new Document({
sections: [{
properties: {},
children: Object.values(sections).map(text =>
// 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\n")
new TextRun("\n") // Ensures spacing between paragraphs
]
})
)
}]
);
// Create the document
const doc = new Document({
sections: [{ properties: {}, children: paragraphs }]
});
return Packer.toBuffer(doc);