/* ============================================================
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 = '