Danielbrdz's picture
Upload 15 files
6528acb verified
Raw
History Blame Contribute Delete
10.6 kB
// ─── 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;
}