Sanjana Vekhande commited on
Commit ·
b051edc
1
Parent(s): 98edf11
fix: wire up real backend, /api/ prefix, blob download
Browse files- backend/main.py +3 -3
- frontend/index.html +83 -24
backend/main.py
CHANGED
|
@@ -19,7 +19,7 @@ app = FastAPI()
|
|
| 19 |
jobs: dict = {}
|
| 20 |
|
| 21 |
|
| 22 |
-
@app.post("/process")
|
| 23 |
async def process(
|
| 24 |
model_files: list[UploadFile] = File(...),
|
| 25 |
mapping_files: list[UploadFile] = File(...),
|
|
@@ -140,7 +140,7 @@ async def run_bootstrap(job_id, model_paths, mapping_paths, expr_path, species_p
|
|
| 140 |
print(traceback.format_exc(), flush=True)
|
| 141 |
|
| 142 |
|
| 143 |
-
@app.get("/progress/{job_id}")
|
| 144 |
async def progress_stream(job_id: str):
|
| 145 |
async def event_stream():
|
| 146 |
last_idx = 0
|
|
@@ -181,7 +181,7 @@ async def progress_stream(job_id: str):
|
|
| 181 |
)
|
| 182 |
|
| 183 |
|
| 184 |
-
@app.get("/download/{job_id}")
|
| 185 |
async def download(job_id: str):
|
| 186 |
job = jobs.get(job_id, {})
|
| 187 |
zip_path = job.get("output_zip")
|
|
|
|
| 19 |
jobs: dict = {}
|
| 20 |
|
| 21 |
|
| 22 |
+
@app.post("/api/process")
|
| 23 |
async def process(
|
| 24 |
model_files: list[UploadFile] = File(...),
|
| 25 |
mapping_files: list[UploadFile] = File(...),
|
|
|
|
| 140 |
print(traceback.format_exc(), flush=True)
|
| 141 |
|
| 142 |
|
| 143 |
+
@app.get("/api/progress/{job_id}")
|
| 144 |
async def progress_stream(job_id: str):
|
| 145 |
async def event_stream():
|
| 146 |
last_idx = 0
|
|
|
|
| 181 |
)
|
| 182 |
|
| 183 |
|
| 184 |
+
@app.get("/api/download/{job_id}")
|
| 185 |
async def download(job_id: str):
|
| 186 |
job = jobs.get(job_id, {})
|
| 187 |
zip_path = job.get("output_zip")
|
frontend/index.html
CHANGED
|
@@ -649,9 +649,9 @@ footer a:hover { color: var(--ink); }
|
|
| 649 |
<div class="stat-label">Species processed</div>
|
| 650 |
</div>
|
| 651 |
</div>
|
| 652 |
-
<
|
| 653 |
⬇ Download Bootstrapped ZIP
|
| 654 |
-
</
|
| 655 |
</div>
|
| 656 |
</div>
|
| 657 |
|
|
@@ -716,6 +716,8 @@ function handleFiles(type, files) {
|
|
| 716 |
el.addEventListener('drop', () => el.style.borderColor = '');
|
| 717 |
});
|
| 718 |
|
|
|
|
|
|
|
| 719 |
async function startJob() {
|
| 720 |
if (!state.model.length) { alert('Please upload at least one model file.'); return; }
|
| 721 |
if (!state.mapping.length) { alert('Please upload at least one mapping file.'); return; }
|
|
@@ -726,30 +728,87 @@ async function startJob() {
|
|
| 726 |
document.getElementById('progressSection').classList.remove('hidden');
|
| 727 |
document.getElementById('resultsSection').classList.add('hidden');
|
| 728 |
document.getElementById('logBox').innerHTML = '';
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
];
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
}
|
| 746 |
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
}
|
| 754 |
|
| 755 |
function appendLog(text, cls = '') {
|
|
|
|
| 649 |
<div class="stat-label">Species processed</div>
|
| 650 |
</div>
|
| 651 |
</div>
|
| 652 |
+
<button class="btn-download" id="downloadBtn" onclick="triggerDownload()">
|
| 653 |
⬇ Download Bootstrapped ZIP
|
| 654 |
+
</button>
|
| 655 |
</div>
|
| 656 |
</div>
|
| 657 |
|
|
|
|
| 716 |
el.addEventListener('drop', () => el.style.borderColor = '');
|
| 717 |
});
|
| 718 |
|
| 719 |
+
let activeJobId = null;
|
| 720 |
+
|
| 721 |
async function startJob() {
|
| 722 |
if (!state.model.length) { alert('Please upload at least one model file.'); return; }
|
| 723 |
if (!state.mapping.length) { alert('Please upload at least one mapping file.'); return; }
|
|
|
|
| 728 |
document.getElementById('progressSection').classList.remove('hidden');
|
| 729 |
document.getElementById('resultsSection').classList.add('hidden');
|
| 730 |
document.getElementById('logBox').innerHTML = '';
|
| 731 |
+
document.getElementById('progressBar').style.width = '0%';
|
| 732 |
+
document.getElementById('progressPct').textContent = '0%';
|
| 733 |
+
document.getElementById('progressLabel').textContent = 'Starting…';
|
| 734 |
+
|
| 735 |
+
// Build form data
|
| 736 |
+
const fd = new FormData();
|
| 737 |
+
state.model.forEach(f => fd.append('model_files', f));
|
| 738 |
+
state.mapping.forEach(f => fd.append('mapping_files', f));
|
| 739 |
+
fd.append('expr_file', state.expr[0]);
|
| 740 |
+
fd.append('batch_count', document.getElementById('batchSlider').value);
|
| 741 |
+
|
| 742 |
+
// Submit job
|
| 743 |
+
let jobId;
|
| 744 |
+
try {
|
| 745 |
+
const res = await fetch('/api/process', { method: 'POST', body: fd });
|
| 746 |
+
if (!res.ok) throw new Error((await res.json()).detail || res.statusText);
|
| 747 |
+
jobId = (await res.json()).job_id;
|
| 748 |
+
activeJobId = jobId;
|
| 749 |
+
appendLog('Files received. Starting bootstrap…', 'info');
|
| 750 |
+
} catch (e) {
|
| 751 |
+
appendLog('Error: ' + e.message, 'err');
|
| 752 |
+
document.getElementById('runBtn').disabled = false;
|
| 753 |
+
return;
|
| 754 |
}
|
| 755 |
|
| 756 |
+
// Stream progress via SSE
|
| 757 |
+
const sse = new EventSource(`/api/progress/${jobId}`);
|
| 758 |
+
|
| 759 |
+
sse.onmessage = e => {
|
| 760 |
+
const d = JSON.parse(e.data);
|
| 761 |
+
|
| 762 |
+
if (d.message) appendLog(d.message, d.type || '');
|
| 763 |
+
|
| 764 |
+
if (d.progress !== undefined) {
|
| 765 |
+
document.getElementById('progressBar').style.width = d.progress + '%';
|
| 766 |
+
document.getElementById('progressPct').textContent = d.progress + '%';
|
| 767 |
+
if (d.label) document.getElementById('progressLabel').textContent = d.label;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
if (d.status === 'done') {
|
| 771 |
+
sse.close();
|
| 772 |
+
document.getElementById('statFiles').textContent = (d.file_count || '—').toLocaleString();
|
| 773 |
+
document.getElementById('statSpecies').textContent = d.species_count || '—';
|
| 774 |
+
document.getElementById('resultsSection').classList.remove('hidden');
|
| 775 |
+
document.getElementById('runBtn').disabled = false;
|
| 776 |
+
// Store job id for download
|
| 777 |
+
document.getElementById('downloadBtn').dataset.jobId = jobId;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
if (d.status === 'error') {
|
| 781 |
+
sse.close();
|
| 782 |
+
appendLog('Fatal error: ' + d.error, 'err');
|
| 783 |
+
document.getElementById('runBtn').disabled = false;
|
| 784 |
+
}
|
| 785 |
+
};
|
| 786 |
+
|
| 787 |
+
sse.onerror = () => {
|
| 788 |
+
sse.close();
|
| 789 |
+
appendLog('Connection lost. Check logs.', 'err');
|
| 790 |
+
document.getElementById('runBtn').disabled = false;
|
| 791 |
+
};
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
async function triggerDownload() {
|
| 795 |
+
const jobId = document.getElementById('downloadBtn').dataset.jobId;
|
| 796 |
+
if (!jobId) return;
|
| 797 |
+
try {
|
| 798 |
+
const res = await fetch(`/api/download/${jobId}`);
|
| 799 |
+
if (!res.ok) throw new Error('Download failed');
|
| 800 |
+
const blob = await res.blob();
|
| 801 |
+
const url = URL.createObjectURL(blob);
|
| 802 |
+
const a = document.createElement('a');
|
| 803 |
+
a.href = url;
|
| 804 |
+
a.download = 'geneExpr_bootstrapped.zip';
|
| 805 |
+
document.body.appendChild(a);
|
| 806 |
+
a.click();
|
| 807 |
+
document.body.removeChild(a);
|
| 808 |
+
URL.revokeObjectURL(url);
|
| 809 |
+
} catch (e) {
|
| 810 |
+
alert('Download failed: ' + e.message);
|
| 811 |
+
}
|
| 812 |
}
|
| 813 |
|
| 814 |
function appendLog(text, cls = '') {
|