Spaces:
Sleeping
Sleeping
| /** | |
| * 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 => `<span class="keyword-pill">${escapeHtml(k)}</span>`) | |
| .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) => ` | |
| <div class="edit-card selected expanded" data-index="${i}"> | |
| <div class="edit-card-header"> | |
| <div class="edit-checkbox"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20,6 9,17 4,12"/></svg> | |
| </div> | |
| <span class="edit-section">${escapeHtml(edit.section)}</span> | |
| <span class="edit-priority priority-${edit.priority}">${edit.priority}</span> | |
| <button class="edit-toggle-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"/></svg></button> | |
| </div> | |
| <div class="edit-body"> | |
| <p class="edit-reasoning">${escapeHtml(edit.reasoning)}</p> | |
| <div class="edit-visual-diff"> | |
| <div class="diff-col diff-col-original"> | |
| <span class="diff-col-label">Original</span> | |
| <div class="diff-col-text">${escapeHtml(edit.display_original || edit.original_text)}</div> | |
| </div> | |
| <div class="diff-col diff-col-modified"> | |
| <span class="diff-col-label">Suggested (editable)</span> | |
| <textarea class="edit-suggestion-textarea" data-index="${i}">${escapeHtml(edit.display_modified || edit.modified_text)}</textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `).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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><polyline points="20,6 9,17 4,12"/></svg> 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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="spin"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg> 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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polygon points="5 3 19 12 5 21 5 3"/></svg> 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); | |
| })(); | |