/** * ResumeForge V2 — Frontend with PDF Preview & Visual Edit Mode */ (function () { 'use strict'; // ─── State ─── const state = { resumeContent: '', jobDescription: '', fileName: 'resume.tex', pdfSessionId: null, analysis: null, editSelections: {}, editModifiedTexts: {}, // Tracks user edits to suggestions modifiedContent: '', exportSessionId: null, }; const $ = (s) => document.querySelector(s); const $$ = (s) => document.querySelectorAll(s); const els = { stageInput: $('#stage-input'), stageReview: $('#stage-review'), stageExport: $('#stage-export'), dropZone: $('#drop-zone'), fileInput: $('#file-input'), btnBrowse: $('#btn-browse'), jdInput: $('#jd-input'), instructionsInput: $('#instructions-input'), resumeFileInfo: $('#resume-file-info'), fileName: $('#file-name'), btnRemoveFile: $('#btn-remove-file'), btnLoadSample: $('#btn-load-sample'), btnAnalyze: $('#btn-analyze'), loadingOverlay: $('#loading-overlay'), loadingStatus: $('#loading-status'), pdfCompileStatus: $('#pdf-compile-status'), pdfPlaceholder: $('#pdf-placeholder'), pdfCanvas: $('#pdf-canvas'), reviewPdfCanvas: $('#review-pdf-canvas'), exportPdfCanvas: $('#export-pdf-canvas'), scoreBefore: $('#score-before'), scoreAfter: $('#score-after'), keywordPills: $('#keyword-pills'), analysisSummary: $('#analysis-summary'), editCount: $('#edit-count'), editsContainer: $('#edits-container'), btnSelectAll: $('#btn-select-all'), btnDeselectAll: $('#btn-deselect-all'), btnBackInput: $('#btn-back-input'), btnApply: $('#btn-apply'), selectedCount: $('#selected-count'), btnDownloadPdf: $('#btn-download-pdf'), btnDownloadTex: $('#btn-download-tex'), btnCopyTex: $('#btn-copy-tex'), btnStartOver: $('#btn-start-over'), btnEditSource: $('#btn-edit-source'), btnIterateAi: $('#btn-iterate-ai'), manualEditCard: $('#manual-edit-card'), mainExportCard: $('.export-actions-pane .export-card:not(#manual-edit-card)'), btnDoneEditing: $('#btn-done-editing'), btnCompilePreview: $('#btn-compile-preview'), manualLatexEditor: $('#manual-latex-editor'), keyModal: $('#key-modal'), apiKeyInput: $('#api-key-input'), btnSaveKey: $('#btn-save-key'), toast: $('#toast'), toastMessage: $('#toast-message'), toastClose: $('#toast-close'), }; const SAMPLE_JD = `Senior Full-Stack Engineer — FinTech Startup We are looking for a Senior Full-Stack Engineer to join our growing team building the next generation of digital banking infrastructure. Responsibilities: - Design and implement scalable microservices using Python (FastAPI/Django) and Go - Build and maintain React-based web applications with TypeScript - Architect and optimize cloud-native solutions on AWS (ECS, Lambda, DynamoDB, SQS) - Implement robust CI/CD pipelines and infrastructure as code using Terraform - Lead technical design reviews and mentor junior engineers - Work with product managers to translate business requirements into technical solutions - Ensure high availability and security compliance (SOC 2, PCI-DSS) Requirements: - 5+ years of professional software engineering experience - Strong proficiency in Python and TypeScript/JavaScript - Experience with cloud platforms (AWS preferred) and containerization (Docker, Kubernetes) - Experience with relational and NoSQL databases (PostgreSQL, DynamoDB, Redis) - Familiarity with event-driven architectures and message queues - BS/MS in Computer Science or equivalent experience Nice to have: - Experience in FinTech or regulated industries - Knowledge of Terraform or CloudFormation - Experience with GraphQL - Contributions to open-source projects`; // ─── Utils ─── function showToast(msg, duration = 5000) { els.toastMessage.textContent = msg; els.toast.classList.remove('hidden'); clearTimeout(state._toastTimer); state._toastTimer = setTimeout(() => els.toast.classList.add('hidden'), duration); } function escapeHtml(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } function updateAnalyzeButton() { const hasResume = state.resumeContent.length > 20; const hasJD = els.jdInput.value.trim().length > 20; els.btnAnalyze.disabled = !(hasResume && hasJD); } function switchStage(name) { $$('.stage').forEach(s => s.classList.remove('active')); $$('.step').forEach(s => s.classList.remove('active', 'completed')); $$('.step-connector').forEach(c => c.classList.remove('completed')); const map = { input: 1, review: 2, export: 3 }; const n = map[name]; $(`#stage-${name}`).classList.add('active'); $$('.step').forEach(s => { const sn = parseInt(s.dataset.step); if (sn < n) s.classList.add('completed'); if (sn === n) s.classList.add('active'); }); $$('.step-connector').forEach((c, i) => { if (i + 1 < n) c.classList.add('completed'); }); window.scrollTo({ top: 0, behavior: 'smooth' }); } function getSelectedCount() { return Object.values(state.editSelections).filter(Boolean).length; } function updateSelectedCount() { const c = getSelectedCount(); els.selectedCount.textContent = c; els.btnApply.disabled = c === 0; } // ─── PDF Rendering ─── async function renderPdf(sessionId, canvas) { if (!sessionId) return; try { const url = `/api/pdf/${sessionId}`; const pdf = await pdfjsLib.getDocument(url).promise; const page = await pdf.getPage(1); // Scale to fit container width const container = canvas.parentElement; const containerWidth = container.clientWidth - 32; const unscaledViewport = page.getViewport({ scale: 1 }); const scale = Math.min(containerWidth / unscaledViewport.width, 2.0); const viewport = page.getViewport({ scale }); canvas.width = viewport.width; canvas.height = viewport.height; canvas.classList.remove('hidden'); await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport, }).promise; } catch (err) { console.error('PDF render error:', err); } } async function compilePdf(latexContent) { const resp = await fetch('/api/compile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ latex_content: latexContent }), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || 'Compilation failed'); } const data = await resp.json(); return data.session_id; } // ─── File Handling ─── function handleFile(file) { if (!file) return; const ext = '.' + file.name.split('.').pop().toLowerCase(); if (!['.tex', '.latex', '.txt'].includes(ext)) { showToast('Please upload a .tex file.'); return; } state.fileName = file.name; const reader = new FileReader(); reader.onload = async (e) => { state.resumeContent = e.target.result; els.dropZone.classList.add('hidden'); els.resumeFileInfo.classList.remove('hidden'); els.fileName.textContent = file.name; updateAnalyzeButton(); await compileAndPreview(); }; reader.readAsText(file); } async function compileAndPreview() { els.pdfCompileStatus.classList.remove('hidden'); els.pdfPlaceholder.classList.add('hidden'); try { state.pdfSessionId = await compilePdf(state.resumeContent); await renderPdf(state.pdfSessionId, els.pdfCanvas); } catch (err) { showToast('PDF compilation failed: ' + err.message); els.pdfPlaceholder.classList.remove('hidden'); } finally { els.pdfCompileStatus.classList.add('hidden'); } } // ─── API Key ─── async function checkApiKey() { try { const r = await fetch('/api/key/status'); return (await r.json()).has_key; } catch { return false; } } async function saveApiKey(key) { const r = await fetch('/api/key/set', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_key: key }), }); return r.ok; } // ─── Analyze ─── async function analyzeResume() { if (!(await checkApiKey())) { els.keyModal.classList.remove('hidden'); els.apiKeyInput.focus(); return; } els.loadingOverlay.classList.remove('hidden'); try { const resp = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resume_content: state.resumeContent, job_description: els.jdInput.value, additional_instructions: els.instructionsInput ? els.instructionsInput.value : "", }), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || 'Analysis failed'); } state.analysis = await resp.json(); state.jobDescription = els.jdInput.value; switchStage('review'); renderReviewStage(); } catch (err) { showToast(err.message); } finally { els.loadingOverlay.classList.add('hidden'); } } // ─── Apply ─── async function applyEdits() { const selectedEdits = state.analysis.edits .map((e, i) => { if (!state.editSelections[i]) return null; return { original_text: e.original_text, modified_text: state.editModifiedTexts[i] || e.modified_text, }; }) .filter(Boolean); if (selectedEdits.length === 0) { showToast('Select at least one edit.'); return; } els.loadingOverlay.classList.remove('hidden'); els.loadingStatus.textContent = 'Applying edits and compiling PDF...'; try { const resp = await fetch('/api/apply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resume_content: state.resumeContent, edits: selectedEdits, }), }); if (!resp.ok) throw new Error('Failed to apply edits'); const data = await resp.json(); state.modifiedContent = data.modified_content; state.exportSessionId = data.session_id; switchStage('export'); // Render export PDF if (state.exportSessionId) { await renderPdf(state.exportSessionId, els.exportPdfCanvas); } } catch (err) { showToast(err.message); } finally { els.loadingOverlay.classList.add('hidden'); } } // ─── Render Review ─── function renderReviewStage() { const a = state.analysis; // Render original PDF in review pane if (state.pdfSessionId) { renderPdf(state.pdfSessionId, els.reviewPdfCanvas); } // Scores els.scoreBefore.textContent = a.match_score_before; els.scoreAfter.textContent = a.match_score_after; // Summary els.analysisSummary.textContent = a.summary; // Keywords els.keywordPills.innerHTML = a.keyword_gaps .map(k => `${escapeHtml(k)}`) .join(''); // Edits els.editCount.textContent = a.edits.length; state.editSelections = {}; state.editModifiedTexts = {}; a.edits.forEach((_, i) => { state.editSelections[i] = true; }); els.editsContainer.innerHTML = a.edits.map((edit, i) => `
`).join(''); updateSelectedCount(); // Event listeners els.editsContainer.querySelectorAll('.edit-card').forEach(card => { const idx = parseInt(card.dataset.index); card.querySelector('.edit-checkbox').addEventListener('click', (e) => { e.stopPropagation(); state.editSelections[idx] = !state.editSelections[idx]; card.classList.toggle('selected', state.editSelections[idx]); updateSelectedCount(); }); card.querySelector('.edit-toggle-btn').addEventListener('click', (e) => { e.stopPropagation(); card.classList.toggle('expanded'); }); card.querySelector('.edit-card-header').addEventListener('click', () => { card.classList.toggle('expanded'); }); }); // Track user edits to suggestion textareas // Note: the textarea shows display_modified (readable text) but we need to // detect if the user changed it. If they did, we'll need to update the LaTeX. // For simplicity, if user edits the display text, we update modified_text // by replacing the display portion in the LaTeX. els.editsContainer.querySelectorAll('.edit-suggestion-textarea').forEach(ta => { const idx = parseInt(ta.dataset.index); const edit = state.analysis.edits[idx]; ta.addEventListener('input', () => { // If user modifies the displayed text, update the modified LaTeX // We do a simple approach: replace the display_modified portion in modified_text const newDisplayText = ta.value; // If the display was different from original display, mark as user-edited if (newDisplayText !== (edit.display_modified || edit.modified_text)) { // Simple heuristic: rebuild the LaTeX by replacing the readable content // while preserving LaTeX structure state.editModifiedTexts[idx] = rebuildLatex( edit.modified_text, edit.display_modified || edit.modified_text, newDisplayText ); } }); }); } /** * Rebuild LaTeX text by replacing the display portion. * Simple approach: if the original LaTeX has known patterns, replace them. * Fallback: just use the new text as-is (losing LaTeX formatting). */ function rebuildLatex(originalLatex, originalDisplay, newDisplay) { // Try to preserve LaTeX structure by doing a text replacement // First check if the display text is literally in the LaTeX (common case) if (originalLatex.includes(originalDisplay)) { return originalLatex.replace(originalDisplay, newDisplay); } // Fallback: return the new display text with basic LaTeX wrapping if item if (originalLatex.trim().startsWith('\\item')) { return '\\item ' + newDisplay; } return newDisplay; } // ─── Init ─── function init() { // File handling els.btnBrowse.addEventListener('click', () => els.fileInput.click()); els.fileInput.addEventListener('change', (e) => handleFile(e.target.files[0])); els.dropZone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropZone.classList.add('dragover'); }); els.dropZone.addEventListener('dragleave', () => els.dropZone.classList.remove('dragover')); els.dropZone.addEventListener('drop', (e) => { e.preventDefault(); els.dropZone.classList.remove('dragover'); handleFile(e.dataTransfer.files[0]); }); els.dropZone.addEventListener('click', () => els.fileInput.click()); els.btnRemoveFile.addEventListener('click', () => { state.resumeContent = ''; state.pdfSessionId = null; els.fileInput.value = ''; els.dropZone.classList.remove('hidden'); els.resumeFileInfo.classList.add('hidden'); els.pdfCanvas.classList.add('hidden'); els.pdfPlaceholder.classList.remove('hidden'); updateAnalyzeButton(); }); els.jdInput.addEventListener('input', updateAnalyzeButton); // Load sample els.btnLoadSample.addEventListener('click', async () => { try { const resp = await fetch('/sample/sample_resume.tex'); if (resp.ok) state.resumeContent = await resp.text(); } catch { showToast('Could not load sample.'); return; } els.jdInput.value = SAMPLE_JD; els.dropZone.classList.add('hidden'); els.resumeFileInfo.classList.remove('hidden'); els.fileName.textContent = 'sample_resume.tex'; updateAnalyzeButton(); await compileAndPreview(); }); // Analyze els.btnAnalyze.addEventListener('click', analyzeResume); // Review controls els.btnSelectAll.addEventListener('click', () => { Object.keys(state.editSelections).forEach(k => { state.editSelections[k] = true; }); els.editsContainer.querySelectorAll('.edit-card').forEach(c => c.classList.add('selected')); updateSelectedCount(); }); els.btnDeselectAll.addEventListener('click', () => { Object.keys(state.editSelections).forEach(k => { state.editSelections[k] = false; }); els.editsContainer.querySelectorAll('.edit-card').forEach(c => c.classList.remove('selected')); updateSelectedCount(); }); els.btnBackInput.addEventListener('click', () => switchStage('input')); els.btnApply.addEventListener('click', applyEdits); // Export els.btnDownloadPdf.addEventListener('click', () => { if (state.exportSessionId) { const a = document.createElement('a'); const dlName = state.fileName.replace(/\.tex$/, '_tailored.pdf'); a.href = `/api/pdf/${state.exportSessionId}?download=true&filename=${encodeURIComponent(dlName)}`; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } }); els.btnDownloadTex.addEventListener('click', () => { const blob = new Blob([state.modifiedContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = state.fileName.replace(/\.tex$/, '_tailored.tex'); a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); els.btnCopyTex.addEventListener('click', () => { navigator.clipboard.writeText(state.modifiedContent).then(() => { const orig = els.btnCopyTex.innerHTML; els.btnCopyTex.innerHTML = ' Copied!'; setTimeout(() => { els.btnCopyTex.innerHTML = orig; }, 2000); }); }); els.btnStartOver.addEventListener('click', () => { state.analysis = null; state.editSelections = {}; state.editModifiedTexts = {}; state.modifiedContent = ''; state.exportSessionId = null; switchStage('input'); }); // ─── V2.1 New Features ─── els.btnIterateAi.addEventListener('click', () => { // Take the currently tailored resume and start over with it state.resumeContent = state.modifiedContent; state.fileName = state.fileName.replace(/\.tex$/, '_tailored.tex'); // Reset state state.analysis = null; state.editSelections = {}; state.editModifiedTexts = {}; state.modifiedContent = ''; state.pdfSessionId = state.exportSessionId; // Reuse the compiled PDF session state.exportSessionId = null; // Update UI els.fileInput.value = ''; // clear physical file input els.fileName.textContent = state.fileName; els.dropZone.classList.add('hidden'); els.resumeFileInfo.classList.remove('hidden'); switchStage('input'); updateAnalyzeButton(); // Automatically preview the new state if (state.pdfSessionId) { renderPdf(state.pdfSessionId, els.pdfCanvas); } }); els.btnEditSource.addEventListener('click', () => { els.mainExportCard.classList.add('hidden'); els.manualEditCard.classList.remove('hidden'); els.btnCompilePreview.classList.remove('hidden'); $('#export-pdf-title').classList.add('hidden'); $('#export-success-badge').classList.add('hidden'); els.manualLatexEditor.value = state.modifiedContent; }); els.btnDoneEditing.addEventListener('click', () => { els.manualEditCard.classList.add('hidden'); els.mainExportCard.classList.remove('hidden'); els.btnCompilePreview.classList.add('hidden'); $('#export-pdf-title').classList.remove('hidden'); $('#export-success-badge').classList.remove('hidden'); }); els.btnCompilePreview.addEventListener('click', async () => { const newLatex = els.manualLatexEditor.value; els.btnCompilePreview.disabled = true; els.btnCompilePreview.innerHTML = ' Compiling...'; try { const newSessionId = await compilePdf(newLatex); state.exportSessionId = newSessionId; state.modifiedContent = newLatex; await renderPdf(state.exportSessionId, els.exportPdfCanvas); showToast('PDF Updated successfully!'); } catch (err) { showToast('Compilation error: ' + err.message); } finally { els.btnCompilePreview.disabled = false; els.btnCompilePreview.innerHTML = ' Compile & Preview'; } }); // Toast els.toastClose.addEventListener('click', () => els.toast.classList.add('hidden')); // API Key modal const btnChangeKey = $('#btn-change-key'); if (btnChangeKey) { btnChangeKey.addEventListener('click', () => { els.keyModal.classList.remove('hidden'); els.apiKeyInput.focus(); }); } const btnCloseModal = $('#btn-close-modal'); if (btnCloseModal) { btnCloseModal.addEventListener('click', () => { els.keyModal.classList.add('hidden'); }); } els.btnSaveKey.addEventListener('click', async () => { const key = els.apiKeyInput.value.trim(); if (!key) { els.apiKeyInput.style.borderColor = '#ef4444'; return; } els.btnSaveKey.disabled = true; els.btnSaveKey.textContent = 'Saving...'; if (await saveApiKey(key)) { els.keyModal.classList.add('hidden'); els.apiKeyInput.value = ''; // Only auto-analyze if we have both inputs, otherwise just save if (!els.btnAnalyze.disabled && state.resumeContent && els.jdInput.value.trim()) { analyzeResume(); } else { showToast('API key updated.'); } } else { showToast('Failed to save API key.'); } els.btnSaveKey.disabled = false; els.btnSaveKey.textContent = 'Save & Continue'; }); els.apiKeyInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') els.btnSaveKey.click(); els.apiKeyInput.style.borderColor = ''; }); } document.addEventListener('DOMContentLoaded', init); })();