/* ═══════════════════════════════════════════════════════════════════════════ Biopesticide-AI — Frontend Logic v2 Fixes: chart containers now have explicit heights, charts render properly. Adds: wet-lab simulation tab with Monte Carlo results rendering. ═══════════════════════════════════════════════════════════════════════════ */ let lastResult = null; let charts = { efficacy: null, halflife: null }; document.addEventListener('DOMContentLoaded', () => { fetchStatus(); setupExamples(); setupTabs(); setupExport(); setupPestCards(); document.getElementById('design-btn').addEventListener('click', runDesign); document.getElementById('simulate-btn').addEventListener('click', runSimulation); // Custom input mode: Ctrl+Enter to submit const pestReport = document.getElementById('pest-report'); if (pestReport) { pestReport.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'Enter') runDesign(); }); } }); function setupPestCards() { document.querySelectorAll('.pest-card').forEach(card => { card.addEventListener('click', (e) => { if (card.id === 'custom-input-btn') { // Reveal the custom input card document.getElementById('custom-input-card').style.display = 'block'; document.getElementById('card-mode-controls').style.display = 'none'; document.getElementById('pest-report').focus(); return; } // Pest card clicked: run design immediately with this species const species = card.dataset.species; const crop = card.dataset.crop; const prompt = card.dataset.prompt; // Highlight the selected card document.querySelectorAll('.pest-card').forEach(c => c.classList.remove('selected')); card.classList.add('selected'); runDesignFromCard(species, crop, prompt); }); }); } async function runDesignFromCard(species, crop, prompt) { const topK = parseInt(document.getElementById('top-k-card').value) || 10; const loadingState = document.getElementById('loading-state'); const errorState = document.getElementById('error-state'); const resultsSection = document.getElementById('results-section'); loadingState.style.display = 'block'; errorState.style.display = 'none'; resultsSection.style.display = 'none'; const loadingSteps = [ `Loading ${species.replace(/_/g, ' ')} transcripts...`, 'Tiling into 200-nt dsRNA precursors...', 'Dicing into 21-nt siRNAs...', 'Scoring efficacy with dilated CNN...', 'Checking off-target risk against 14 species...', 'Predicting environmental half-life with PINN...', 'Ranking candidates...', 'Generating safety cards with Llama 3.2 3B...', 'Generating regulatory memo...' ]; let stepIdx = 0; const loadingInterval = setInterval(() => { document.getElementById('loading-subtext').textContent = loadingSteps[stepIdx % loadingSteps.length]; stepIdx++; }, 800); try { const resp = await fetch('/api/design', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_text: prompt, top_k: topK, pest_species: species, // bypasses LLM parsing }), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || `HTTP ${resp.status}`); } const result = await resp.json(); lastResult = result; loadingState.style.display = 'none'; resultsSection.style.display = 'block'; renderStats(result); renderPestReport(result.pest_report); renderCandidates(result.candidates); renderCharts(result.candidates); renderOffTargetHeatmap(result.candidates); renderSafetyCards(result.safety_cards); renderRegulatoryMemo(result.regulatory_memo); document.getElementById('simulation-results').innerHTML = ''; resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) { loadingState.style.display = 'none'; errorState.style.display = 'block'; document.getElementById('error-text').textContent = e.message; } finally { clearInterval(loadingInterval); } } async function fetchStatus() { try { const resp = await fetch('/api/status'); const data = await resp.json(); const dot = document.getElementById('status-dot'); const text = document.getElementById('status-text'); if (data.llm_active) { dot.className = 'status-dot active'; text.textContent = data.llm_status; } else { dot.className = 'status-dot degraded'; text.textContent = data.llm_status; } } catch (e) { document.getElementById('status-dot').className = 'status-dot error'; document.getElementById('status-text').textContent = 'Backend offline'; } } function setupExamples() { document.querySelectorAll('.example-chip').forEach(chip => { chip.addEventListener('click', () => { document.getElementById('pest-report').value = chip.dataset.text; document.getElementById('pest-report').focus(); }); }); } function setupTabs() { document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); document.getElementById('tab-' + tab.dataset.tab).classList.add('active'); }); }); } async function runDesign() { const userText = document.getElementById('pest-report').value.trim(); const topK = parseInt(document.getElementById('top-k').value) || 10; if (!userText) { alert('Please describe your pest problem first.'); return; } const btn = document.getElementById('design-btn'); const loadingState = document.getElementById('loading-state'); const errorState = document.getElementById('error-state'); const resultsSection = document.getElementById('results-section'); btn.disabled = true; btn.querySelector('.design-btn-text').textContent = 'Designing...'; loadingState.style.display = 'block'; errorState.style.display = 'none'; resultsSection.style.display = 'none'; const loadingSteps = [ 'Parsing pest report with Llama 3.2 3B...', 'Tiling pest transcripts into 200-nt precursors...', 'Dicing into 21-nt siRNAs...', 'Scoring efficacy with dilated CNN...', 'Checking off-target risk against 14 species...', 'Predicting environmental half-life with PINN...', 'Ranking candidates...', 'Generating safety cards with Llama 3.2 3B...', 'Generating regulatory memo...' ]; let stepIdx = 0; const loadingInterval = setInterval(() => { document.getElementById('loading-subtext').textContent = loadingSteps[stepIdx % loadingSteps.length]; stepIdx++; }, 1500); try { const resp = await fetch('/api/design', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_text: userText, top_k: topK }), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.detail || `HTTP ${resp.status}`); } const result = await resp.json(); lastResult = result; loadingState.style.display = 'none'; resultsSection.style.display = 'block'; renderStats(result); renderPestReport(result.pest_report); renderCandidates(result.candidates); renderCharts(result.candidates); renderOffTargetHeatmap(result.candidates); renderSafetyCards(result.safety_cards); renderRegulatoryMemo(result.regulatory_memo); // Reset simulation results document.getElementById('simulation-results').innerHTML = ''; resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) { loadingState.style.display = 'none'; errorState.style.display = 'block'; document.getElementById('error-text').textContent = e.message; } finally { clearInterval(loadingInterval); btn.disabled = false; btn.querySelector('.design-btn-text').textContent = 'Design dsRNA candidates'; } } function renderStats(result) { const strip = document.getElementById('stats-strip'); const stats = [ { num: `${result.elapsed_seconds}s`, lbl: 'Design loop' }, { num: result.n_transcripts, lbl: 'Transcripts' }, { num: result.n_precursors, lbl: 'Precursors' }, { num: result.n_sirnas, lbl: 'siRNAs scored' }, { num: `$${result.total_cost_estimate.toFixed(4)}`, lbl: 'Est. cost' }, ]; strip.innerHTML = stats.map(s => `
No pest report parsed.
'; return; } const species = pest.pest_species || pest.species || 'unknown'; const crop = pest.crop || 'unknown'; const severity = pest.severity || 'unknown'; const location = pest.location || 'unknown'; const notes = pest.notes || ''; card.innerHTML = `| Target species | ${species} |
| Crop | ${crop} |
| Severity | ${severity} |
| Location | ${location} |
| Notes | ${notes} |
No candidates generated.
'; return; } grid.innerHTML = candidates.map((c, i) => { const score = c.final_score || 0; let rankClass = 'rank-good'; if (score < 0.3) rankClass = 'rank-warn'; if (score < 0.15) rankClass = 'rank-poor'; const hl = c.half_life_hours || 0; const hlDays = (hl / 24).toFixed(1); const effPct = ((c.efficacy || 0) * 100).toFixed(1); const scorePct = (score * 100).toFixed(1); return `| # | '; allSpecies.forEach(sp => { const shortName = sp.split('_').map(w => w[0].toUpperCase() + w.slice(1)).join(' '); html += `${shortName} | `; }); html += '
|---|---|
| ${i + 1} | `; allSpecies.forEach(sp => { const val = perSp[sp] || 0; let bg = '#ffffff'; if (val > 0.1) bg = '#964039'; else if (val > 0.05) bg = '#b69045'; else if (val > 0.01) bg = '#fef6e4'; const text = val > 0 ? val.toFixed(3) : '·'; const textColor = val > 0.05 ? '#ffffff' : '#2d2d2d'; html += `${text} | `; }); html += '
No safety cards generated.
'; return; } if (typeof safetyCards === 'string') { container.innerHTML = `No safety cards generated.
'; } function renderRegulatoryMemo(memo) { const container = document.getElementById('regulatory-memo-content'); if (!memo) { container.innerHTML = 'No regulatory memo generated.
'; return; } container.innerHTML = simpleMarkdown(memo); } // ─── Wet-lab simulation ───────────────────────────────────────────────────── async function runSimulation() { if (!lastResult || !lastResult.candidates || lastResult.candidates.length === 0) { alert('Run the design pipeline first.'); return; } const btn = document.getElementById('simulate-btn'); const resultsDiv = document.getElementById('simulation-results'); btn.disabled = true; btn.textContent = 'Running 1000-trial Monte Carlo simulation...'; resultsDiv.innerHTML = 'Simulating cellular knockdown pipeline...
'; try { const resp = await fetch('/api/simulate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ candidates: lastResult.candidates.slice(0, 5), pest_species: lastResult.pest_report?.pest_species || 'nilaparvata_lugens', }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const result = await resp.json(); renderSimulationResults(result); } catch (e) { resultsDiv.innerHTML = `${e.message}
No simulation results.
'; return; } let html = '| # | siRNA | Mean KD | 95% CI | P(KD>70%) | Delivery | Uptake | RISC | Tier |
|---|---|---|---|---|---|---|---|---|
| ${i + 1} | ${t.sirna_seq} | ${meanKd}% | [${ciLow}, ${ciHigh}]% | ${pGood}% | ${delivery}% | ${uptake}% | ${risc}% | ${tier} |
${s.desc}
$1');
html = html.replace(/\n\n/g, '');
html = html.replace(/\n/g, '
');
html = '
' + html + '
'; html = html.replace(/<\/p>/g, ''); return html; }