// 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 ? '' : ''}
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 = `
`;
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 + '
' : ''}
`;
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 += ``;
messagesEl.innerHTML += ``;
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 += ``;
}
}
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();