Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions extension-ready/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@

const pendingRequests = new Map(); // requestId -> AbortController

function rateLimitError(response) {
const resetHeader = response.headers.get('RateLimit-Reset') || response.headers.get('X-RateLimit-Reset');
if (resetHeader) {
const resetSec = Number(resetHeader);
if (!isNaN(resetSec) && resetSec > 1e9) {
const resetTime = new Date(resetSec * 1000);
const hh = resetTime.getHours().toString().padStart(2, '0');
const mm = resetTime.getMinutes().toString().padStart(2, '0');
return `Rate limit reached — you can try again at ${hh}:${mm}.`;
}
}
return 'Rate limit reached — please try again in up to 60 minutes.';
}

const DEFAULT_PROXY_URL = 'https://draftapply.onrender.com';

async function getProxyUrl() {
Expand Down Expand Up @@ -104,7 +118,7 @@ async function ensureContentScriptInjected(tabId) {
});
await chrome.scripting.executeScript({
target: { tabId, allFrames: true },
files: ['page-extractor.js', 'content.js']
files: ['stats.js', 'page-extractor.js', 'content.js']
});
} catch (err) {
console.warn('Could not inject content script:', err.message);
Expand Down Expand Up @@ -343,6 +357,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
}

if (response.status === 429) throw new Error(rateLimitError(response));
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `Error ${response.status}`);
Expand Down Expand Up @@ -399,6 +414,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
}

if (response.status === 429) throw new Error(rateLimitError(response));
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `Error ${response.status}`);
Expand Down Expand Up @@ -456,7 +472,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!mainFrameReady) {
try {
await chrome.scripting.insertCSS({ target: { tabId, frameIds: [0] }, files: ['content.css'] });
await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, files: ['page-extractor.js', 'content.js'] });
await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, files: ['stats.js', 'page-extractor.js', 'content.js'] });
} catch { /* restricted page */ }
// Brief delay for content script to initialize after fresh injection
await new Promise(r => setTimeout(r, 300));
Expand Down
2 changes: 2 additions & 0 deletions extension-ready/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ class DraftApplyExtension {
this.setNativeValue(target, message.answer);
this.dispatchInputEvents(target, message.answer);
this.showNotification('Answer inserted!');
globalThis.DraftApplyStats?.track?.('answersInserted')?.catch?.(() => {});
} catch (e) {
console.warn('[DraftApply] Insert from parent failed:', e);
}
Expand Down Expand Up @@ -1276,6 +1277,7 @@ class DraftApplyExtension {
this.currentField = target;
this.hideModal();
this.showNotification('Answer inserted!');
globalThis.DraftApplyStats?.track?.('answersInserted')?.catch?.(() => {});
} catch (e) {
console.warn('[DraftApply] Insert failed:', e);
this.showNotification('Could not insert into that field. Try clicking the field and typing once, then Insert again.', 'error');
Expand Down
11 changes: 11 additions & 0 deletions extension-ready/cv-export.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@
Chrome only adds those when no @page margin is set via CSS. */
@page { margin: 0; }

/* Keep each experience/education entry together — no mid-entry page breaks */
.cv-entry { break-inside: avoid; }

@media print {
body { background: white; }
#toolbar { display: none !important; }
Expand All @@ -207,6 +210,13 @@
padding: 20mm 18mm;
min-height: auto;
}
.cv-entry { break-inside: avoid; }
.cv-entry-row { break-after: avoid; }
.cv-job-title { break-before: avoid; break-after: avoid; }
.cv-role-focus { break-before: avoid; break-after: avoid; }
.cv-bullets { break-before: avoid; }
.cv-bullets li { break-inside: avoid; }
.cv-section-header { break-after: avoid; }
}
</style>
</head>
Expand All @@ -215,6 +225,7 @@
<div id="toolbar">
<span id="toolbar-title">DraftApply — Harvard CV</span>
<button id="print-btn">Save as PDF / Print</button>
<button id="word-btn">Download Word (.doc)</button>
<button class="btn-close" id="close-btn">Close</button>
</div>

