// CV Application - Frontend JavaScript const API = ''; let currentChatId = null; // ============================================================ // NAVIGATION // ============================================================ document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', e => { e.preventDefault(); document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); link.classList.add('active'); const page = link.dataset.page; document.getElementById('page-' + page).classList.add('active'); // Load data for page if (page === 'dashboard') loadDashboard(); if (page === 'candidates') loadCandidates(); if (page === 'templates') loadTemplates(); if (page === 'requirements') loadRequirements(); if (page === 'generated') loadGeneratedCVs(); if (page === 'chat') loadChat(); }); }); // ============================================================ // API helpers // ============================================================ async function api(path, method = 'GET', body = null) { const opts = { method, headers: {} }; if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } const resp = await fetch(API + path, opts); if (!resp.ok) { const err = await resp.text(); throw new Error(err); } return resp.json(); } function toast(msg, type = 'success') { const el = document.createElement('div'); el.className = 'toast ' + type; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 4000); } // ============================================================ // DASHBOARD // ============================================================ async function loadDashboard() { const stats = await api('/api/stats'); const grid = document.getElementById('stats-grid'); grid.innerHTML = `
${stats.total_candidates}
Total Candidates
${stats.parsed_candidates}
Parsed CVs
${stats.unique_skills}
Unique Skills
${stats.total_templates}
Templates
${stats.total_requirements}
Requirements
${stats.total_generated_cvs}
Generated CVs
`; // Recent candidates const data = await api('/api/candidates?limit=5'); document.getElementById('recent-candidates').innerHTML = data.candidates.map(c => `
${c.first_name || ''} ${c.last_name || ''} ${c.email ? '· ' + c.email : ''} ${c.parse_status}
`).join('') || '

No candidates yet

'; } // ============================================================ // CANDIDATES // ============================================================ async function loadCandidates(search = '') { const data = await api('/api/candidates' + (search ? '?search=' + encodeURIComponent(search) : '')); const list = document.getElementById('candidates-list'); if (!data.candidates.length) { list.innerHTML = '

No candidates found. Upload some CVs to get started.

'; return; } list.innerHTML = data.candidates.map(c => `
${c.first_name || ''} ${c.last_name || ''} ${c.email ? '· ' + c.email : ''}
${(c.preview || '').substring(0, 100)}...
${c.parse_status}
`).join(''); } function searchCandidates() { const q = document.getElementById('search-input').value; loadCandidates(q); } document.getElementById('search-input')?.addEventListener('keypress', e => { if (e.key === 'Enter') searchCandidates(); }); async function viewCandidate(id) { const data = await api('/api/candidates/' + id); const c = data.candidate; const body = `

${c.first_name || ''} ${c.last_name || ''}

