Spaces:
Running
Running
| // ─── shared.js ─── | |
| // Centralized logic for Navbar, API calls, Toast notifications, and SHAP rendering | |
| // 1. Navbar Injection | |
| const navHTML = ` | |
| <header class="top-nav"> | |
| <div class="top-nav-logo"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="logo-pulse"> | |
| <path d="M22 12h-4l-3 9L9 3l-3 9H2" /> | |
| </svg> | |
| <span class="logo-title">MedBarcenas Hemodiálisis Beta 4</span> | |
| </div> | |
| <nav class="top-nav-links"> | |
| <a href="index.html" class="top-nav-link" id="nav-mort"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg> | |
| Mortalidad | |
| </a> | |
| <a href="peso_seco.html" class="top-nav-link" id="nav-peso"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M12 6v14"/><path d="M12 20H4"/><path d="M12 20h8"/><path d="M20 9l-4 3H8L4 9"/></svg> | |
| Peso Seco | |
| </a> | |
| <a href="compatibilidad.html" class="top-nav-link" id="nav-compat"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 10.5C3 12 3 14.5 4.5 16s4-1.5 5.5-3m1.5-1.5c1.5-1.5 4-1.5 5.5 0s1.5 4 0 5.5-4-1.5-5.5-3m0-1.5c-1-1-1-2.5 0-3.5s2.5-1 3.5 0M9 13.5c-1 1-2.5 1-3.5 0s-1-2.5 0-3.5"/></svg> | |
| Trasplante | |
| </a> | |
| <a href="acceso_vascular.html" class="top-nav-link" id="nav-acc"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 12V4H6v8a6 6 0 0 0 12 0Z"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="9" y1="22" x2="15" y2="22"/></svg> | |
| Acceso Vascular | |
| </a> | |
| <a href="idh_checkpoint.html" class="top-nav-link" id="nav-idh"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> | |
| IDH Checkpoint | |
| </a> | |
| <a href="orquestador.html" class="top-nav-link" id="nav-orq"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg> | |
| Orquestador | |
| </a> | |
| <a href="bitacora.html" class="top-nav-link" id="nav-bitacora"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> | |
| Bitácora | |
| </a> | |
| </nav> | |
| </header> | |
| `; | |
| function injectNavbar(activeId) { | |
| const container = document.getElementById('navbar-container'); | |
| if (container) { | |
| container.innerHTML = navHTML; | |
| if (activeId) { | |
| const activeLink = document.getElementById(activeId); | |
| if (activeLink) activeLink.classList.add('active'); | |
| } | |
| } | |
| } | |
| // 2. Toast Notifications | |
| function showToast(message, type = 'error') { | |
| let toastContainer = document.getElementById('toast-container'); | |
| if (!toastContainer) { | |
| toastContainer = document.createElement('div'); | |
| toastContainer.id = 'toast-container'; | |
| document.body.appendChild(toastContainer); | |
| } | |
| const toast = document.createElement('div'); | |
| toast.className = `toast toast-${type}`; | |
| const icon = type === 'error' ? | |
| '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' : | |
| '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'; | |
| toast.innerHTML = ` | |
| ${icon} | |
| <span>${message}</span> | |
| `; | |
| toastContainer.appendChild(toast); | |
| // Trigger animation | |
| setTimeout(() => toast.classList.add('show'), 10); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 4000); | |
| } | |
| // 3. API Client | |
| async function apiClient(endpoint, payload, loaderBtnId) { | |
| const btn = loaderBtnId ? document.getElementById(loaderBtnId) : null; | |
| let originalHtml = ''; | |
| if (btn) { | |
| originalHtml = btn.innerHTML; | |
| btn.classList.add('loading'); | |
| btn.innerHTML = ` | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin"> | |
| <path d="M21 12a9 9 0 11-6.219-8.56"/> | |
| </svg> | |
| Calculando... | |
| `; | |
| } | |
| try { | |
| // En Gradio nativo, la API vive en /gradio_api/run/nombre_api | |
| const apiName = endpoint.replace('/api/', 'api_'); | |
| // USAMOS LA URL ABSOLUTA DEL ESPACIO GRADIO PROXY PARA PERMITIR QUE EL FRONTEND VIVA EN UN STATIC SPACE | |
| const proxyBaseUrl = 'https://danielbrdz-medbarcenas-beta-4.hf.space'; | |
| const gradioEndpoint = `${proxyBaseUrl}/gradio_api/run/${apiName}`; | |
| const response = await fetch(gradioEndpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| // Gradio exige envolver el payload en {"data": [ ... ]} | |
| body: JSON.stringify({ data: [payload] }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const rawData = await response.json(); | |
| // Gradio devuelve la respuesta en {"data": [ ... ]} | |
| let finalData = rawData; | |
| if (rawData.data && Array.isArray(rawData.data) && rawData.data.length > 0) { | |
| finalData = rawData.data[0]; | |
| } | |
| if (finalData.error) { | |
| throw new Error(finalData.error); | |
| } | |
| return finalData; | |
| } catch (err) { | |
| showToast('Error de conexión: ' + err.message, 'error'); | |
| throw err; | |
| } finally { | |
| if (btn) { | |
| btn.classList.remove('loading'); | |
| btn.innerHTML = originalHtml; | |
| } | |
| } | |
| } | |
| // 4. SHAP Explanations Renderer | |
| function renderSharedExplanations(explanations, containerId, customTitle, customSubtitle, customLabels, customColorUp = '#f0435a', customColorDown = '#16D391', reverseArrowDirection = false) { | |
| const container = document.getElementById(containerId); | |
| if (!explanations || explanations.length === 0) { | |
| if(container) container.innerHTML = ''; | |
| return; | |
| } | |
| const maxShap = Math.max(...explanations.map(e => Math.abs(e.shap))); | |
| const title = customTitle || '¿Por qué este resultado?'; | |
| const subtitle = customSubtitle || 'Factores que más influyeron en la predicción (ordenados por importancia)'; | |
| let html = ` | |
| <div class="explanations-header"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/> | |
| </svg> | |
| <span>${title}</span> | |
| </div> | |
| <div class="explanations-subtitle">${subtitle}</div> | |
| `; | |
| explanations.forEach((exp, i) => { | |
| let isPositive = exp.direction === 'up'; | |
| let color = isPositive ? customColorUp : customColorDown; | |
| let arrow = isPositive ? '▲' : '▽'; | |
| if (reverseArrowDirection) { | |
| arrow = isPositive ? '▽' : '▲'; | |
| } | |
| let labelText = isPositive ? 'Aumenta riesgo' : 'Reduce riesgo'; | |
| if (customLabels) { | |
| labelText = isPositive ? customLabels.up : customLabels.down; | |
| } else { | |
| if (exp.label) labelText = exp.label; | |
| } | |
| const barPct = Math.min((Math.abs(exp.shap) / maxShap) * 100, 100); | |
| html += ` | |
| <div class="explanation-card" onclick="this.classList.toggle('expanded')"> | |
| <div class="explanation-main"> | |
| <div class="explanation-rank">${i + 1}</div> | |
| <div class="explanation-content"> | |
| <div class="explanation-title"> | |
| <span class="explanation-feature">${exp.feature}</span> | |
| <span class="explanation-direction" style="color:${color}">${arrow} ${labelText}</span> | |
| </div> | |
| <div class="explanation-bar-track"> | |
| <div class="explanation-bar-fill" style="width:${barPct}%;background:${color}"></div> | |
| </div> | |
| </div> | |
| <div class="explanation-chevron">▾</div> | |
| </div> | |
| <div class="explanation-detail"> | |
| <p>${exp.explanation}</p> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| html += ` | |
| <div class="medical-disclaimer"> | |
| <div class="medical-disclaimer-icon-wrapper"> | |
| <svg class="medical-disclaimer-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/> | |
| <line x1="12" y1="9" x2="12" y2="13"/> | |
| <line x1="12" y1="17" x2="12.01" y2="17"/> | |
| </svg> | |
| </div> | |
| <div class="medical-disclaimer-content"> | |
| <div class="medical-disclaimer-title">Aviso Importante</div> | |
| <div class="medical-disclaimer-text"> | |
| Esta herramienta es únicamente un recurso de apoyo clínico. Los algoritmos presentados <strong>NO constituyen un diagnóstico médico</strong> ni reemplazan el juicio del médico tratante. | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| container.innerHTML = html; | |
| } | |