Spaces:
Sleeping
Sleeping
File size: 9,255 Bytes
3651f88 d5a4481 3651f88 d5a4481 3651f88 a52b643 7759bfb a52b643 7759bfb f057ca2 d5a4481 3651f88 8d38bb5 3651f88 7759bfb f057ca2 7759bfb d5a4481 3651f88 8d38bb5 3651f88 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | /**
* 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';
}
});
});
|