saitejatirunagari's picture
feat: competency-aware keyword placement, Skills-first design (v2.6.0)
8d38bb5
Raw
History Blame
9.26 kB
/**
* options.js β€” ATS Resume Generator extension options page
*
* chrome.storage.local key contract (shared with background.js in Plan 04):
* resume_b64 {string} Base64-encoded PDF bytes
* resume_sha256 {string} Hex SHA-256 of the original PDF bytes
* resume_filename {string} Original filename for display
* api_url {string} HF Space base URL (e.g. https://user-space.hf.space)
* api_token {string} Bearer token sent as X-Api-Token header
*
* Background service worker (background.js) reads:
* chrome.storage.local.get(['resume_b64', 'api_token', 'api_url'])
*/
// ─── Version display ──────────────────────────────────────────────────────────
document.getElementById('ext_version').textContent = 'v' + chrome.runtime.getManifest().version;
// ─── Test Connection button ──────────────────────────────────────────────────
document.getElementById('test_conn_btn').addEventListener('click', async () => {
const url = document.getElementById('api_url').value.trim();
const token = document.getElementById('api_token').value.trim();
const statusEl = document.getElementById('test_conn_status');
if (!url) { statusEl.textContent = 'Enter API URL first.'; statusEl.style.color = '#dc2626'; return; }
statusEl.textContent = 'Testing...'; statusEl.style.color = '#64748b';
try {
const r = await fetch(`${url.replace(/\/$/, '')}/api/health`, {
headers: { 'X-Api-Token': token || '' }, signal: AbortSignal.timeout(10000),
});
if (r.ok) { statusEl.textContent = 'Connected!'; statusEl.style.color = '#16a34a'; }
else if (r.status === 401) { statusEl.textContent = 'Auth failed β€” check token.'; statusEl.style.color = '#dc2626'; }
else { statusEl.textContent = `Error (HTTP ${r.status})`; statusEl.style.color = '#dc2626'; }
} catch (e) { statusEl.textContent = 'Cannot reach backend.'; statusEl.style.color = '#dc2626'; }
});
// ─── Load stored settings on page open ───────────────────────────────────────
chrome.storage.local.get(
['api_url', 'api_token', 'resume_filename', 'resume_sha256', 'resume_latex', 'gen_version_default', 'autofill_profile_json', 'default_cover_letter_latex'],
(data) => {
if (data.api_url) {
document.getElementById('api_url').value = data.api_url;
}
if (data.api_token) {
document.getElementById('api_token').value = data.api_token;
}
if (data.resume_filename) {
const shortHash = (data.resume_sha256 || '').slice(0, 16);
document.getElementById('resume_status').textContent =
`Stored: ${data.resume_filename} (SHA-256: ${shortHash}...)`;
}
if (data.resume_latex && data.resume_latex.trim()) {
document.getElementById('resume_latex').value = data.resume_latex;
document.getElementById('latex_status').textContent =
`LaTeX saved (${data.resume_latex.length.toLocaleString()} chars). Takes priority over the PDF.`;
} else if (self.DEFAULT_RESUME_LATEX) {
// R20: seed the built-in default resume so the extension works out-of-the-box.
document.getElementById('resume_latex').value = self.DEFAULT_RESUME_LATEX;
chrome.storage.local.set({ resume_latex: self.DEFAULT_RESUME_LATEX });
document.getElementById('latex_status').textContent =
`Default resume loaded (${self.DEFAULT_RESUME_LATEX.length.toLocaleString()} chars). Edit to use your own.`;
}
if (data.gen_version_default) {
document.getElementById('gen_version_default').value = data.gen_version_default;
}
if (data.autofill_profile_json) {
document.getElementById('autofill_profile_json').value = data.autofill_profile_json;
}
if (data.default_cover_letter_latex && data.default_cover_letter_latex.trim()) {
document.getElementById('cover_letter_latex').value = data.default_cover_letter_latex;
document.getElementById('cl_latex_status').textContent =
`Custom template saved (${data.default_cover_letter_latex.length.toLocaleString()} chars).`;
}
}
);
// ─── SHA-256 helper via Web Crypto API (available in extension context) ───────
async function sha256Hex(buffer) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
// ─── ArrayBuffer β†’ base64 string ─────────────────────────────────────────────
function bufToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// ─── Auto-evict generation caches to free quota for settings ─────────────────
// ponytail: settings always win over stale generation results
async function ensureQuota(needBytes) {
const LIMIT = chrome.storage.local.QUOTA_BYTES || 10485760;
const used = await new Promise(r => chrome.storage.local.getBytesInUse(null, r));
if (used + needBytes < LIMIT * 0.85) return true;
await new Promise(r => chrome.storage.local.remove(
['ats_results', 'ats_app_results', 'ats_bulk_history'], r));
const after = await new Promise(r => chrome.storage.local.getBytesInUse(null, r));
return after + needBytes < LIMIT * 0.95;
}
// ─── Save button handler ──────────────────────────────────────────────────────
document.getElementById('save_btn').addEventListener('click', async () => {
const api_url = document.getElementById('api_url').value.trim();
const api_token = document.getElementById('api_token').value.trim();
const fileInput = document.getElementById('resume_file');
const statusEl = document.getElementById('status_msg');
// Validate required fields
if (!api_url || !api_token) {
statusEl.textContent = 'Error: API URL and token are required.';
statusEl.style.color = '#dc2626';
return;
}
const toStore = { api_url, api_token };
// ─── LaTeX source (priority input) ─────────────────────────────────────────
const latexSrc = document.getElementById('resume_latex').value.trim();
toStore.resume_latex = latexSrc; // store '' to allow clearing it
toStore.gen_version_default = document.getElementById('gen_version_default').value;
const autofillProfile = document.getElementById('autofill_profile_json').value.trim();
if (autofillProfile) {
try {
JSON.parse(autofillProfile);
toStore.autofill_profile_json = autofillProfile;
} catch (_) {
statusEl.textContent = 'Error: Autofill profile must be valid JSON.';
statusEl.style.color = '#dc2626';
return;
}
} else {
toStore.autofill_profile_json = '';
}
document.getElementById('latex_status').textContent = latexSrc
? `LaTeX saved (${latexSrc.length.toLocaleString()} chars). Takes priority over the PDF.`
: 'No LaTeX saved.';
const clLatex = document.getElementById('cover_letter_latex').value.trim();
toStore.default_cover_letter_latex = clLatex;
document.getElementById('cl_latex_status').textContent = clLatex
? `Custom template saved (${clLatex.length.toLocaleString()} chars).`
: 'No custom template β€” using built-in default.';
// Handle optional resume upload
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
if (!file.name.toLowerCase().endsWith('.pdf')) {
statusEl.textContent = 'Error: Please select a PDF file.';
statusEl.style.color = '#dc2626';
return;
}
const buffer = await file.arrayBuffer();
const resume_b64 = bufToBase64(buffer);
const resume_sha256 = await sha256Hex(buffer);
const resume_filename = file.name;
toStore.resume_b64 = resume_b64;
toStore.resume_sha256 = resume_sha256;
toStore.resume_filename = resume_filename;
document.getElementById('resume_status').textContent =
`Stored: ${resume_filename} (SHA-256: ${resume_sha256.slice(0, 16)}...)`;
}
// Auto-evict generation caches if storage is tight
const payloadSize = JSON.stringify(toStore).length * 2;
if (!(await ensureQuota(payloadSize))) {
statusEl.textContent = 'Error: Storage full even after clearing caches. Remove the PDF resume or reduce LaTeX size.';
statusEl.style.color = '#dc2626';
return;
}
// Persist to chrome.storage.local
chrome.storage.local.set(toStore, () => {
if (chrome.runtime.lastError) {
statusEl.textContent = 'Error saving: ' + chrome.runtime.lastError.message;
statusEl.style.color = '#dc2626';
} else {
statusEl.textContent = 'Settings saved successfully.';
statusEl.style.color = '#16a34a';
}
});
});