${c.email ? '
' + c.email + '
' : ''} ${c.phone ? '
' + c.phone + '
' : ''} ${c.linkedin ? '
LinkedIn: ' + c.linkedin + '
' : ''}
${c.summary ? '

Summary

' + c.summary + '

' : ''}

Skills (${data.skills.length})

${data.skills.map(s => `
${s.skill_name} · ${s.skill_category || 'General'} · ${s.proficiency || 'N/A'}
${s.years_experience || 0} yrs
`).join('') || '

No skills extracted

'}

Experience (${data.experience.length})

${data.experience.map(e => `
${e.position || ''} at ${e.company || ''}
${formatDate(e.start_date)} — ${e.end_date ? formatDate(e.end_date) : 'Present'}
${e.description ? '

' + e.description + '

' : ''}
`).join('') || '

No experience extracted

'}

Education (${data.education.length})

${data.education.map(e => `
${e.degree || ''} ${e.field_of_study ? 'in ' + e.field_of_study : ''}
${e.institution || ''}
`).join('') || '

No education extracted

'}

Raw CV Text

${c.raw_cv_text || 'No raw text available'}
`; showModal(body, 'Candidate Details'); } async function deleteCandidate(id) { if (!confirm('Delete this candidate and all related data?')) return; await api('/api/candidates/' + id, 'DELETE'); toast('Candidate deleted'); loadCandidates(); } // ============================================================ // UPLOAD // ============================================================ const uploadZone = document.getElementById('upload-zone'); uploadZone?.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone?.addEventListener('dragleave', () => uploadZone.classList.remove('dragover')); uploadZone?.addEventListener('drop', e => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]); }); async function uploadFile(file) { if (!file) return; const status = document.getElementById('upload-status'); status.innerHTML = '
Uploading and parsing with AI... this may take 30-60 seconds
'; const formData = new FormData(); formData.append('file', file); try { const resp = await fetch(API + '/api/candidates/upload', { method: 'POST', body: formData }); const data = await resp.json(); if (data.status === 'parsed') { status.innerHTML = `
Parsed successfully!
Name: ${data.parsed_data.first_name || ''} ${data.parsed_data.last_name || ''}
Skills: ${data.parsed_data.skills?.length || 0} | Experience: ${data.parsed_data.experience?.length || 0} | Education: ${data.parsed_data.education?.length || 0}
`; toast('CV uploaded and parsed successfully'); } else { status.innerHTML = `
Parse error: ${data.error}
`; } } catch (e) { status.innerHTML = `
Upload failed: ${e.message}
`; } } // ============================================================ // TEMPLATES // ============================================================ async function loadTemplates() { const data = await api('/api/templates'); const list = document.getElementById('templates-list'); if (!data.templates.length) { list.innerHTML = '

No templates yet. Create one manually or generate with AI.

'; return; } list.innerHTML = data.templates.map(t => `
${t.name} ${t.created_by}
${t.description || ''}
`).join(''); } function showTemplateModal() { const body = `
`; showModal(body, 'Create Template'); } function showTemplateGenModal() { const body = `
`; showModal(body, 'Generate Template with AI'); } async function createTemplate() { const structure = JSON.parse(document.getElementById('tmpl-structure').value || '{}'); await api('/api/templates', 'POST', { name: document.getElementById('tmpl-name').value, description: document.getElementById('tmpl-desc').value, template_structure: structure, styling: document.getElementById('tmpl-styling').value, created_by: 'manual' }); closeModal(); toast('Template created'); loadTemplates(); } async function generateTemplate() { const desc = document.getElementById('tmpl-gen-desc').value; if (!desc) { toast('Description required', 'error'); return; } const btn = document.querySelector('.modal button'); btn.textContent = 'Generating...'; btn.disabled = true; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 120000); // 2 min timeout const resp = await fetch(API + '/api/templates/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: document.getElementById('tmpl-gen-name').value || 'AI Generated Template', description: desc }), signal: controller.signal }); clearTimeout(timeout); if (!resp.ok) { const err = await resp.text(); throw new Error(err); } closeModal(); toast('Template generated with AI'); loadTemplates(); } catch(e) { btn.textContent = 'Generate with AI'; btn.disabled = false; if (e.name === 'AbortError') { toast('Generation timed out after 2 minutes. Try a shorter description.', 'error'); } else { toast('Generation failed: ' + e.message, 'error'); } } } async function deleteTemplate(id) { if (!confirm('Delete this template?')) return; await api('/api/templates/' + id, 'DELETE'); toast('Template deleted'); loadTemplates(); } // ============================================================ // REQUIREMENTS // ============================================================ async function loadRequirements() { const data = await api('/api/requirements'); const list = document.getElementById('requirements-list'); if (!data.requirements.length) { list.innerHTML = '

No requirements yet. Create one to start matching candidates.

'; return; } list.innerHTML = data.requirements.map(r => `
${r.title} ${r.customer_name ? ' · ' + r.customer_name + '' : ''}
${r.description || ''}
${(r.requirements || []).length} position(s) requested
`).join(''); } function showRequirementModal() { const body = `

Define each position with required skills and years of experience.

`; showModal(body, 'Create Requirement Request'); } async function createRequirement() { const positions = JSON.parse(document.getElementById('req-positions').value || '[]'); await api('/api/requirements', 'POST', { title: document.getElementById('req-title').value, customer_name: document.getElementById('req-customer').value, description: document.getElementById('req-desc').value, requirements: positions }); closeModal(); toast('Requirement created'); loadRequirements(); } async function viewRequirement(id) { const data = await api('/api/requirements/' + id); const positions = data.requirements || []; const body = `

${data.title}

${data.customer_name ? '

Customer: ' + data.customer_name + '

' : ''} ${data.description ? '

' + data.description + '

' : ''}

Positions (${positions.length})

${positions.map((p, i) => `
${p.position_title || 'Position ' + (i+1)} ${p.quantity ? 'Qty: ' + p.quantity + '' : ''} ${p.experience_years ? 'Min ' + p.experience_years + ' yrs' : ''} ${p.description ? '

' + p.description + '

' : ''} ${(p.skills || []).map(s => `
${s.required ? '🔒' : '⚪'} ${s.name} ${s.min_years ? '(min ' + s.min_years + ' yrs)' : ''}
`).join('')}
`).join('')} ${data.generated_cvs && data.generated_cvs.length ? `

Generated CVs (${data.generated_cvs.length})

${data.generated_cvs.map(g => `
${g.position_title || 'N/A'} — ${g.first_name || ''} ${g.last_name || ''} ${g.status}
`).join('')} ` : ''} `; showModal(body, 'Requirement Details'); } async function matchRequirement(id) { const list = document.getElementById('requirements-list'); // Show loading const oldHTML = list.innerHTML; list.innerHTML = '
Matching candidates with AI... this may take a while
'; try { const data = await api('/api/requirements/' + id + '/match', 'POST', {}); const results = data.matches.filter(m => !m.error); results.sort((a, b) => (b.match_score || 0) - (a.match_score || 0)); const body = `

Match Results

${results.length} candidates matched

${results.map(m => `
${m.candidate_name || 'Unknown'}
${m.position_title || ''}
Match: ${m.matching_skills?.join(', ') || 'none'} | Missing: ${m.missing_skills?.join(', ') || 'none'}
${m.reasoning || ''}
${m.match_score || 0}%
`).join('')}
`; showModal(body, 'Match Results'); list.innerHTML = oldHTML; } catch(e) { list.innerHTML = oldHTML; toast('Match failed: ' + e.message, 'error'); } } async function generateForRequirement(id) { if (!confirm('Generate tailored CVs for all parsed candidates? This may take several minutes.')) return; const list = document.getElementById('requirements-list'); const oldHTML = list.innerHTML; list.innerHTML = '
Generating CVs with AI... this may take several minutes
'; try { const data = await api('/api/requirements/' + id + '/generate', 'POST', {}); closeModal(); list.innerHTML = oldHTML; toast(`Generated ${data.generated.length} CVs`); loadGeneratedCVs(); document.querySelector('[data-page="generated"]').click(); } catch(e) { list.innerHTML = oldHTML; toast('Generation failed: ' + e.message, 'error'); } } async function deleteRequirement(id) { if (!confirm('Delete this requirement and all generated CVs?')) return; await api('/api/requirements/' + id, 'DELETE'); toast('Requirement deleted'); loadRequirements(); } // ============================================================ // GENERATED CVs // ============================================================ async function loadGeneratedCVs() { const data = await api('/api/generated-cvs'); const list = document.getElementById('generated-list'); if (!data.generated_cvs.length) { list.innerHTML = '

No generated CVs yet. Match and generate from a requirement request.

'; return; } list.innerHTML = data.generated_cvs.map(g => `
${g.first_name || ''} ${g.last_name || ''} · ${g.position_title || 'N/A'}
Generated: ${formatDate(g.generation_date)} · Score: ${g.match_score || 0}%
${g.status}
`).join(''); } async function viewGeneratedCV(id) { const data = await api('/api/generated-cvs/' + id); const content = data.edited_content || data.generated_content || ''; const body = `

${data.first_name || ''} ${data.last_name || ''} — ${data.position_title || ''}

Generated: ${formatDate(data.generation_date)} · Score: ${data.match_score || 0}% · Status: ${data.status}

${data.match_reasoning ? '

Changes made: ' + data.match_reasoning + '

' : ''}
Preview
Edit
${content}
`; showModal(body, 'Generated CV', 'large'); } function switchTab(el, tabId) { el.parentElement.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); el.classList.add('active'); document.getElementById('cv-preview-tab').style.display = tabId === 'cv-preview-tab' ? 'block' : 'none'; document.getElementById('cv-edit-tab').style.display = tabId === 'cv-edit-tab' ? 'block' : 'none'; } async function saveGeneratedCV(id) { const content = document.getElementById('cv-edit-content').value; const status = document.getElementById('cv-status-select').value; await api('/api/generated-cvs/' + id, 'PUT', { edited_content: content, status }); toast('CV saved'); closeModal(); loadGeneratedCVs(); } async function deleteGeneratedCV(id) { if (!confirm('Delete this generated CV?')) return; await api('/api/generated-cvs/' + id, 'DELETE'); toast('Generated CV deleted'); loadGeneratedCVs(); } async function downloadGenPDF(genId) { toast('Rendering PDF...'); try { const result = await api('/api/generated-cvs/' + genId + '/render-pdf', 'POST'); if (result.pdf_url) { window.open(result.pdf_url, '_blank'); toast('PDF generated'); } } catch(e) { toast('PDF render failed: ' + e.message, 'error'); } } // ============================================================ // CHAT // ============================================================ async function loadChat() { // Just prepare the chat interface } async function sendChat() { const input = document.getElementById('chat-input'); const msg = input.value.trim(); if (!msg) return; input.value = ''; const messagesEl = document.getElementById('chat-messages'); // Clear placeholder if (messagesEl.querySelector('.text-muted')) messagesEl.innerHTML = ''; // Add user message messagesEl.innerHTML += `
${escapeHtml(msg)}
`; messagesEl.innerHTML += `
Thinking...
`; messagesEl.scrollTop = messagesEl.scrollHeight; try { const data = await api('/api/chat', 'POST', { message: msg, conversation_id: currentChatId }); currentChatId = data.conversation_id; document.getElementById('chat-loading').remove(); messagesEl.innerHTML += `
${escapeHtml(data.response)}
`; messagesEl.scrollTop = messagesEl.scrollHeight; } catch(e) { document.getElementById('chat-loading').remove(); messagesEl.innerHTML += `
Error: ${e.message}
`; } } document.getElementById('chat-input')?.addEventListener('keypress', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } }); // ============================================================ // MODAL + UTILITIES // ============================================================ function showModal(body, title, size = '') { const overlay = document.getElementById('modal-overlay'); overlay.innerHTML = ` `; overlay.classList.add('active'); } function closeModal() { document.getElementById('modal-overlay').classList.remove('active'); } document.getElementById('modal-overlay')?.addEventListener('click', e => { if (e.target === document.getElementById('modal-overlay')) closeModal(); }); function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatDate(d) { if (!d) return ''; try { return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short' }); } catch { return d; } } // Initial load loadDashboard();