PhoenixAgent / app /templates /batch.html
VarunRS5457
Reshape flow: redirect to classify page with AI pre-filled classifications
16d2e95
Raw
History Blame Contribute Delete
16.9 kB
{% extends "base.html" %}
{% block title %}Batch Processing — Aseesa Ingestion Agent{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Batch Processing</h1>
<p class="text-gray-500">Upload multiple datasets and process them all at once using default classifications</p>
</div>
<!-- Product selector -->
<div class="mb-6">
<div class="flex justify-center gap-3" id="batch-product-selector">
<label class="cursor-pointer">
<input type="radio" name="batch_product" value="reshape" class="peer hidden" checked>
<div class="peer-checked:border-brand-500 peer-checked:bg-brand-50 peer-checked:ring-2 peer-checked:ring-brand-200
border-2 border-gray-200 rounded-lg px-6 py-2.5 text-center transition-all hover:border-gray-300">
<span class="font-semibold text-gray-900 text-sm">Stars</span>
</div>
</label>
<label class="cursor-pointer">
<input type="radio" name="batch_product" value="qvey" class="peer hidden">
<div class="peer-checked:border-brand-500 peer-checked:bg-brand-50 peer-checked:ring-2 peer-checked:ring-brand-200
border-2 border-gray-200 rounded-lg px-6 py-2.5 text-center transition-all hover:border-gray-300">
<span class="font-semibold text-gray-900 text-sm">Qvey</span>
</div>
</label>
</div>
</div>
<!-- Batch upload area -->
<div class="bg-white rounded-2xl border border-gray-200 p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-gray-900">Batch Groups</h2>
<button type="button" onclick="addGroup()"
class="px-3 py-1.5 bg-brand-600 text-white text-xs font-medium rounded-lg hover:bg-brand-700 transition-colors flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Add Group
</button>
</div>
<p class="text-xs text-gray-400 mb-4">Each group is processed as a separate session. Files within a group are joined together.</p>
<div id="batch-groups" class="space-y-3">
<!-- Group 1 (default) -->
<div class="batch-group border border-gray-200 rounded-xl p-4" data-group-idx="0">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-brand-100 text-brand-600 flex items-center justify-center text-xs font-bold group-number">1</span>
<input type="text" class="group-name text-sm font-medium text-gray-900 border-0 border-b border-transparent focus:border-brand-400 focus:ring-0 px-0 py-0 w-48"
value="Group 1" placeholder="Group name">
</div>
<button type="button" onclick="removeGroup(this)" class="text-gray-400 hover:text-red-500 transition-colors" title="Remove group">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="border-2 border-dashed border-gray-200 rounded-lg p-4 text-center cursor-pointer hover:border-brand-400 transition-colors group-drop-zone"
onclick="this.querySelector('input[type=file]').click()">
<p class="text-xs text-gray-400">Drop files or click to add — .sas7bdat, .csv, .xpt, .xlsx, .xls</p>
<input type="file" accept=".sas7bdat,.csv,.xpt,.xlsx,.xls" multiple class="hidden group-file-input"
onchange="updateGroupFiles(this)">
</div>
<div class="group-file-list mt-2 space-y-1"></div>
</div>
</div>
</div>
<!-- Output format -->
<div class="bg-white rounded-2xl border border-gray-200 p-6 mb-6">
<div class="flex items-center gap-4">
<span class="text-sm font-medium text-gray-700">Output format:</span>
<div class="flex items-center gap-1.5">
<label class="cursor-pointer">
<input type="radio" name="batch_format" value="csv" checked class="peer hidden">
<span class="peer-checked:bg-brand-600 peer-checked:text-white px-3 py-1.5 rounded-lg text-xs font-medium bg-gray-100 text-gray-600 transition-colors">CSV</span>
</label>
<label class="cursor-pointer">
<input type="radio" name="batch_format" value="xlsx" class="peer hidden">
<span class="peer-checked:bg-brand-600 peer-checked:text-white px-3 py-1.5 rounded-lg text-xs font-medium bg-gray-100 text-gray-600 transition-colors">Excel</span>
</label>
</div>
</div>
</div>
<!-- Run batch button -->
<button type="button" id="run-batch-btn" onclick="runBatch()" disabled
class="w-full py-3 bg-brand-600 text-white font-medium rounded-xl hover:bg-brand-700 transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span>Process All Groups</span>
</button>
<!-- Progress area -->
<div id="batch-progress" class="hidden mt-6 space-y-4">
<div class="flex items-center justify-between text-sm">
<span id="batch-progress-label" class="font-medium text-brand-600">Processing...</span>
<span id="batch-progress-count" class="text-gray-500 font-mono text-xs">0 / 0</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div id="batch-progress-bar" class="bg-brand-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div id="batch-results" class="space-y-2"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let groupCounter = 1;
function getSelectedProduct() {
const checked = document.querySelector('input[name="batch_product"]:checked');
return checked ? checked.value : 'reshape';
}
function getSelectedFormat() {
const checked = document.querySelector('input[name="batch_format"]:checked');
return checked ? checked.value : 'csv';
}
function addGroup() {
groupCounter++;
const container = document.getElementById('batch-groups');
const div = document.createElement('div');
div.className = 'batch-group border border-gray-200 rounded-xl p-4 fade-in';
div.dataset.groupIdx = groupCounter - 1;
div.innerHTML = `
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<span class="w-6 h-6 rounded-full bg-brand-100 text-brand-600 flex items-center justify-center text-xs font-bold group-number">${groupCounter}</span>
<input type="text" class="group-name text-sm font-medium text-gray-900 border-0 border-b border-transparent focus:border-brand-400 focus:ring-0 px-0 py-0 w-48"
value="Group ${groupCounter}" placeholder="Group name">
</div>
<button type="button" onclick="removeGroup(this)" class="text-gray-400 hover:text-red-500 transition-colors" title="Remove group">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="border-2 border-dashed border-gray-200 rounded-lg p-4 text-center cursor-pointer hover:border-brand-400 transition-colors group-drop-zone"
onclick="this.querySelector('input[type=file]').click()">
<p class="text-xs text-gray-400">Drop files or click to add — .sas7bdat, .csv, .xpt, .xlsx, .xls</p>
<input type="file" accept=".sas7bdat,.csv,.xpt,.xlsx,.xls" multiple class="hidden group-file-input"
onchange="updateGroupFiles(this)">
</div>
<div class="group-file-list mt-2 space-y-1"></div>
`;
container.appendChild(div);
renumberGroups();
}
function removeGroup(btn) {
const groups = document.querySelectorAll('.batch-group');
if (groups.length <= 1) return;
btn.closest('.batch-group').remove();
renumberGroups();
updateRunButton();
}
function renumberGroups() {
document.querySelectorAll('.batch-group').forEach((g, i) => {
g.querySelector('.group-number').textContent = i + 1;
});
}
function updateGroupFiles(input) {
const group = input.closest('.batch-group');
const listDiv = group.querySelector('.group-file-list');
listDiv.innerHTML = '';
for (const file of input.files) {
const div = document.createElement('div');
div.className = 'flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-xs';
div.innerHTML = `
<span class="font-mono text-gray-700">${file.name}</span>
<span class="text-gray-400">${formatSize(file.size)}</span>
`;
listDiv.appendChild(div);
}
updateRunButton();
}
function updateRunButton() {
const groups = document.querySelectorAll('.batch-group');
let hasFiles = false;
groups.forEach(g => {
const input = g.querySelector('.group-file-input');
if (input && input.files && input.files.length > 0) hasFiles = true;
});
document.getElementById('run-batch-btn').disabled = !hasFiles;
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(1) + ' MB';
}
async function runBatch() {
const groups = document.querySelectorAll('.batch-group');
const product = getSelectedProduct();
const format = getSelectedFormat();
const btn = document.getElementById('run-batch-btn');
const progressDiv = document.getElementById('batch-progress');
const progressBar = document.getElementById('batch-progress-bar');
const progressLabel = document.getElementById('batch-progress-label');
const progressCount = document.getElementById('batch-progress-count');
const resultsDiv = document.getElementById('batch-results');
// Collect groups that have files
const validGroups = [];
groups.forEach(g => {
const input = g.querySelector('.group-file-input');
const name = g.querySelector('.group-name').value || 'Unnamed';
if (input && input.files && input.files.length > 0) {
validGroups.push({ name, files: input.files });
}
});
if (validGroups.length === 0) return;
btn.disabled = true;
btn.classList.add('opacity-50');
progressDiv.classList.remove('hidden');
progressDiv.classList.add('fade-in');
resultsDiv.innerHTML = '';
let completed = 0;
const total = validGroups.length;
progressCount.textContent = `0 / ${total}`;
for (const group of validGroups) {
progressLabel.textContent = `Processing "${group.name}"...`;
// Step 1: Upload
const uploadData = new FormData();
uploadData.append('product', product);
for (const file of group.files) {
uploadData.append('files', file);
}
let sessionId = null;
try {
const uploadResp = await fetch('/ui/upload', { method: 'POST', body: uploadData });
const uploadHtml = await uploadResp.text();
// Extract session_id from the response HTML
const match = uploadHtml.match(/classify\/([a-f0-9-]+)/);
if (!match) throw new Error('Upload failed — no session ID returned');
sessionId = match[1];
// Step 2: Auto-generate with default classifications
const genData = new FormData();
genData.append('session_id', sessionId);
genData.append('product', product);
genData.append('output_format', format);
genData.append('exclude_training', '1');
genData.append('batch_defaults', '1');
const genResp = await fetch('/ui/generate', { method: 'POST', body: genData });
const genHtml = await genResp.text();
const isSuccess = genHtml.includes('generate_success') || genHtml.includes('Files Ready') || genHtml.includes('download');
completed++;
resultsDiv.innerHTML += `
<div class="flex items-center gap-3 px-4 py-2.5 rounded-lg ${isSuccess ? 'bg-emerald-50 border border-emerald-200' : 'bg-red-50 border border-red-200'} fade-in">
<svg class="w-4 h-4 ${isSuccess ? 'text-emerald-500' : 'text-red-500'} flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${isSuccess
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>'
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>'}
</svg>
<span class="text-sm font-medium ${isSuccess ? 'text-emerald-700' : 'text-red-700'}">${group.name}</span>
${isSuccess && sessionId ? `<a href="/ui/download/${sessionId}" class="ml-auto text-xs font-medium text-brand-600 hover:text-brand-700">Download</a>` : ''}
</div>
`;
} catch (err) {
completed++;
resultsDiv.innerHTML += `
<div class="flex items-center gap-3 px-4 py-2.5 rounded-lg bg-red-50 border border-red-200 fade-in">
<svg class="w-4 h-4 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="text-sm font-medium text-red-700">${group.name}: ${err.message}</span>
</div>
`;
}
progressCount.textContent = `${completed} / ${total}`;
progressBar.style.width = (completed / total * 100) + '%';
}
progressLabel.textContent = `Batch complete — ${completed} / ${total} processed`;
btn.disabled = false;
btn.classList.remove('opacity-50');
}
// Enable drag-and-drop on group zones
document.addEventListener('dragover', (e) => {
const zone = e.target.closest('.group-drop-zone');
if (zone) {
e.preventDefault();
zone.classList.add('border-brand-400', 'bg-brand-50');
}
});
document.addEventListener('dragleave', (e) => {
const zone = e.target.closest('.group-drop-zone');
if (zone) {
zone.classList.remove('border-brand-400', 'bg-brand-50');
}
});
document.addEventListener('drop', (e) => {
const zone = e.target.closest('.group-drop-zone');
if (zone) {
e.preventDefault();
zone.classList.remove('border-brand-400', 'bg-brand-50');
const input = zone.querySelector('.group-file-input');
if (input && e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
updateGroupFiles(input);
}
}
});
</script>
{% endblock %}