Expand Down
163 changes: 161 additions & 2 deletions extension-ready/cv-export.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
(async () => {
document.getElementById('print-btn')?.addEventListener('click', () => window.print());
document.getElementById('word-btn')?.addEventListener('click', () => {
const content = document.getElementById('cv-content');
const title = document.title || 'Tailored CV';
if (!content?.innerHTML?.trim()) return;
downloadWordDocument(content.innerHTML, title);
});
document.getElementById('close-btn')?.addEventListener('click', async () => {
try {
const tab = await chrome.tabs.getCurrent();
Expand Down Expand Up @@ -133,6 +139,148 @@ function contactLink(label, url) {
return `<a href="${esc(url)}" target="_blank" rel="noopener">${esc(label)}</a>`;
}

function safeDownloadName(title) {
const clean = String(title || 'Tailored CV')
.replace(/\s+CV\s*$/i, ' CV')
.replace(/[\\/:*?"<>|]+/g, '')
.replace(/\s+/g, ' ')
.trim();
return `${clean || 'Tailored CV'}.doc`;
}

function buildWordDocument(cvHtml, title = 'Tailored CV') {
const cleanTitle = esc(String(title || 'Tailored CV'));
return `<!DOCTYPE html>
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="urn:schemas-microsoft-com:office:word"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="utf-8">
<title>${cleanTitle}</title>
<!--[if gte mso 9]>
<xml>
<w:WordDocument>
<w:View>Print</w:View>
<w:Zoom>100</w:Zoom>
<w:DoNotOptimizeForBrowser/>
</w:WordDocument>
</xml>
<![endif]-->
<style>
@page { margin: 0.7in; }
body {
font-family: Calibri, Georgia, serif;
font-size: 11pt;
line-height: 1.45;
color: #111;
}
.cv-name {
font-size: 18pt;
font-weight: 700;
text-align: center;
text-transform: uppercase;
letter-spacing: 3px;
margin-bottom: 5px;
}
.cv-headline {
font-size: 11pt;
font-style: italic;
text-align: center;
color: #444;
margin-bottom: 3px;
}
.cv-contact {
font-size: 9.5pt;
text-align: center;
color: #555;
line-height: 1.5;
margin: 0;
}
.cv-contact a, .cv-body a, .cv-bullets li a {
color: #111;
text-decoration: underline;
}
.cv-header-rule {
border: none;
border-top: 1.5pt solid #0a0a0a;
margin: 10pt 0 0;
}
.cv-section-header {
font-size: 10pt;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
border-bottom: 1.5pt solid #0a0a0a;
padding-bottom: 2pt;
margin-top: 16pt;
margin-bottom: 7pt;
}
.cv-entry-row {
margin-top: 9pt;
width: 100%;
clear: both;
}
.cv-company {
font-size: 11.6pt;
font-weight: 800;
}
.cv-entry-dates {
font-size: 9.5pt;
color: #444;
float: right;
margin-left: 12pt;
}
.cv-job-title {
font-size: 10.5pt;
font-style: italic;
font-weight: 600;
color: #333;
margin: 0 0 3pt;
}
.cv-role-focus {
font-size: 10pt;
font-style: italic;
color: #4b5563;
margin: 0 0 5pt;
}
.cv-bullets {
padding-left: 18pt;
margin: 4pt 0;
}
.cv-bullets li {
font-size: 10.5pt;
line-height: 1.45;
margin-bottom: 2pt;
}
.cv-body {
font-size: 10.5pt;
line-height: 1.5;
margin: 0 0 3pt;
}
.cv-date-line {
font-size: 9.5pt;
color: #555;
margin-bottom: 2pt;
}
.cv-spacer { height: 4pt; }
</style>
</head>
<body>${cvHtml}</body>
</html>`;
}

function downloadWordDocument(cvHtml, title = 'Tailored CV') {
const blob = new Blob(['\ufeff', buildWordDocument(cvHtml, title)], { type: 'application/msword' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = safeDownloadName(title);
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}

function splitSkillLine(line) {
const text = String(line || '')
.replace(/\)\s*(?=[A-Z][A-Za-z/& ]{2,36}:)/g, ') ')
Expand Down Expand Up @@ -201,12 +349,21 @@ function formatCvToHtml(rawText) {
let pendingCompany = null;
// True immediately after emitting an entry row, so the next short line → cv-job-title
let afterEntryRow = false;
let openEntry = false;

const closeList = () => {
if (listOpen) { html += '</ul>'; listOpen = false; }
};

const closeEntry = () => {
if (openEntry) { html += '</div>'; openEntry = false; }
};

const emitEntryRow = (company, dates) => {
closeList();
closeEntry();
html += '<div class="cv-entry">';
openEntry = true;
html += '<div class="cv-entry-row">';
html += `<span class="cv-company">${esc(company)}</span>`;
if (dates) html += `<span class="cv-entry-dates">${esc(dates)}</span>`;
Expand Down Expand Up @@ -258,6 +415,7 @@ function formatCvToHtml(rawText) {
if (isSectionHeader(line)) {
closeList();
flushPendingCompany(null);
closeEntry();
afterEntryRow = false;
if (inHeader) {
html += '<hr class="cv-header-rule">';
Expand Down Expand Up @@ -342,7 +500,7 @@ function formatCvToHtml(rawText) {
}

// Line immediately after an entry row → job title (italic)
if (afterEntryRow && line.length < 70 && !isContactLine(line) && !isDateLine(line)) {
if (afterEntryRow && line.length < 120 && !isContactLine(line) && !isDateLine(line)) {
const title = stripJobTitleLabel(line);
if (title) html += `<p class="cv-job-title">${esc(title)}</p>`;
afterEntryRow = false;
Expand All @@ -356,7 +514,7 @@ function formatCvToHtml(rawText) {
}

// Pending company + next short non-date line → flush company (no dates), emit as job title
if (pendingCompany !== null && line.length < 70 && !isContactLine(line) && !isDateLine(line)) {
if (pendingCompany !== null && line.length < 120 && !isContactLine(line) && !isDateLine(line)) {
flushPendingCompany(null);
afterEntryRow = false;
const title = stripJobTitleLabel(line);
Expand Down Expand Up @@ -393,5 +551,6 @@ function formatCvToHtml(rawText) {

closeList();
flushPendingCompany(null);
closeEntry();
return html;
}
2 changes: 1 addition & 1 deletion extension-ready/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"https://*.glassdoor.com/*",
"https://*.glassdoor.co.uk/*"
],
"js": ["page-extractor.js", "content.js"],
"js": ["stats.js", "page-extractor.js", "content.js"],
"css": ["content.css"],
"run_at": "document_idle",
"all_frames": true
Expand Down
27 changes: 17 additions & 10 deletions extension-ready/page-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,23 +435,30 @@ class PageExtractor {
extractCleanPageText() {
if (!document.body) return '';

const clone = document.body.cloneNode(true);
const removeSelectors = [
'script', 'style', 'nav', 'footer', 'header',
'aside', '.sidebar', '.navigation', '.menu',
'.cookie', '.popup', '.modal', '.ad', '.advertisement'
];

for (const selector of removeSelectors) {
clone.querySelectorAll(selector).forEach(el => el.remove());
let bodyText = '';
try {
const clone = document.body.cloneNode(true);
const removeSelectors = [
'script', 'style', 'nav', 'footer', 'header',
'aside', '.sidebar', '.navigation', '.menu',
'.cookie', '.popup', '.modal', '.ad', '.advertisement'
];
for (const selector of removeSelectors) {
clone.querySelectorAll(selector).forEach(el => el.remove());
}
bodyText = clone.textContent || '';
} catch {
// cloneNode can throw on pages with date/number inputs whose value attribute
// is "undefined" or out of range (e.g. Ashby application forms)
bodyText = document.body.innerText || document.body.textContent || '';
}

const shadowText = this.openRoots()
.filter(root => root !== document)
.map(root => root.textContent || '')
.join('\n');

return this.cleanText(`${clone.textContent || ''}\n${shadowText}`);
return this.cleanText(`${bodyText}\n${shadowText}`);
}

/**
Expand Down
Loading
Loading