// CV Batches - Frontend
let currentBatchId = null;
// ============================================================
// NAV — load batches when page is shown
// ============================================================
const batchNavObserver = new MutationObserver(() => {
if (document.getElementById('page-batches') && document.getElementById('page-batches').classList.contains('active')) {
loadBatches();
}
});
document.addEventListener('DOMContentLoaded', () => {
const bp = document.getElementById('page-batches');
if (bp) {
if (bp.classList.contains('active')) loadBatches();
batchNavObserver.observe(bp, { attributes: true, attributeFilter: ['class'] });
}
});
// ============================================================
// LIST BATCHES
// ============================================================
async function loadBatches() {
const list = document.getElementById('batches-list');
if (!list) return;
list.innerHTML = '
Loading...
';
try {
const resp = await fetch('/api/batches');
const data = await resp.json();
if (!data.batches || !data.batches.length) {
list.innerHTML = 'No batches yet. Create one manually or upload a requirements document.
';
return;
}
list.innerHTML = data.batches.map(b => `
${b.description ? '
' + escapeHtml(b.description) + '
' : ''}
Created: ${new Date(b.created_at).toLocaleDateString()}
${b.positions && b.positions.length ? ' · Positions: ' + b.positions.length : ''}
`).join('');
} catch (e) {
list.innerHTML = 'Error: ' + e.message + '
';
}
}
// ============================================================
// CREATE BATCH (manual)
// ============================================================
function showCreateBatchModal() {
const body = `
`;
showModal(body, 'New Batch');
}
async function createBatch() {
const name = document.getElementById('new-batch-name').value.trim();
if (!name) { toast('Name required', 'error'); return; }
const description = document.getElementById('new-batch-desc').value.trim();
try {
const resp = await fetch('/api/batches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
});
const data = await resp.json();
closeModal();
toast('Batch created');
openBatch(data.id);
} catch (e) {
toast('Error: ' + e.message, 'error');
}
}
// ============================================================
// UPLOAD DOCUMENT to create batch
// ============================================================
function showUploadBatchModal() {
const body = `
Drop requirements document here or click to browse
PDF, DOCX, or TXT — AI will extract job positions automatically
`;
showModal(body, 'Upload Requirements Document');
// Setup drag-and-drop
const zone = document.getElementById('batch-upload-zone');
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); });
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
zone.addEventListener('drop', e => {
e.preventDefault();
zone.classList.remove('dragover');
if (e.dataTransfer.files.length) uploadBatchDoc(e.dataTransfer.files[0]);
});
}
async function uploadBatchDoc(file) {
if (!file) return;
const status = document.getElementById('batch-upload-status');
status.innerHTML = ' Extracting requirements with AI... this may take 30-60 seconds
';
const formData = new FormData();
formData.append('file', file);
try {
const resp = await fetch('/api/batches/upload', { method: 'POST', body: formData });
const data = await resp.json();
if (resp.ok) {
status.innerHTML = 'Batch created! Extracted ' + (data.positions?.length || 0) + ' positions.
';
setTimeout(() => {
closeModal();
openBatch(data.id);
}, 1500);
toast('Batch created from document');
} else {
status.innerHTML = 'Upload failed: ' + (data.detail || 'Unknown error') + '
';
}
} catch (e) {
status.innerHTML = 'Upload failed: ' + e.message + '
';
}
}
// ============================================================
// BATCH DETAIL VIEW
// ============================================================
let currentBatch = null;
let batchTemplates = [];
async function loadBatchTemplates() {
try {
const resp = await fetch('http://' + location.hostname + ':8771/api/carbone/templates');
const data = await resp.json();
batchTemplates = data.templates || [];
} catch (e) {
batchTemplates = [];
}
}
async function openBatch(batchId) {
currentBatchId = batchId;
document.getElementById('batches-list-view').style.display = 'none';
const detail = document.getElementById('batch-detail-view');
detail.style.display = 'block';
detail.innerHTML = 'Loading batch...
';
await loadBatchTemplates();
try {
const resp = await fetch('/api/batches/' + batchId);
const data = await resp.json();
currentBatch = data;
renderBatchDetail(data);
} catch (e) {
detail.innerHTML = 'Error: ' + e.message + '
';
}
}
function renderBatchDetail(data) {
const batch = data.batch;
const items = data.items || [];
// Group items by position
const byPosition = {};
items.forEach(item => {
const pos = item.position_title || 'Unassigned';
if (!byPosition[pos]) byPosition[pos] = [];
byPosition[pos].push(item);
});
// Build positions HTML
const positions = batch.positions || [];
let positionsHtml = '';
if (positions.length) {
positionsHtml = '';
positions.forEach(p => {
const posItems = byPosition[p.job_title] || [];
positionsHtml += `
${escapeHtml(p.job_title || '')} ${p.num_positions > 1 ? '×' + p.num_positions : ''}
${p.description ? '
' + escapeHtml(p.description) + '
' : ''}
${p.required_skills && p.required_skills.length ? '
Required: ' + p.required_skills.map(s => '' + escapeHtml(s) + '').join(' ') + '
' : ''}
${p.required_years ? '
Min years: ' + p.required_years + '
' : ''}
${p.required_certs && p.required_certs.length ? '
Certs: ' + p.required_certs.map(c => '' + escapeHtml(c) + '').join(' ') + '
' : ''}
${posItems.length ? `
${posItems.map(item => `
${escapeHtml(item.first_name || '')} ${escapeHtml(item.last_name || '')}
${item.match_score}%
${item.status}
${item.match_reasoning ? '
' + escapeHtml(item.match_reasoning) + '
' : ''}
${item.status !== 'approved' ? '' : ''}
${item.status !== 'removed' ? '' : ''}
`).join('')}
` : '
No candidates matched yet. Click "Analyze & Match" to find candidates.
'}
`;
});
positionsHtml += '
';
}
// Unassigned items
const unassigned = byPosition['Unassigned'] || [];
if (unassigned.length) {
positionsHtml += '';
positionsHtml += unassigned.map(item => `
${escapeHtml(item.first_name || '')} ${escapeHtml(item.last_name || '')} ${item.match_score}%
`).join('');
positionsHtml += '
';
}
// Build template selector options
const currentTemplateId = batch.template_id || '';
const templateOptions = batchTemplates.map(t =>
''
).join('');
document.getElementById('batch-detail-view').innerHTML = `
${escapeHtml(batch.name)}
${escapeHtml(batch.description || '')}
${positionsHtml}
`;
}
async function saveBatchTemplate() {
const templateId = document.getElementById('batch-template-select').value;
try {
await fetch('/api/batches/' + currentBatchId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: currentBatch.batch.name,
description: currentBatch.batch.description,
template_id: templateId
})
});
toast('Template saved for this batch');
} catch (e) {
toast('Error saving template: ' + e.message, 'error');
}
}
function closeBatchDetail() {
document.getElementById('batches-list-view').style.display = 'block';
document.getElementById('batch-detail-view').style.display = 'none';
currentBatchId = null;
closeBatchChat();
loadBatches();
}
// ============================================================
// ANALYZE & MATCH
// ============================================================
async function analyzeBatch() {
if (!currentBatchId) return;
toast('Analyzing candidates... this may take a minute');
try {
const resp = await fetch('/api/batches/' + currentBatchId + '/analyze', { method: 'POST' });
const data = await resp.json();
if (resp.ok) {
toast('Matched ' + data.matched + ' candidates');
openBatch(currentBatchId);
} else {
toast('Error: ' + (data.detail || 'Analysis failed'), 'error');
}
} catch (e) {
toast('Error: ' + e.message, 'error');
}
}
// ============================================================
// APPROVE / REMOVE ITEMS
// ============================================================
async function approveItem(itemId) {
await fetch('/api/batches/' + currentBatchId + '/items/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'approved' })
});
toast('Candidate approved');
openBatch(currentBatchId);
}
async function removeItem(itemId) {
await fetch('/api/batches/' + currentBatchId + '/items/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'removed' })
});
toast('Candidate removed');
openBatch(currentBatchId);
}
// ============================================================
// DELETE BATCH
// ============================================================
async function deleteBatch() {
if (!currentBatchId || !confirm('Delete this batch and all its items?')) return;
await fetch('/api/batches/' + currentBatchId, { method: 'DELETE' });
toast('Batch deleted');
closeBatchDetail();
}
// ============================================================
// BATCH CHAT (slide-in panel)
// ============================================================
function openBatchChat() {
const panel = document.getElementById('batch-chat-panel');
panel.style.display = 'flex';
renderBatchChatMessages(currentBatch?.chat || []);
}
function closeBatchChat() {
document.getElementById('batch-chat-panel').style.display = 'none';
}
function renderBatchChatMessages(messages) {
const container = document.getElementById('batch-chat-messages');
if (!messages.length) {
container.innerHTML = 'Start discussing this batch with the AI...
';
return;
}
container.innerHTML = messages.map(m => `
`).join('');
container.scrollTop = container.scrollHeight;
}
async function sendBatchChat() {
const input = document.getElementById('batch-chat-input');
const message = input.value.trim();
if (!message || !currentBatchId) return;
input.value = '';
// Show user message immediately
const container = document.getElementById('batch-chat-messages');
container.innerHTML += ``;
container.scrollTop = container.scrollHeight;
// Show loading
container.innerHTML += '';
container.scrollTop = container.scrollHeight;
try {
const resp = await fetch('/api/batches/' + currentBatchId + '/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await resp.json();
// Remove loading and show response
document.getElementById('chat-loading')?.remove();
container.innerHTML += `${escapeHtml(data.response)}
`;
container.scrollTop = container.scrollHeight;
} catch (e) {
document.getElementById('chat-loading')?.remove();
toast('Chat error: ' + e.message, 'error');
}
}
// Enter key to send in chat
document.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('batch-chat-input');
if (input) {
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendBatchChat();
}
});
}
});