// Main popup script
class CoverLetterGenerator {
constructor() {
this.currentJobDescription = "";
this.generatedCoverLetter = "";
this.apiStatus = { status: "unknown", message: "", type: "info" };
this.init();
}
async init() {
await this.checkSetup();
this.bindEvents();
await this.loadPersistedCoverLetter();
this.showDetectedJobDescription();
// Only check API status if user is about to generate a cover letter, not on every popup open
}
async loadPersistedCoverLetter() {
try {
const result = await chrome.storage.local.get(["generatedCoverLetter"]);
if (result.generatedCoverLetter) {
this.generatedCoverLetter = result.generatedCoverLetter;
this.displayGeneratedCoverLetter();
}
} catch (error) {
console.log("No persisted cover letter found or error loading:", error);
}
}
async savePersistedCoverLetter(coverLetter) {
try {
await chrome.storage.local.set({ generatedCoverLetter: coverLetter });
console.log("Cover letter saved to storage");
} catch (error) {
console.error("Error saving cover letter to storage:", error);
}
}
async clearPersistedCoverLetter() {
try {
await chrome.storage.local.remove(["generatedCoverLetter"]);
this.generatedCoverLetter = "";
console.log("Cover letter cleared from storage");
} catch (error) {
console.error("Error clearing cover letter from storage:", error);
}
}
displayGeneratedCoverLetter() {
if (this.generatedCoverLetter) {
this.showResult(this.generatedCoverLetter);
this.showStatus("Previous cover letter restored! 📄", "info");
// Hide the generate button when we have a persisted letter
const generateBtn = document.getElementById("generateBtn");
if (generateBtn) {
generateBtn.style.display = "none";
}
}
}
async showDetectedJobDescription() {
// Always try to scrape and show the job description
const jobDescription = await this.scrapeJobDescription();
const jobInfoElement = document.getElementById("jobInfo");
const jobInfoSection = document.getElementById("jobInfoSection");
if (jobInfoElement && jobInfoSection) {
if (jobDescription && jobDescription !== "Could not extract job description") {
jobInfoElement.textContent = jobDescription.substring(0, 300) + (jobDescription.length > 300 ? "..." : "");
jobInfoSection.style.display = "block";
} else {
jobInfoElement.textContent = "No job description detected on this page.";
jobInfoSection.style.display = "block";
}
}
}
async checkApiStatus() {
// Show API status in statusMessage, not apiStatusBar
this.showStatus("Checking Gemini API status...", "info");
try {
const result = await chrome.storage.local.get(["geminiApiKey"]);
const apiKey = result.geminiApiKey;
if (!apiKey) {
this.showStatus("API key not set", "error");
return;
}
// Use Gemini 2.0 Flash Experimental endpoint
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [{ parts: [{ text: "Ping" }] }],
generationConfig: { temperature: 0.1, maxOutputTokens: 5 },
}),
}
);
if (!response.ok) {
const errorData = await response.json();
this.showStatus(`API error: ${errorData.error?.message || "Unknown error"}`, "error");
return;
}
const data = await response.json();
if (data.candidates && data.candidates[0] && data.candidates[0].content) {
this.showStatus("✅ Gemini API connected", "success");
} else {
this.showStatus("API responded, but no valid content", "error");
}
} catch (error) {
this.showStatus("API connection failed: " + error.message, "error");
}
}
showApiStatus(message, type) {
const bar = document.getElementById("apiStatusBar");
if (bar) {
bar.textContent = message;
bar.className = `status ${type}`;
bar.style.display = "block";
}
}
async checkSetup() {
try {
const result = await chrome.storage.local.get(["coverLetterTemplate", "geminiApiKey", "personalDetails"]);
// Check what features are available
const hasApiKey = result.geminiApiKey && result.geminiApiKey.trim();
const hasCoverTemplate = result.coverLetterTemplate && result.coverLetterTemplate.trim();
const hasPersonalDetails =
result.personalDetails && Object.keys(result.personalDetails).some((key) => result.personalDetails[key]);
// Show setup required only if nothing is configured
if (!hasApiKey && !hasCoverTemplate && !hasPersonalDetails) {
this.showSetupRequired();
return false;
}
// Show main content and conditionally display features
this.showMainContent();
await this.configureAvailableFeatures(hasApiKey, hasPersonalDetails, hasCoverTemplate);
return true;
} catch (error) {
console.error("Error checking setup:", error);
this.showStatus("Error checking configuration", "error");
return false;
}
}
async configureAvailableFeatures(hasApiKey, hasPersonalDetails, hasCoverTemplate) {
const generateBtn = document.getElementById("generateBtn");
const autofillBtn = document.getElementById("autofillBtn");
const divider = document.querySelector(".divider");
// Configure cover letter generation (API key AND personal details required)
if (hasApiKey && hasPersonalDetails) {
generateBtn.style.display = "block";
generateBtn.disabled = false;
generateBtn.title = "Cover letter generation available";
} else {
generateBtn.style.display = "none";
}
// Configure autofill functionality (personal details required)
if (hasPersonalDetails) {
// Check if resume file is available to update button text
try {
const result = await chrome.storage.local.get(["resumeFileOriginal"]);
const hasResume = result.resumeFileOriginal;
autofillBtn.style.display = "block";
autofillBtn.disabled = false;
if (hasResume) {
autofillBtn.textContent = "📝 Autofill Form + Attach Resume";
autofillBtn.title = "Autofill application forms with your profile and attach saved resume";
} else {
autofillBtn.textContent = "📝 Autofill Application Form";
autofillBtn.title = "Autofill application forms with your profile";
}
} catch (error) {
console.error("Error checking resume file:", error);
autofillBtn.style.display = "block";
autofillBtn.disabled = false;
autofillBtn.textContent = "📝 Autofill Application Form";
autofillBtn.title = "Autofill application forms with your profile";
}
} else {
autofillBtn.style.display = "none";
}
// Show/hide divider based on what's visible
if (generateBtn.style.display === "block" && autofillBtn.style.display === "block") {
divider.style.display = "block";
} else {
divider.style.display = "none";
}
// Show status message about available features
this.showFeatureStatus(hasApiKey, hasPersonalDetails);
}
showFeatureStatus(hasApiKey, hasPersonalDetails) {
let statusMessages = [];
// Cover letter generation status
if (!hasPersonalDetails) {
if (!hasApiKey) {
statusMessages.push("⚠️ Add your API key and upload your resume in Settings to enable all features.");
} else {
statusMessages.push(
"⚠️ Upload your resume in Settings to enable cover letter generation and smart autofill."
);
}
} else if (!hasApiKey) {
statusMessages.push("⚠️ Add your API key to enable cover letter generation.");
}
// If both features are available, show a success message
if (hasApiKey && hasPersonalDetails) {
statusMessages.push("✅ All features enabled!");
}
const statusDiv = document.getElementById("apiStatusBar");
if (statusMessages.length > 0) {
statusDiv.innerHTML = statusMessages.join("
");
statusDiv.style.display = "block";
statusDiv.className = "status info";
// Auto-hide success message after 4 seconds
if (hasApiKey && hasPersonalDetails) {
setTimeout(() => {
if (statusDiv) {
statusDiv.style.display = "none";
}
}, 4000);
}
} else {
statusDiv.style.display = "none";
}
}
showSetupRequired() {
document.getElementById("setupRequired").style.display = "block";
document.getElementById("mainContent").style.display = "none";
}
showMainContent() {
document.getElementById("setupRequired").style.display = "none";
document.getElementById("mainContent").style.display = "block";
}
bindEvents() {
document.getElementById("openOptionsBtn")?.addEventListener("click", () => {
chrome.runtime.openOptionsPage();
});
document.getElementById("settingsBtn")?.addEventListener("click", () => {
chrome.runtime.openOptionsPage();
});
document.getElementById("generateBtn")?.addEventListener("click", () => {
this.generateCoverLetter();
});
document.getElementById("autofillBtn")?.addEventListener("click", () => {
this.handleAutofill();
});
document.getElementById("downloadBtn")?.addEventListener("click", () => {
this.downloadPDF();
});
document.getElementById("regenerateBtn")?.addEventListener("click", () => {
// Regenerate directly without showing the generate button
this.generateCoverLetter();
});
document.getElementById("deleteCoverLetterBtn")?.addEventListener("click", async () => {
await this.clearPersistedCoverLetter();
// Hide the result section and show generate button
const resultSection = document.getElementById("resultSection");
if (resultSection) resultSection.style.display = "none";
const generateBtn = document.getElementById("generateBtn");
if (generateBtn) {
generateBtn.textContent = "✨ Generate Cover Letter";
generateBtn.style.display = "block";
}
this.showStatus("Cover letter deleted! 🗑️", "info");
});
document.getElementById("attachCoverLetterBtn")?.addEventListener("click", () => {
this.attachCoverLetterToForm();
});
}
async generateCoverLetter() {
try {
this.showLoading(true);
this.showStatus("", "");
// Hide the cover letter preview while generating
const resultSection = document.getElementById("resultSection");
if (resultSection) resultSection.style.display = "none";
// Check API status before generating
await this.checkApiStatus();
// Get job description from current page
const jobDescription = await this.scrapeJobDescription();
if (!jobDescription) {
throw new Error("Could not extract job description from this page");
}
this.currentJobDescription = jobDescription;
// Update the job info display with the scraped description
const jobInfoElement = document.getElementById("jobInfo");
const jobInfoSection = document.getElementById("jobInfoSection");
if (jobInfoElement && jobInfoSection) {
jobInfoElement.textContent = jobDescription.substring(0, 300) + (jobDescription.length > 300 ? "..." : "");
jobInfoSection.style.display = "block";
}
// Generate cover letter using Gemini API
const coverLetter = await this.callGeminiAPI(jobDescription);
this.generatedCoverLetter = coverLetter;
// Save to storage for persistence
await this.savePersistedCoverLetter(coverLetter);
this.showResult(coverLetter);
this.showStatus("Cover letter generated successfully! 🎉", "success");
// Hide the generate button after success
const generateBtn = document.getElementById("generateBtn");
if (generateBtn) generateBtn.style.display = "none";
} catch (error) {
console.error("Error generating cover letter:", error);
this.showStatus(`Error: ${error.message}`, "error");
} finally {
this.showLoading(false);
}
}
async scrapeJobDescription() {
return new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
try {
const result = await chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
function: () => {
// Try multiple strategies to extract job description
const selectors = [
".job-description",
".job-details",
'[data-testid="job-description"]',
".description",
".job-summary",
".posting-description",
".job-posting-details",
".job-content",
".position-description",
];
let jobText = "";
// Try specific selectors first
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
jobText = element.innerText.trim();
if (jobText.length > 100) {
return jobText;
}
}
}
// Fallback: look for long text blocks
const textElements = document.querySelectorAll("p, div, section");
let longestText = "";
textElements.forEach((el) => {
const text = el.innerText.trim();
if (text.length > longestText.length && text.length > 200) {
// Check if it contains job-related keywords
const jobKeywords = [
"position",
"role",
"responsibility",
"requirement",
"experience",
"skill",
"job",
"career",
];
const lowerText = text.toLowerCase();
if (jobKeywords.some((keyword) => lowerText.includes(keyword))) {
longestText = text;
}
}
});
return longestText || jobText || "Could not extract job description";
},
});
resolve(result[0]?.result || "");
} catch (error) {
console.error("Error scraping job description:", error);
resolve("");
}
});
});
}
async callGeminiAPI(jobDescription) {
const result = await chrome.storage.local.get([
"personalDetails",
"coverLetterTemplate",
"geminiApiKey",
"coverLetterTones",
]);
// Build resume content from profile fields
let resumeContent = "";
if (result.personalDetails) {
const pd = result.personalDetails;
resumeContent = `Name: ${pd.firstName || ""} ${pd.lastName || ""}\nEmail: ${pd.email || ""}`;
if (pd.phone) resumeContent += `\nPhone: ${pd.phone}`;
if (pd.address) resumeContent += `\nAddress: ${pd.address}`;
if (pd.summary) resumeContent += `\nSummary: ${pd.summary}`;
if (pd.experience) resumeContent += `\nExperience: ${pd.experience}`;
if (pd.education) resumeContent += `\nEducation: ${pd.education}`;
if (pd.skills) resumeContent += `\nSkills: ${pd.skills}`;
// Add any other fields as needed
}
// Build tone instruction
let toneInstruction = "";
if (result.coverLetterTones && result.coverLetterTones.length > 0) {
toneInstruction = `\n8. Write in a ${result.coverLetterTones.join(", ")} tone`;
}
// Build length instruction based on whether user has a template
let lengthInstruction = "";
if (result.coverLetterTemplate && result.coverLetterTemplate.trim() !== "") {
lengthInstruction = "4. Match the length and style of the provided cover letter template";
} else {
lengthInstruction = "4. Write exactly 3-4 paragraphs with approximately 250-400 words total";
}
const prompt = `
You are a professional cover letter writer. Generate a personalized cover letter based on the following:
RESUME:
${resumeContent}
COVER LETTER TEMPLATE:
${result.coverLetterTemplate || "No template provided - create from scratch"}
JOB DESCRIPTION:
${jobDescription}
Instructions:
1. Use the provided resume information to highlight relevant experience
2. Follow the structure and tone of the cover letter template if provided
3. Customize the content to match the specific job description
${lengthInstruction}
5. Keep it professional and compelling, showing enthusiasm for the role
6. Include specific examples from the resume that match job requirements
7. Ensure proper paragraph structure with clear opening, body, and closing${toneInstruction}
Generate only the cover letter content, no additional commentary.
`.trim();
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${result.geminiApiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
contents: [
{
parts: [
{
text: prompt,
},
],
},
],
generationConfig: {
temperature: 0.7,
topK: 40,
topP: 0.95,
maxOutputTokens: 1024,
},
}),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Gemini API error: ${errorData.error?.message || "Unknown error"}`);
}
const data = await response.json();
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content) {
throw new Error("Invalid response from Gemini API");
}
return data.candidates[0].content.parts[0].text;
}
// showJobInfo is now handled by showDetectedJobDescription always
showResult(coverLetter) {
const resultSection = document.getElementById("resultSection");
const previewElement = document.getElementById("coverLetterPreview");
if (resultSection && previewElement) {
previewElement.textContent = coverLetter;
resultSection.style.display = "block";
}
// Add or update the 'Open in New Window' button
let openBtn = document.getElementById("openInNewWindowBtn");
if (!openBtn) {
openBtn = document.createElement("button");
openBtn.id = "openInNewWindowBtn";
openBtn.className = "button secondary";
openBtn.style.marginTop = "10px";
openBtn.textContent = "🗔 Open in New Window";
// Insert after preview
previewElement.parentNode.insertBefore(openBtn, previewElement.nextSibling);
}
openBtn.onclick = async () => {
await this.openCoverLetterInNewWindow();
};
}
async openCoverLetterInNewWindow() {
const letter = this.generatedCoverLetter || "";
if (!letter) return;
// Store the letter content temporarily
const previewId = "preview_" + Date.now();
await chrome.storage.local.set({ [previewId]: letter });
// Open preview with the ID
const url = chrome.runtime.getURL(`src/pages/preview.html?id=${previewId}`);
window.open(url, "_blank", "width=600,height=800");
}
showLoading(show) {
const loadingSection = document.getElementById("loadingSection");
const generateSection = document.getElementById("generateSection");
if (loadingSection && generateSection) {
loadingSection.style.display = show ? "block" : "none";
generateSection.style.display = show ? "none" : "block";
}
}
showStatus(message, type) {
const statusElement = document.getElementById("statusMessage");
if (statusElement && message) {
statusElement.textContent = message;
statusElement.className = `status ${type}`;
statusElement.style.display = "block";
// Auto-hide all messages after a few seconds
let hideDelay = 4000; // Default 4 seconds
if (type === "success") {
hideDelay = 3000; // Success messages: 3 seconds
} else if (type === "info") {
hideDelay = 4000; // Info messages: 4 seconds
} else if (type === "error") {
hideDelay = 6000; // Error messages: 6 seconds (longer to read)
}
setTimeout(() => {
if (statusElement) {
statusElement.style.display = "none";
}
}, hideDelay);
} else if (statusElement) {
statusElement.style.display = "none";
}
}
splitTextToFit(text, font, fontSize, maxWidth) {
// Normalize line breaks and split text into paragraphs
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const paragraphs = normalizedText.split(/\n\s*\n/);
const allLines = [];
for (const paragraph of paragraphs) {
if (paragraph.trim() === "") {
// Empty paragraph - add blank line
allLines.push("");
continue;
}
// Handle single line breaks within paragraphs (preserve intentional line breaks)
const paragraphLines = paragraph.split("\n");
for (let i = 0; i < paragraphLines.length; i++) {
const paragraphLine = paragraphLines[i].trim();
if (paragraphLine === "") {
allLines.push("");
continue;
}
// Split paragraph line into words and wrap
const words = paragraphLine.split(/\s+/);
let currentLine = "";
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
try {
const testWidth = font.widthOfTextAtSize(testLine, fontSize);
if (testWidth <= maxWidth) {
currentLine = testLine;
} else {
if (currentLine) {
allLines.push(currentLine);
currentLine = word;
} else {
// Word is too long, break it
allLines.push(word);
}
}
} catch (error) {
// Fallback: just use character count estimation
const estimatedWidth = testLine.length * fontSize * 0.6;
if (estimatedWidth <= maxWidth) {
currentLine = testLine;
} else {
if (currentLine) {
allLines.push(currentLine);
currentLine = word;
} else {
allLines.push(word);
}
}
}
}
if (currentLine) {
allLines.push(currentLine);
}
}
// Add blank line after paragraph (except for the last one)
if (paragraphs.indexOf(paragraph) < paragraphs.length - 1) {
allLines.push("");
}
}
return allLines;
}
async downloadPDF() {
if (!this.generatedCoverLetter) {
this.showStatus("No cover letter to download", "error");
return;
}
this.showStatus("Creating PDF...", "info");
try {
if (!window.PDFLib) {
throw new Error("PDF library not loaded");
}
const { PDFDocument, rgb, StandardFonts } = window.PDFLib;
const pdfDoc = await PDFDocument.create();
let page = pdfDoc.addPage([595.28, 841.89]); // A4 size in points
const { width, height } = page.getSize();
// Set font and size
const fontSize = 12;
const font = await pdfDoc.embedFont(StandardFonts.TimesRoman);
// Split text into lines that fit the page
const margin = 50;
const maxWidth = width - margin * 2;
const lines = this.splitTextToFit(this.generatedCoverLetter, font, fontSize, maxWidth);
// Add text to page
let yPosition = height - margin;
const lineHeight = fontSize + 2;
for (const line of lines) {
if (yPosition < margin + lineHeight) {
// Add new page if needed
page = pdfDoc.addPage([595.28, 841.89]);
yPosition = page.getSize().height - margin;
}
// Handle empty lines (paragraph breaks)
if (line === "") {
yPosition -= lineHeight; // Just add space for blank line
} else {
page.drawText(line, {
x: margin,
y: yPosition,
size: fontSize,
font: font,
color: rgb(0, 0, 0),
});
yPosition -= lineHeight;
}
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const element = document.createElement("a");
element.href = url;
element.download = `cover-letter-${new Date().toISOString().split("T")[0]}.pdf`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
URL.revokeObjectURL(url);
this.showStatus("Cover letter PDF downloaded! 📥", "success");
} catch (error) {
console.error("PDF creation error:", error);
this.showStatus(`PDF error: ${error.message}`, "error");
// fallback to txt
try {
const element = document.createElement("a");
const file = new Blob([this.generatedCoverLetter], { type: "text/plain" });
element.href = URL.createObjectURL(file);
element.download = `cover-letter-${new Date().toISOString().split("T")[0]}.txt`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
this.showStatus("Cover letter downloaded as TXT (PDF unavailable)", "success");
} catch (txtError) {
this.showStatus("Error downloading cover letter", "error");
}
}
}
// Autofill Methods
async handleAutofill() {
try {
// Check if personal details are configured
const result = await chrome.storage.local.get(["personalDetails", "resumeFileOriginal", "resumeFileName"]);
if (!result.personalDetails || !this.hasRequiredPersonalDetails(result.personalDetails)) {
this.showAutofillStatus("Please configure your personal details in settings first.", "error");
return;
}
this.showAutofillStatus("🔍 Scanning page for form fields...", "info");
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const currentTab = tabs[0];
// First, scan for forms
const scanResult = await chrome.tabs.sendMessage(currentTab.id, {
action: "scanForForms",
});
if (scanResult.formInfo.inputsFound === 0) {
this.showAutofillStatus("❌ No fillable form fields found on this page.", "error");
return;
}
// Show scan results
this.displayFormScanResults(scanResult.formInfo);
// Perform autofill of text fields
this.showAutofillStatus("📝 Filling form fields...", "info");
const autofillResult = await chrome.tabs.sendMessage(currentTab.id, {
action: "autofillForm",
personalDetails: result.personalDetails,
});
let successMessage = "✅ Form autofilled successfully!";
let hasResume = result.resumeFileOriginal && result.resumeFileName;
// Also try to attach resume file if available
if (hasResume) {
try {
this.showAutofillStatus("📄 Attaching resume file...", "info");
await chrome.scripting.executeScript({
target: { tabId: currentTab.id },
func: injectResumeFileScript,
args: [result.resumeFileOriginal, result.resumeFileName],
});
successMessage = "✅ Form autofilled and resume attached successfully!";
} catch (resumeError) {
console.error("Error attaching resume during autofill:", resumeError);
successMessage = "✅ Form autofilled! (Resume attachment failed - no suitable file upload found)";
}
}
if (autofillResult.success) {
this.showAutofillStatus(successMessage, "success");
} else {
this.showAutofillStatus("⚠️ Autofill completed with some issues. Please review the form.", "warning");
}
} catch (error) {
console.error("Error during autofill:", error);
if (error.message.includes("Could not establish connection")) {
this.showAutofillStatus("❌ Please refresh the page and try again.", "error");
} else {
this.showAutofillStatus("❌ Error during autofill. Please try again.", "error");
}
}
}
hasRequiredPersonalDetails(personalDetails) {
const requiredFields = ["firstName", "lastName", "email"];
return requiredFields.every((field) => personalDetails[field] && personalDetails[field].trim());
}
showAutofillStatus(message, type) {
const statusElement = document.getElementById("autofillStatus");
const resultElement = document.getElementById("formScanResult");
if (statusElement && resultElement) {
resultElement.innerHTML = `