/* ============================================================ Alopecia Digital Twin — app.js ============================================================ */ document.addEventListener('DOMContentLoaded', () => { // ── Data store let allPapers = []; let filteredPapers = []; let charts = {}; let currentPage = 1; const PAGE_SIZE = 20; // ── Tab Nav const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`tab-${btn.dataset.tab}`).classList.add('active'); // Lazy-render charts when Stats tab opens if (btn.dataset.tab === 'stats' && allPapers.length) renderCharts(); }); }); // ── Element refs (Digital Twin) const diseaseSelect = document.getElementById('disease-select'); const modalitySelect = document.getElementById('modality-select'); const deliverySelect = document.getElementById('delivery-select'); const synthesizeBtn = document.getElementById('btn-synthesize'); const visPlaceholder = document.getElementById('vis-placeholder'); const visActive = document.getElementById('vis-active'); const mechanismImage = document.getElementById('mechanism-image'); const activePathway = document.getElementById('active-pathway'); const mechanismDesc = document.getElementById('mechanism-description'); const efficacyBar = document.getElementById('efficacy-bar'); const bioBar = document.getElementById('bio-bar'); const evidenceBar = document.getElementById('evidence-bar'); const insightsList = document.getElementById('insights-list'); const paperCountEl = document.getElementById('paper-count'); const lastUpdatedEl = document.getElementById('last-updated'); const relatedCount = document.getElementById('related-count'); const relatedNum = document.getElementById('related-num'); const insightFilter = document.getElementById('insight-disease-filter'); // ── Digital Twin control logic diseaseSelect.addEventListener('change', e => { const val = e.target.value; modalitySelect.disabled = !val; if (!val) { modalitySelect.value = ''; deliverySelect.disabled = true; deliverySelect.value = ''; synthesizeBtn.disabled = true; relatedCount.classList.add('hidden'); } }); modalitySelect.addEventListener('change', e => { deliverySelect.disabled = !e.target.value; if (!e.target.value) { deliverySelect.value = ''; synthesizeBtn.disabled = true; } }); deliverySelect.addEventListener('change', e => { synthesizeBtn.disabled = !e.target.value; }); // ── Synthesize synthesizeBtn.addEventListener('click', () => { const disease = diseaseSelect.value; const modality = modalitySelect.value; const delivery = deliverySelect.value; visPlaceholder.classList.add('hidden'); visActive.classList.remove('hidden'); const config = getTwinConfig(disease, modality, delivery); activePathway.textContent = config.pathway; mechanismDesc.innerHTML = `${config.desc}
${config.descKo}`; mechanismImage.style.opacity = '0'; setTimeout(() => { mechanismImage.src = config.image; mechanismImage.onload = () => { mechanismImage.style.opacity = '0.85'; }; mechanismImage.onerror = () => { mechanismImage.style.opacity = '0'; }; }, 400); // Animate bars setTimeout(() => { efficacyBar.style.width = config.efficacy + '%'; efficacyBar.textContent = config.efficacy + '%'; bioBar.style.width = config.bio + '%'; bioBar.textContent = config.bio + '%'; }, 600); // Count related papers const related = allPapers.filter(p => p.disease === disease && (modality === 'Other / Unknown' || p.modality === modality) ); const pct = allPapers.length ? Math.round((related.length / allPapers.length) * 100) : 0; evidenceBar.style.width = Math.min(pct * 4, 100) + '%'; evidenceBar.textContent = related.length + ' papers'; relatedNum.textContent = related.length; relatedCount.classList.remove('hidden'); }); function getTwinConfig(disease, modality, delivery) { const modalityMap = { 'Microneedles': { efficacy: 91, bio: 95 }, 'Stem Cells & Exosomes': { efficacy: 88, bio: 82 }, 'Natural Extracts': { efficacy: 72, bio: 68 }, 'Nanoparticles/Nanogels': { efficacy: 85, bio: 88 }, 'Other / Unknown': { efficacy: 78, bio: 75 }, }; const stats = modalityMap[modality] || { efficacy: 78, bio: 75 }; if (disease === 'Androgenetic Alopecia') return { pathway: 'Wnt/β-catenin Upregulation (Wnt/β-카테닌 신호 활성화)', desc: `Utilizing ${modality} via ${delivery}, the treatment bypasses DHT binding barriers, stimulating Dermal Papilla Cells and driving hair follicles back into the Anagen growth phase.`, descKo: `${modality}을(를) ${delivery} 경로로 전달하여 DHT 결합 차단막을 우회합니다. 진피유두세포(Dermal Papilla Cell)를 자극해 모낭을 다시 성장기(Anagen Phase)로 전환시킵니다. Wnt 경로 활성화는 β-카테닌의 핵 내 이동을 촉진하여 모발 생성 관련 유전자 전사를 유도합니다.`, image: 'images/wnt_pathway.png', ...stats }; if (disease === 'Alopecia Areata') return { pathway: 'JAK-STAT Pathway Inhibition (JAK-STAT 경로 억제)', desc: `Application of ${modality} (${delivery}) locally suppresses aberrant CD8+ T-cell immune responses, disabling the inflammatory signal loop that attacks the hair bulb.`, descKo: `${modality}(${delivery})을(를) 국소 적용하여 비정상적인 CD8+ T세포 면역 반응을 억제합니다. 모낭의 면역 특권(Immune Privilege)이 무너지면서 발생하는 자가면역 공격 신호 루프를 JAK1/2 억제를 통해 차단하고, 모낭 주변 염증을 완화합니다.`, image: 'images/jak_stat_pathway.png', ...stats }; return { pathway: 'Anti-Apoptosis / ROS Scavenging (세포자멸사 억제 / 활성산소 제거)', desc: `${modality} delivered through ${delivery} mitigates severe oxidative stress and prevents p53-mediated apoptosis in rapidly dividing matrix cells during toxic exposure.`, descKo: `${modality}을(를) ${delivery}로 전달하여 항암 독성으로 인한 극심한 산화 스트레스를 완화합니다. 급속 분열 중인 모기질세포(Matrix Cell)에서 p53 매개 세포자멸사(Apoptosis)를 억제하고, 활성산소종(ROS)을 제거하여 모낭 세포 생존율을 높입니다.`, image: 'images/apoptosis_pathway.png', ...stats }; } // ── Insight filter insightFilter.addEventListener('change', () => { updateInsights(currentData); }); let currentData = null; function updateInsights(data) { if (!data) return; currentData = data; const filterVal = insightFilter.value; const papers = filterVal ? data.papers.filter(p => p.disease === filterVal) : data.papers; const recent = papers.slice(0, 12); insightsList.innerHTML = ''; if (!recent.length) { insightsList.innerHTML = '
No papers found.
'; return; } recent.forEach(paper => { const card = document.createElement('div'); card.className = 'insight-card'; const abbr = paper.disease.includes('Androgenetic') ? 'AGA Target' : paper.disease.includes('Areata') ? 'AA Target' : 'CIA Target'; card.innerHTML = `
${abbr}
${paper.title}
${paper.modality} ${paper.pubYear || 'Recent'}
`; insightsList.appendChild(card); }); } // ── Chart rendering const CHART_COLORS = ['#6699ff', '#a855f7', '#22d3ee', '#34d399', '#fbbf24', '#f87171', '#818cf8']; const chartDefaults = { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#7b82b0', font: { size: 11 }, boxWidth: 12 } } } }; function renderCharts() { if (!currentData?.stats) return; const s = currentData.stats; renderDoughnut('chart-disease', s.by_disease); renderDoughnut('chart-modality', s.by_modality); renderBar('chart-trend', s.trend_by_year, 'Papers Published'); renderDoughnut('chart-aga-modality', s.disease_modality?.['Androgenetic Alopecia'] || {}); renderDoughnut('chart-aa-modality', s.disease_modality?.['Alopecia Areata'] || {}); } function renderDoughnut(canvasId, dataObj) { const canvas = document.getElementById(canvasId); if (!canvas) return; if (charts[canvasId]) charts[canvasId].destroy(); const labels = Object.keys(dataObj); const values = Object.values(dataObj); charts[canvasId] = new Chart(canvas, { type: 'doughnut', data: { labels, datasets: [{ data: values, backgroundColor: CHART_COLORS, borderColor: 'rgba(255,255,255,0.05)', borderWidth: 1 }] }, options: { ...chartDefaults, cutout: '60%' } }); } function renderBar(canvasId, dataObj, label) { const canvas = document.getElementById(canvasId); if (!canvas) return; if (charts[canvasId]) charts[canvasId].destroy(); const labels = Object.keys(dataObj); const values = Object.values(dataObj); charts[canvasId] = new Chart(canvas, { type: 'bar', data: { labels, datasets: [{ label, data: values, backgroundColor: 'rgba(102,153,255,0.4)', borderColor: '#6699ff', borderWidth: 1, borderRadius: 6 }] }, options: { ...chartDefaults, scales: { x: { ticks: { color: '#7b82b0', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,0.04)' } }, y: { ticks: { color: '#7b82b0', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,0.06)' }, beginAtZero: true } } } }); } // ── Paper Library const searchInput = document.getElementById('search-input'); const filterDisease = document.getElementById('filter-disease'); const filterModality = document.getElementById('filter-modality'); const filterYear = document.getElementById('filter-year'); const papersTbody = document.getElementById('papers-tbody'); const tableCountEl = document.getElementById('table-count'); const btnPrev = document.getElementById('btn-prev'); const btnNext = document.getElementById('btn-next'); const pageInfo = document.getElementById('page-info'); let sortCol = 'pubYear'; let sortAsc = false; function populateYearFilter(papers) { const years = [...new Set(papers.map(p => p.pubYear).filter(y => y))].sort((a, b) => b - a); years.forEach(y => { const opt = document.createElement('option'); opt.value = y; opt.textContent = y; filterYear.appendChild(opt); }); } function applyFilters() { const q = searchInput.value.toLowerCase(); const dis = filterDisease.value; const mod = filterModality.value; const yr = filterYear.value; filteredPapers = allPapers.filter(p => { if (dis && p.disease !== dis) return false; if (mod && p.modality !== mod) return false; if (yr && p.pubYear !== yr) return false; if (q && !(p.title.toLowerCase().includes(q) || (p.abstract || '').toLowerCase().includes(q))) return false; return true; }); filteredPapers.sort((a, b) => { const va = a[sortCol] || ''; const vb = b[sortCol] || ''; return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va); }); currentPage = 1; renderTable(); } function makePdfFilename(title) { // Windows에서 금지된 문자만 제거. 나머지는 agent1이 저장한 실제 파일명과 동일하게 유지. // agent1_fetcher.py: 특수문자 제거 없이 {title}.pdf 그대로 저장 const illegal = /[\\/:*?"<>|]/g; return title.replace(illegal, '').trim() + '.pdf'; } function downloadPdf(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); } function renderTable() { const total = filteredPapers.length; const pages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const start = (currentPage - 1) * PAGE_SIZE; const slice = filteredPapers.slice(start, start + PAGE_SIZE); tableCountEl.textContent = total; pageInfo.textContent = `Page ${currentPage} of ${pages}`; btnPrev.disabled = currentPage <= 1; btnNext.disabled = currentPage >= pages; papersTbody.innerHTML = ''; if (!slice.length) { papersTbody.innerHTML = 'No papers match your filter.'; return; } slice.forEach(p => { const tr = document.createElement('tr'); const diseaseClass = p.disease.includes('Androgenetic') ? 'disease-aga' : p.disease.includes('Areata') ? 'disease-aa' : 'disease-cia'; const diseaseLabel = p.disease.includes('Androgenetic') ? 'AGA (남성형 탈모)' : p.disease.includes('Areata') ? 'AA (원형 탈모)' : 'CIA (항암 탈모)'; const pdfFilename = makePdfFilename(p.title); const pdfUrl = `../papers/${encodeURIComponent(pdfFilename)}`; tr.style.cursor = 'pointer'; tr.title = 'PDF 다운로드 클릭 (Click to download PDF)'; tr.innerHTML = ` ${p.pubYear || '—'} ${p.title} 📄 ${diseaseLabel} ${p.modality} ${p.mechanism || '—'}`; tr.addEventListener('click', () => downloadPdf(pdfUrl, pdfFilename)); papersTbody.appendChild(tr); }); } // Sorting document.querySelectorAll('.sortable').forEach(th => { th.addEventListener('click', () => { const col = th.dataset.col; if (sortCol === col) sortAsc = !sortAsc; else { sortCol = col; sortAsc = true; } applyFilters(); }); }); // Filters [searchInput, filterDisease, filterModality, filterYear].forEach(el => el.addEventListener('input', applyFilters)); btnPrev.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderTable(); } }); btnNext.addEventListener('click', () => { const pages = Math.ceil(filteredPapers.length / PAGE_SIZE); if (currentPage < pages) { currentPage++; renderTable(); } }); // ── Data Polling async function fetchData() { try { const res = await fetch(`../analysis_results.json?t=${Date.now()}`); if (!res.ok) return; const data = await res.json(); if (!data || !data.papers) return; allPapers = data.papers; filteredPapers = [...allPapers]; // Header paperCountEl.textContent = data.total_papers; const updated = data.last_updated ? new Date(data.last_updated).toLocaleString('ko-KR', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—'; lastUpdatedEl.textContent = updated; // Year filter filterYear.innerHTML = ''; populateYearFilter(allPapers); // Insights updateInsights(data); // Paper table applyFilters(); // Charts (only if Stats tab is active) if (document.getElementById('tab-stats').classList.contains('active')) renderCharts(); } catch (err) { console.warn('Waiting for analysis_results.json...', err); } } fetchData(); setInterval(fetchData, 10000); });