Download web/diagnosis.html from CYV28/RetinAI: direct link, hf CLI and curl.
- Browser
- Download file 73 kB
-
https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/diagnosis.html
- Command line
-
hf download hf://spaces/CYV28/RetinAI/web/diagnosis.html
-
curl -L -o diagnosis.html https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/diagnosis.html
73 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
| <meta name="description" content=""> | |
| <meta name="author" content=""> | |
| <link rel="icon" href="favicon.ico"> | |
| <title>Diagnóstico IA</title> | |
| <link rel="stylesheet" href="css/simplebar.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <link rel="stylesheet" href="css/select2.css"> | |
| <link rel="stylesheet" href="css/dropzone.css"> | |
| <link rel="stylesheet" href="css/uppy.min.css"> | |
| <link rel="stylesheet" href="css/jquery.steps.css"> | |
| <link rel="stylesheet" href="css/jquery.timepicker.css"> | |
| <link rel="stylesheet" href="css/quill.snow.css"> | |
| <link rel="stylesheet" href="css/daterangepicker.css"> | |
| <script> | |
| (function() { | |
| var dark = localStorage.getItem('theme') === 'dark'; | |
| document.write( | |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'dark' : 'light') + '.css" id="' + (dark ? 'dark' : 'light') + 'Theme">' + | |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'light' : 'dark') + '.css" id="' + (dark ? 'light' : 'dark') + 'Theme" disabled>' | |
| ); | |
| })(); | |
| </script> | |
| <style> | |
| .upload-zone { | |
| border: 3px dashed #dee2e6; | |
| border-radius: 12px; | |
| padding: 3rem 2rem; | |
| text-align: center; | |
| background: #f8f9fa; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| min-height: 300px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| user-select: none; | |
| } | |
| .upload-zone:hover { | |
| background: #f3f0ff; | |
| border-color: #6c757d; | |
| transform: translateY(-2px); | |
| } | |
| .upload-zone.dragover { | |
| border-color: #28a745; | |
| background: #f0fff4; | |
| transform: scale(1.02); | |
| } | |
| .upload-zone * { | |
| pointer-events: none; | |
| } | |
| .image-preview { | |
| max-width: 100%; | |
| max-height: 400px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| .analysis-card { | |
| color: white; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .result-positive { | |
| background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%); | |
| border-left: 4px solid #dc3545; | |
| } | |
| .result-negative { | |
| background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); | |
| border-left: 4px solid #28a745; | |
| } | |
| .confidence-meter { | |
| height: 8px; | |
| background: #e9ecef; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .confidence-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #28a745, #ffc107, #dc3545); | |
| transition: width 0.8s ease; | |
| } | |
| .patient-search { | |
| position: relative; | |
| } | |
| .patient-results { | |
| position: absolute; | |
| top: 100%; | |
| left: 0; | |
| right: 0; | |
| background: white; | |
| border: 1px solid #dee2e6; | |
| border-radius: 4px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| z-index: 1000; | |
| display: none; | |
| } | |
| .patient-item { | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| border-bottom: 1px solid #f8f9fa; | |
| } | |
| .patient-item:hover { | |
| background: #f8f9fa; | |
| } | |
| .model-status { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 1050; | |
| } | |
| .pulse-animation { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| 100% { transform: scale(1); } | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .gradcam-btn { | |
| background-color: #dc3545; | |
| border: 1px solid #dc3545; | |
| color: white; | |
| } | |
| .gradcam-btn:hover { | |
| background-color: #c82333; | |
| border-color: #bd2130; | |
| color: white; | |
| } | |
| .gradcam-modal .modal-dialog { | |
| max-width: 95vw; | |
| } | |
| .gradcam-modal .modal-content { | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .dual-analysis-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| } | |
| .analysis-column { | |
| flex: 1; | |
| min-width: 300px; | |
| } | |
| @media (max-width: 992px) { | |
| .dual-analysis-container { | |
| flex-direction: column; | |
| } | |
| } | |
| .hidden-file-input { | |
| position: absolute; | |
| left: -9999px; | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| /* ── TOAST NOTIFICATIONS ─────────────────────────── */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 1.5rem; | |
| right: 1.5rem; | |
| z-index: 99999; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.6rem; | |
| pointer-events: none; | |
| } | |
| .toast-notify { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 12px; | |
| min-width: 300px; | |
| max-width: 380px; | |
| padding: 14px 16px; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.15); | |
| background: #fff; | |
| border-left: 5px solid #ccc; | |
| pointer-events: all; | |
| opacity: 0; | |
| transform: translateX(60px); | |
| transition: opacity 0.3s ease, transform 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .toast-notify.show { opacity: 1; transform: translateX(0); } | |
| .toast-notify.hide { opacity: 0; transform: translateX(60px); } | |
| .toast-notify.toast-success { border-left-color: #28a745; } | |
| .toast-notify.toast-error { border-left-color: #dc3545; } | |
| .toast-notify.toast-warning { border-left-color: #ffc107; } | |
| .toast-notify.toast-info { border-left-color: #17a2b8; } | |
| .toast-icon { flex-shrink: 0; margin-top: 1px; } | |
| .toast-success .toast-icon svg { stroke: #28a745; } | |
| .toast-error .toast-icon svg { stroke: #dc3545; } | |
| .toast-warning .toast-icon svg { stroke: #ffc107; } | |
| .toast-info .toast-icon svg { stroke: #17a2b8; } | |
| .toast-body { flex: 1; } | |
| .toast-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 2px; } | |
| .toast-success .toast-title { color: #155724; } | |
| .toast-error .toast-title { color: #721c24; } | |
| .toast-warning .toast-title { color: #856404; } | |
| .toast-info .toast-title { color: #0c5460; } | |
| .toast-msg { font-size: 0.825rem; color: #555; line-height: 1.4; } | |
| .toast-close { | |
| background: none; border: none; font-size: 1rem; | |
| line-height: 1; color: #aaa; cursor: pointer; padding: 0; flex-shrink: 0; | |
| } | |
| .toast-close:hover { color: #333; } | |
| .toast-progress { | |
| position: absolute; | |
| bottom: 0; left: 0; | |
| height: 3px; | |
| border-radius: 0 0 0 10px; | |
| } | |
| .toast-success .toast-progress { background: #28a745; } | |
| .toast-error .toast-progress { background: #dc3545; } | |
| .toast-warning .toast-progress { background: #ffc107; } | |
| .toast-info .toast-progress { background: #17a2b8; } | |
| .alert-info h6, | |
| .alert-info p, | |
| .alert-info strong { | |
| color: #212529 ; | |
| } | |
| .alert-light small, | |
| .alert-light strong { | |
| color: #212529 ; | |
| } | |
| .patient-results { | |
| color: #212529 ; | |
| } | |
| .patient-item, | |
| .patient-item strong, | |
| .patient-item small { | |
| color: #212529 ; | |
| } | |
| </style> | |
| </head> | |
| <body class="vertical light"> | |
| <div class="wrapper"> | |
| <!-- Navegación superior --> | |
| <nav class="topnav navbar navbar-light"> | |
| <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar"> | |
| <i data-feather="menu" class="navbar-toggler-icon"></i> | |
| </button> | |
| <ul class="nav"> | |
| <li class="nav-item"> | |
| <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light"> | |
| <i data-feather="sun"></i> | |
| </a> | |
| <li class="nav-item"> | |
| <a href="#" onclick="confirmLogout()" class="nav-link text-muted" title="Cerrar sesión" style="display: flex; align-items: center; gap: 6px;"> | |
| <span class="avatar avatar-sm mt-2"> | |
| <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle"> | |
| </span> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Cerrar sesión"> | |
| <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> | |
| <polyline points="16 17 21 12 16 7"/> | |
| <line x1="21" y1="12" x2="9" y2="12"/> | |
| </svg> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar> | |
| <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle"> | |
| <i data-feather="x"><span class="sr-only"></span></i> | |
| </a> | |
| <nav class="vertnav navbar navbar-light"> | |
| <!-- nav bar --> | |
| <div class="w-100 mb-4 d-flex"> | |
| <a class="navbar-brand mx-auto mt-2 flex-fill text-center"> | |
| <img src="assets/images/RetinAILogo.png" alt="Logo" id="sidebarLogo" style="max-width:140px; transition: opacity 0.3s ease;"> | |
| <img src="assets/images/LogoMini.png" alt="Logo Mini" id="sidebarLogoMini" style="max-width:44px; display:none; transition: opacity 0.3s ease;"> | |
| </a> | |
| </div> | |
| <ul class="navbar-nav flex-fill w-100 mb-2"> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./index.html"> | |
| <i data-feather="home" class="fe-16"></i> | |
| <span class="ml-3 item-text">Dashboard</span> | |
| </a> | |
| </li> | |
| <li class="nav-item active"> | |
| <a class="nav-link" href="./diagnosis.html"> | |
| <i data-feather="eye" class="fe-16"></i> | |
| <span class="ml-3 item-text">Diagnóstico</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./patients.html"> | |
| <i data-feather="users" class="fe-16"></i> | |
| <span class="ml-3 item-text">Pacientes</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./historial-consultas.html"> | |
| <i data-feather="file-text" class="fe-16"></i> | |
| <span class="ml-3 item-text">Consultas</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./users.html"> | |
| <i data-feather="user" class="fe-16"></i> | |
| <span class="ml-3 item-text">Usuarios</span> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| </aside> | |
| <!-- Contenido principal --> | |
| <main role="main" class="main-content"> | |
| <div class="container-fluid"> | |
| <div class="row justify-content-center"> | |
| <div class="col-12"> | |
| <!-- Header --> | |
| <div class="row align-items-center mb-4"> | |
| <div class="col"> | |
| <h2 class="h3 page-title"> | |
| <i data-feather="eye" class="fe-24 mr-2 text-primary"></i> | |
| Diagnóstico con Inteligencia Artificial | |
| </h2> | |
| <p class="text-muted">Diagnostico de retinopatía diabética mediante análisis de imágenes</p> | |
| </div> | |
| </div> | |
| <!-- Formulario principal --> | |
| <div class="row"> | |
| <div class="col-lg-8"> | |
| <div class="card shadow mb-4"> | |
| <div class="card-header"> | |
| <h5 class="card-title mb-0"> | |
| <i data-feather="upload" class="fe-16 mr-2"></i>Cargar Imágenes para Análisis | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="dual-analysis-container"> | |
| <!-- Columna 1 - Ojo Derecho --> | |
| <div class="analysis-column"> | |
| <h6 class="text-center mb-3">Ojo Derecho</h6> | |
| <!-- Input oculto para zona 2 --> | |
| <input type="file" id="imageInput2" class="hidden-file-input" accept="image/*"> | |
| <!-- Zona de carga 2 --> | |
| <div id="uploadZone2" class="upload-zone" data-zone="2"> | |
| <i data-feather="camera" class="fe-48 text-muted mb-3"></i> | |
| <h5 class="text-muted">Arrastra una imagen aquí o haz clic para seleccionar</h5> | |
| <p class="text-muted mb-0">Formatos soportados: JPG, PNG, JPEG (Máx. 10MB)</p> | |
| </div> | |
| <!-- Preview de imagen 2 --> | |
| <div id="imagePreview2" style="display: none;" class="text-center mt-4"> | |
| <img id="previewImg2" class="image-preview" alt="Vista previa"> | |
| <div class="mt-3"> | |
| <button class="btn btn-success btn-lg mr-2" id="analyzeBtn2"> | |
| <i data-feather="zap" class="fe-16 mr-2"></i>Analizar con IA | |
| </button> | |
| <button class="btn btn-outline-secondary mr-2" id="clearBtn2"> | |
| <i data-feather="x" class="fe-16 mr-2"></i>Limpiar | |
| </button> | |
| <button class="btn btn-warning gradcam-btn" id="gradcamBtn2" style="display: none;"> | |
| <i data-feather="eye" class="fe-16 mr-2"></i>Ver Grad-CAM | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Resultados del analisis 2 --> | |
| <div id="analysisResults2" style="display: none;" class="mt-4"> | |
| <div class="card shadow mb-4 analysis-card"> | |
| <div class="card-body"> | |
| <h5 class="mb-4"> | |
| <i data-feather="activity" class="fe-16 mr-2"></i>Resultados - Ojo Derecho | |
| </h5> | |
| <div id="resultContent2"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Columna 2 - Ojo Izquierdo --> | |
| <div class="analysis-column"> | |
| <h6 class="text-center mb-3">Ojo Izquierdo</h6> | |
| <!-- Input oculto para zona 1 --> | |
| <input type="file" id="imageInput1" class="hidden-file-input" accept="image/*"> | |
| <!-- Zona de carga 1 --> | |
| <div id="uploadZone1" class="upload-zone" data-zone="1"> | |
| <i data-feather="camera" class="fe-48 text-muted mb-3"></i> | |
| <h5 class="text-muted">Arrastra una imagen aquí o haz clic para seleccionar</h5> | |
| <p class="text-muted mb-0">Formatos soportados: JPG, PNG, JPEG (Máx. 10MB)</p> | |
| </div> | |
| <!-- Preview de imagen 1 --> | |
| <div id="imagePreview1" style="display: none;" class="text-center mt-4"> | |
| <img id="previewImg1" class="image-preview" alt="Vista previa"> | |
| <div class="mt-3"> | |
| <button class="btn btn-success btn-lg mr-2" id="analyzeBtn1"> | |
| <i data-feather="zap" class="fe-16 mr-2"></i>Analizar con IA | |
| </button> | |
| <button class="btn btn-outline-secondary mr-2" id="clearBtn1"> | |
| <i data-feather="x" class="fe-16 mr-2"></i>Limpiar | |
| </button> | |
| <button class="btn btn-warning gradcam-btn" id="gradcamBtn1" style="display: none;"> | |
| <i data-feather="eye" class="fe-16 mr-2"></i>Ver Grad-CAM | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Resultados del analisis 1 --> | |
| <div id="analysisResults1" style="display: none;" class="mt-4"> | |
| <div class="card shadow mb-4 analysis-card"> | |
| <div class="card-body"> | |
| <h5 class="mb-4"> | |
| <i data-feather="activity" class="fe-16 mr-2"></i>Resultados - Ojo Izquierdo | |
| </h5> | |
| <div id="resultContent1"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Panel lateral --> | |
| <div class="col-lg-4"> | |
| <div class="card shadow mb-4"> | |
| <div class="card-header"> | |
| <h6 class="card-title mb-0"> | |
| <i data-feather="user" class="fe-16 mr-2"></i>Información del Paciente y Consulta | |
| </h6> | |
| </div> | |
| <div class="card-body"> | |
| <div class="form-group patient-search"> | |
| <label for="patientSearch">Buscar Paciente</label> | |
| <input type="text" class="form-control" id="patientSearch" placeholder="Nombre del paciente..."> | |
| <div id="patientResults" class="patient-results"></div> | |
| </div> | |
| <div id="selectedPatientInfo" style="display: none;"> | |
| <div class="alert alert-info"> | |
| <h6 id="patientName"></h6> | |
| <p class="mb-0" id="patientDetails"></p> | |
| <input type="hidden" id="selectedPatientId"> | |
| </div> | |
| </div> | |
| <hr> | |
| <div class="card"> | |
| <div class="card-header bg-light"> | |
| <h5 class="mr-2">Ojo Derecho</h5> | |
| </div> | |
| <div class="form-group" style="margin: 20px"> | |
| <label for="diabeticRetinopathy_right">Retinopatía Diabética</label> | |
| <select class="form-control" id="diabeticRetinopathy_right"> | |
| <option value="false">No</option> | |
| <option value="true">Sí</option> | |
| </select> | |
| </div> | |
| </div> | |
| <hr> | |
| <div class="card"> | |
| <div class="card-header bg-light"> | |
| <h5 class="mr-2">Ojo Izquierdo</h5> | |
| </div> | |
| <div class="form-group" style="margin: 20px"> | |
| <label for="diabeticRetinopathy_left">Retinopatía Diabética</label> | |
| <select class="form-control" id="diabeticRetinopathy_left"> | |
| <option value="false">No</option> | |
| <option value="true">Sí</option> | |
| </select> | |
| </div> | |
| </div> | |
| <hr> | |
| <div class="form-group"> | |
| <label for="consultationNotes">Notas de la Consulta</label> | |
| <textarea class="form-control" id="consultationNotes" rows="3" placeholder="Observaciones adicionales..."></textarea> | |
| </div> | |
| <button class="btn btn-primary btn-block" id="saveConsultationBtn" type="button"> | |
| <i data-feather="save" class="fe-16 mr-2"></i>Guardar Consulta | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Importante --> | |
| <div class="card shadow mb-4"> | |
| <div class="card-header"> | |
| <h6 class="card-title mb-0"> | |
| <i data-feather="info" class="fe-16 mr-2"></i>Importante | |
| </h6> | |
| </div> | |
| <div class="card-body"> | |
| <div class="alert alert-light"> | |
| <small> | |
| <strong>Recuerda:</strong> La IA es una herramienta de apoyo. | |
| Siempre confirma el diagnóstico con examen clínico completo. | |
| </small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script src="js/jquery.min.js"></script> | |
| <script src="js/popper.min.js"></script> | |
| <script src="js/moment.min.js"></script> | |
| <script src="js/bootstrap.min.js"></script> | |
| <script src="js/simplebar.min.js"></script> | |
| <script src="js/daterangepicker.js"></script> | |
| <script src="js/jquery.stickOnScroll.js"></script> | |
| <script src="js/tinycolor-min.js"></script> | |
| <script src="js/config.js"></script> | |
| <script src="api.js"></script> | |
| <script> | |
| $(document).ready(function() { | |
| console.log('Sistema de diagnostico iniciado'); | |
| // Variables globales | |
| let currentUser = null; | |
| let selectedPatient = null; | |
| let lastAnalysisResult1 = null; // Ojo IZQUIERDO (zona 1) | |
| let lastAnalysisResult2 = null; // Ojo DERECHO (zona 2) | |
| let todayStats = { analysis: 0, positive: 0 }; | |
| // Inicializar aplicacion | |
| initializeDiagnosisApp(); | |
| // Auto-seleccionar paciente si viene por URL (?patient=ID) | |
| async function autoSelectPatientFromURL() { | |
| const params = new URLSearchParams(window.location.search); | |
| const patientId = params.get('patient'); | |
| if (!patientId) return; | |
| const response = await api.getPatient(parseInt(patientId)).catch(() => null); | |
| if (!response || !response.success) return; | |
| const patientData = response.patient; | |
| selectedPatient = patientData; | |
| $('#patientSearch').val(patientData.name); | |
| $('#patientResults').hide(); | |
| const age = patientData.birthDate | |
| ? (moment ? moment().diff(moment(patientData.birthDate), 'years') : 'N/A') | |
| : 'N/A'; | |
| $('#selectedPatientInfo').show(); | |
| $('#patientName').text(patientData.name); | |
| $('#patientDetails').html(` | |
| <strong>ID:</strong> ${patientData.patientID} | | |
| <strong>Edad:</strong> ${age} años | |
| ${patientData.diabetesType ? `| <strong>Diabetes:</strong> ${patientData.diabetesType}` : ''} | |
| `); | |
| $('#selectedPatientId').val(patientData.patientID); | |
| enableSaveButton(); | |
| showAlert('info', `Paciente ${patientData.name} asociado al diagnóstico`); | |
| } | |
| // Esperar a que jQuery esté listo antes de autoseleccionar | |
| $(document).ready(function() { | |
| autoSelectPatientFromURL(); | |
| }); | |
| // FUNCION DE INICIALIZACION | |
| function initializeDiagnosisApp() { | |
| console.log('Inicializando aplicacion de diagnostico...'); | |
| checkAuthentication(); | |
| checkModelStatus(); | |
| setupEventListeners(); | |
| loadTodayStats(); | |
| updateLastUpdate(); | |
| setInterval(updateLastUpdate, 60000); | |
| } | |
| // AUTENTICACION | |
| function checkAuthentication() { | |
| api.getSession().then(function(response) { | |
| if (response && response.success) { | |
| currentUser = response.user; | |
| } else { | |
| window.location.href = 'auth-login.html'; | |
| } | |
| }).catch(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| // VERIFICAR ESTADO DEL MODELO | |
| function checkModelStatus() { | |
| api.getModelInfo().then(function(response) { | |
| if (response && response.loaded) { | |
| $('#modelStatusBadge').removeClass('badge-danger').addClass('badge-success'); | |
| $('#modelStatusText').text('Modelo Cargado'); | |
| } else { | |
| $('#modelStatusBadge').removeClass('badge-success').addClass('badge-danger'); | |
| $('#modelStatusText').text('Modelo No Disponible'); | |
| showAlert('warning', 'El modelo de IA no está disponible.'); | |
| } | |
| }); | |
| } | |
| // EVENT LISTENERS | |
| function setupEventListeners() { | |
| console.log('Configurando event listeners...'); | |
| $(document).on('click', '#uploadZone1', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| console.log('Click detectado en zona 1'); | |
| $('#imageInput1')[0].click(); | |
| }); | |
| $(document).on('click', '#uploadZone2', function(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| console.log('Click detectado en zona 2'); | |
| $('#imageInput2')[0].click(); | |
| }); | |
| // Carga de archivos para ambas zonas | |
| $(document).on('change', '#imageInput1', function(e) { | |
| console.log('Archivo seleccionado en zona 1'); | |
| handleImageSelect(e, 1); | |
| }); | |
| $(document).on('change', '#imageInput2', function(e) { | |
| console.log('Archivo seleccionado en zona 2'); | |
| handleImageSelect(e, 2); | |
| }); | |
| // Drag and drop para ambas zonas | |
| setupDragAndDrop(1); | |
| setupDragAndDrop(2); | |
| // Analisis y limpieza para ambas zonas | |
| $(document).on('click', '#analyzeBtn1', function() { handleImageAnalysis(1); }); | |
| $(document).on('click', '#analyzeBtn2', function() { handleImageAnalysis(2); }); | |
| $(document).on('click', '#clearBtn1', function() { clearImagePreview(1); }); | |
| $(document).on('click', '#clearBtn2', function() { clearImagePreview(2); }); | |
| // Grad-CAM para ambas zonas | |
| $(document).on('click', '#gradcamBtn1', function() { showGradCAMAnalysis(1); }); | |
| $(document).on('click', '#gradcamBtn2', function() { showGradCAMAnalysis(2); }); | |
| // Busqueda de pacientes | |
| $(document).on('input', '#patientSearch', handlePatientSearch); | |
| $(document).on('click', '.patient-item', handlePatientSelection); | |
| // Guardar consulta | |
| $(document).on('click', '#saveConsultationBtn', function(e) { | |
| e.preventDefault(); | |
| console.log('Boton guardar consulta clickeado'); | |
| handleSaveConsultation(); | |
| }); | |
| // Eventos para selectores manuales | |
| $(document).on('change', '#diabeticRetinopathy_right, #diabeticRetinopathy_left', function() { | |
| console.log('Diagnostico manual cambiado'); | |
| enableSaveButton(); | |
| }); | |
| // Cerrar dropdowns al hacer click fuera | |
| $(document).on('click', function(e) { | |
| if (!$(e.target).closest('.patient-search').length) { | |
| $('#patientResults').hide(); | |
| } | |
| }); | |
| console.log('Event listeners configurados correctamente'); | |
| } | |
| // DRAG AND DROP | |
| function setupDragAndDrop(zoneNumber) { | |
| const uploadZone = document.getElementById(`uploadZone${zoneNumber}`); | |
| if (!uploadZone) return; | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, function() { highlight(zoneNumber); }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, function() { unhighlight(zoneNumber); }, false); | |
| }); | |
| uploadZone.addEventListener('drop', function(e) { handleDrop(e, zoneNumber); }, false); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| function highlight(zoneNumber) { | |
| $(`#uploadZone${zoneNumber}`).addClass('dragover'); | |
| } | |
| function unhighlight(zoneNumber) { | |
| $(`#uploadZone${zoneNumber}`).removeClass('dragover'); | |
| } | |
| function handleDrop(e, zoneNumber) { | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| console.log(`Archivo arrastrado a zona ${zoneNumber}`); | |
| handleImageFile(files[0], zoneNumber); | |
| } | |
| } | |
| } | |
| // MANEJO DE ARCHIVOS | |
| function handleImageSelect(e, zoneNumber) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| console.log(`Imagen seleccionada para zona ${zoneNumber}:`, file.name); | |
| handleImageFile(file, zoneNumber); | |
| } | |
| } | |
| function handleImageFile(file, zoneNumber) { | |
| // Validaciones | |
| if (!file.type.startsWith('image/')) { | |
| showAlert('error', 'Por favor selecciona un archivo de imagen valido'); | |
| return; | |
| } | |
| if (file.size > 20 * 1024 * 1024) { | |
| showAlert('error', 'La imagen es demasiado grande. Máximo 20MB'); | |
| return; | |
| } | |
| // Mostrar preview | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| $(`#previewImg${zoneNumber}`).attr('src', e.target.result); | |
| $(`#imagePreview${zoneNumber}`).show().addClass('fade-in'); | |
| $(`#uploadZone${zoneNumber}`).hide(); | |
| $(`#analysisResults${zoneNumber}`).hide(); | |
| $(`#analyzeBtn${zoneNumber}`).prop('disabled', false); | |
| $(`#gradcamBtn${zoneNumber}`).hide(); | |
| }; | |
| reader.readAsDataURL(file); | |
| console.log(`Imagen cargada en zona ${zoneNumber}:`, file.name); | |
| showAlert('success', `Imagen cargada correctamente en ojo ${zoneNumber === 1 ? 'izquierdo' : 'derecho'}. Procede al analisis.`); | |
| } | |
| // ANALISIS DE IMAGEN | |
| function handleImageAnalysis(zoneNumber) { | |
| if (!$(`#previewImg${zoneNumber}`).attr('src')) { | |
| showAlert('error', 'No hay imagen para analizar'); | |
| return; | |
| } | |
| // Mostrar loading | |
| $(`#analyzeBtn${zoneNumber}`).prop('disabled', true) | |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Analizando...') | |
| .addClass('pulse-animation'); | |
| // Obtener datos | |
| const imageData = $(`#previewImg${zoneNumber}`).attr('src'); | |
| const fileName = $(`#imageInput${zoneNumber}`)[0].files[0]?.name || `analisis_${Date.now()}.jpg`; | |
| const patientId = selectedPatient ? selectedPatient.patientID : null; | |
| const notes = $('#consultationNotes').val().trim(); | |
| console.log(`Iniciando analisis zona ${zoneNumber}:`, { | |
| fileName, | |
| patientId, | |
| hasNotes: !!notes | |
| }); | |
| if (false) { // simulation removed | |
| console.log('Eel no disponible - simulando analisis'); | |
| setTimeout(() => { | |
| const mockResult = { | |
| success: true, | |
| prediction: { | |
| class: Math.random() > 0.5 ? 'Retinopatía Diabética' : 'Normal', | |
| class_index: Math.random() > 0.5 ? 1 : 0, | |
| confidence: Math.floor(Math.random() * 30 + 70) // 70-100% | |
| }, | |
| timestamp: new Date().toISOString(), | |
| filename: fileName | |
| }; | |
| if (zoneNumber === 1) { | |
| lastAnalysisResult1 = mockResult; | |
| } else { | |
| lastAnalysisResult2 = mockResult; | |
| } | |
| $(`#analyzeBtn${zoneNumber}`).prop('disabled', false) | |
| .html('<i data-feather="zap" class="fe-16 mr-2"></i>Analizar con IA') | |
| .removeClass('pulse-animation'); | |
| showAnalysisResults(mockResult, zoneNumber); | |
| enableSaveButton(); | |
| }, 2000); | |
| return; | |
| } | |
| // Llamar al modelo | |
| api.predict(imageData, fileName).then(function(result) { | |
| // Restaurar boton | |
| $(`#analyzeBtn${zoneNumber}`).prop('disabled', false) | |
| .html('<i data-feather="zap" class="fe-16 mr-2"></i>Analizar con IA') | |
| .removeClass('pulse-animation'); | |
| if (result && result.success) { | |
| if (zoneNumber === 1) { | |
| lastAnalysisResult1 = result; | |
| } else { | |
| lastAnalysisResult2 = result; | |
| } | |
| showAnalysisResults(result, zoneNumber); | |
| updateTodayStats(); | |
| // Habilitar boton guardar siempre que haya analisis | |
| enableSaveButton(); | |
| console.log(`Analisis completado zona ${zoneNumber}:`, result.prediction.class); | |
| } else { | |
| showAlert('error', result?.error || 'Error en el analisis de la imagen'); | |
| console.log(`Error en analisis zona ${zoneNumber}:`, result); | |
| } | |
| }); | |
| } | |
| // === FUNCION PARA HABILITAR BOTON GUARDAR === | |
| function enableSaveButton() { | |
| // Habilitar si hay paciente seleccionado Y (analisis o diagoóstico manual) | |
| const hasPatient = selectedPatient !== null; | |
| const hasAnalysis = lastAnalysisResult1 !== null || lastAnalysisResult2 !== null; | |
| const hasManualDiagnosis = $('#diabeticRetinopathy_right').val() !== '' || $('#diabeticRetinopathy_left').val() !== ''; | |
| const shouldEnable = hasPatient && (hasAnalysis || hasManualDiagnosis); | |
| console.log('Verificando boton guardar:', { | |
| hasPatient, | |
| hasAnalysis, | |
| hasManualDiagnosis, | |
| shouldEnable | |
| }); | |
| $('#saveConsultationBtn').prop('disabled', !shouldEnable); | |
| if (shouldEnable) { | |
| $('#saveConsultationBtn').removeClass('btn-secondary').addClass('btn-primary'); | |
| } else { | |
| $('#saveConsultationBtn').removeClass('btn-primary').addClass('btn-secondary'); | |
| } | |
| } | |
| // === MOSTRAR RESULTADOS === | |
| function showAnalysisResults(result, zoneNumber) { | |
| const prediction = result.prediction; | |
| const isPositive = prediction.class_index === 1; | |
| const confidence = prediction.confidence; | |
| // Determinar clase de resultado y colores | |
| const resultClass = isPositive ? 'border-warning' : 'border-success'; | |
| const iconClass = isPositive ? 'fe-alert-triangle' : 'fe-check-circle'; | |
| const iconColor = isPositive ? 'text-warning' : 'text-success'; | |
| const resultText = isPositive ? 'Retinopatía Diabética Detectada' : 'No se Detectó Retinopatía Diabética'; | |
| const eyeText = zoneNumber === 1 ? 'Ojo Izquierdo' : 'Ojo Derecho'; | |
| const resultHtml = ` | |
| <!-- Resultado Principal --> | |
| <div class="card shadow-sm ${resultClass} border-2"> | |
| <div class="card-body"> | |
| <div class="row align-items-center"> | |
| <div class="col-2 text-center"> | |
| <i class="fe ${iconClass} fe-48 ${iconColor}"></i> | |
| </div> | |
| <div class="col-10"> | |
| <h5 class="card-title mb-1">${resultText}</h5> | |
| <p class="text-muted mb-0">${eyeText}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Detalles del Analisis --> | |
| <div class="card shadow-sm mt-3"> | |
| <div class="card-header d-flex justify-content-between align-items-center"> | |
| <h6 class="mb-0"> | |
| <i data-feather="bar-chart-2" class="fe-16 mr-2"></i>Detalles del Análisis | |
| </h6> | |
| <span class="badge badge-pill ${isPositive ? 'badge-warning' : 'badge-success'}">${confidence}%</span> | |
| </div> | |
| <div class="card-body"> | |
| <!-- Medidor de Confianza --> | |
| <div class="mb-3"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <small class="text-muted">Nivel de Confianza</small> | |
| <small class="font-weight-bold">${confidence}%</small> | |
| </div> | |
| <div class="progress" style="height: 8px;"> | |
| <div class="progress-bar ${isPositive ? 'bg-warning' : 'bg-success'}" | |
| role="progressbar" | |
| style="width: ${confidence}%" | |
| aria-valuenow="${confidence}" | |
| aria-valuemin="0" | |
| aria-valuemax="100"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Informacion Temporal --> | |
| <div class="row text-center"> | |
| <div class="col-6"> | |
| <div class="border-right"> | |
| <h6 class="text-muted mb-0">Análisis realizado</h6> | |
| <small class="text-muted"> | |
| <i data-feather="clock" class="fe-12 mr-1"></i> | |
| ${moment ? moment(result.timestamp).format('DD/MM/YYYY HH:mm') : new Date(result.timestamp).toLocaleString()} | |
| </small> | |
| </div> | |
| </div> | |
| <div class="col-6"> | |
| <h6 class="text-muted mb-0">Modelo utilizado</h6> | |
| <small class="text-muted"> | |
| <i data-feather="cpu" class="fe-12 mr-1"></i> | |
| EfficientNetB0 | |
| </small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| $(`#resultContent${zoneNumber}`).html(resultHtml); | |
| $(`#analysisResults${zoneNumber}`).show().addClass('fade-in'); | |
| // Cambiar el fondo del contenedor de resultados | |
| $(`#analysisResults${zoneNumber} .analysis-card`).css({ | |
| 'background': '#f8f9fa', | |
| 'color': '#495057' | |
| }); | |
| // Scroll suave hacia los resultados | |
| $('html, body').animate({ | |
| scrollTop: $(`#analysisResults${zoneNumber}`).offset().top - 100 | |
| }, 800); | |
| // Actualizar selectores correctamente | |
| if(zoneNumber === 1) { | |
| // Zona 1 = Ojo IZQUIERDO | |
| document.getElementById('diabeticRetinopathy_left').value = isPositive.toString(); | |
| } else { | |
| // Zona 2 = Ojo DERECHO | |
| document.getElementById('diabeticRetinopathy_right').value = isPositive.toString(); | |
| } | |
| // Mostrar boton Grad-CAM solo si se detecto retinopatia diabetica | |
| if (isPositive) { | |
| $(`#gradcamBtn${zoneNumber}`).show().addClass('pulse-animation'); | |
| console.log(`Retinopatia detectada en zona ${zoneNumber} - Grad-CAM disponible`); | |
| } else { | |
| $(`#gradcamBtn${zoneNumber}`).hide(); | |
| } | |
| // Habilitar boton guardar | |
| enableSaveButton(); | |
| } | |
| // === BUSQUEDA DE PACIENTES === | |
| function handlePatientSearch() { | |
| const searchTerm = $('#patientSearch').val().trim(); | |
| if (searchTerm.length < 1) { | |
| // Mostrar todos los pacientes si el campo está vacío | |
| api.getPatients('').then(function(response) { | |
| if (response && response.success && response.patients.length > 0) { | |
| displayPatientResults(response.patients); | |
| } else { | |
| $('#patientResults').hide(); | |
| } | |
| }); | |
| return; | |
| } | |
| api.getPatients(searchTerm).then(function(response) { | |
| if (response && response.success) { | |
| displayPatientResults(response.patients); | |
| } else { | |
| $('#patientResults').hide(); | |
| } | |
| }); | |
| } | |
| function displayPatientResults(patients) { | |
| const resultsHtml = patients.map(patient => ` | |
| <div class="patient-item" data-patient='${JSON.stringify(patient)}'> | |
| <strong>${patient.name}</strong> | |
| ${patient.birthDate ? `<br><small class="text-muted">Nacimiento: ${moment ? moment(patient.birthDate).format('DD/MM/YYYY') : patient.birthDate}</small>` : ''} | |
| ${patient.diabetesType ? `<br><small class="text-muted">Diabetes: ${patient.diabetesType}</small>` : ''} | |
| </div> | |
| `).join(''); | |
| $('#patientResults').html(resultsHtml).show(); | |
| } | |
| function handlePatientSelection() { | |
| const patientData = $(this).data('patient'); | |
| selectedPatient = patientData; | |
| $('#patientSearch').val(patientData.name); | |
| $('#patientResults').hide(); | |
| // Mostrar informacion del paciente | |
| const age = patientData.birthDate ? (moment ? moment().diff(moment(patientData.birthDate), 'years') : 'N/A') : 'N/A'; | |
| $('#selectedPatientInfo').show(); | |
| $('#patientName').text(patientData.name); | |
| $('#patientDetails').html(` | |
| <strong>ID:</strong> ${patientData.patientID} | | |
| <strong>Edad:</strong> ${age} años | |
| ${patientData.diabetesType ? `| <strong>Diabetes:</strong> ${patientData.diabetesType}` : ''} | |
| `); | |
| $('#selectedPatientId').val(patientData.patientID); | |
| console.log('Paciente seleccionado:', patientData.name); | |
| showAlert('info', `Paciente ${patientData.name} seleccionado correctamente`); | |
| // Habilitar boton de guardar | |
| enableSaveButton(); | |
| } | |
| // === FUNCION CORREGIDA GUARDAR CONSULTA === | |
| function handleSaveConsultation() { | |
| console.log('Iniciando guardado de consulta...'); | |
| if (!selectedPatient) { | |
| showAlert('error', 'Selecciona un paciente antes de guardar'); | |
| return; | |
| } | |
| const notes = $('#consultationNotes').val().trim(); | |
| // Datos correctamente estructurados | |
| const consultationData = { | |
| patientId: selectedPatient.patientID, | |
| notes: notes, | |
| rightEye: { | |
| hasAnalysis: lastAnalysisResult2 !== null, // Zona 2 = Ojo DERECHO | |
| diagnosis: lastAnalysisResult2 ? | |
| (lastAnalysisResult2.prediction.class_index === 1) : | |
| ($('#diabeticRetinopathy_right').val() === 'true'), | |
| confidence: lastAnalysisResult2 ? lastAnalysisResult2.prediction.confidence : null, | |
| imagePath: lastAnalysisResult2 ? lastAnalysisResult2.filename : null | |
| }, | |
| leftEye: { | |
| hasAnalysis: lastAnalysisResult1 !== null, // Zona 1 = Ojo IZQUIERDO | |
| diagnosis: lastAnalysisResult1 ? | |
| (lastAnalysisResult1.prediction.class_index === 1) : | |
| ($('#diabeticRetinopathy_left').val() === 'true'), | |
| confidence: lastAnalysisResult1 ? lastAnalysisResult1.prediction.confidence : null, | |
| imagePath: lastAnalysisResult1 ? lastAnalysisResult1.filename : null | |
| } | |
| }; | |
| // Log detallado para debug | |
| console.log('Datos de consulta a guardar:', { | |
| paciente: selectedPatient.name, | |
| rightEye: consultationData.rightEye, | |
| leftEye: consultationData.leftEye, | |
| notes: notes | |
| }); | |
| // Mostrar loading | |
| $('#saveConsultationBtn').prop('disabled', true) | |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Guardando...'); | |
| if (false) { // simulation removed | |
| console.log('Eel no disponible - simulando guardado'); | |
| setTimeout(() => { | |
| $('#saveConsultationBtn').prop('disabled', false) | |
| .html('<i data-feather="save" class="fe-16 mr-2"></i>Guardar Consulta'); | |
| showAlert('success', 'Consulta guardada correctamente (simulación)'); | |
| }, 1500); | |
| return; | |
| } | |
| // Llamada correcta a EEL | |
| api.saveConsultation(consultationData).then(function(response) { | |
| console.log('Respuesta del servidor:', response); | |
| // Restaurar boton | |
| $('#saveConsultationBtn').prop('disabled', false) | |
| .html('<i data-feather="save" class="fe-16 mr-2"></i>Guardar Consulta'); | |
| if (response && response.success) { | |
| showAlert('success', 'Consulta guardada correctamente'); | |
| console.log('Consulta guardada exitosamente:', response); | |
| } else { | |
| const errorMsg = response?.message || 'Error al guardar la consulta'; | |
| showAlert('error', errorMsg); | |
| console.error('Error guardando consulta:', response); | |
| } | |
| }); | |
| } | |
| // === FUNCION GRAD-CAM === | |
| function showGradCAMAnalysis(zoneNumber) { | |
| const lastResult = zoneNumber === 1 ? lastAnalysisResult1 : lastAnalysisResult2; | |
| if (!lastResult) { | |
| showAlert('error', 'No hay analisis disponible para generar Grad-CAM'); | |
| return; | |
| } | |
| // Solo generar si se detecto retinopatia | |
| if (lastResult.prediction.class_index !== 1) { | |
| showAlert('info', 'Grad-CAM solo se genera para casos positivos de retinopatia diabetica'); | |
| return; | |
| } | |
| const imageData = $(`#previewImg${zoneNumber}`).attr('src'); | |
| const fileName = lastResult.filename; | |
| const eyeName = zoneNumber === 1 ? 'Ojo Izquierdo' : 'Ojo Derecho'; | |
| // Mostrar loading en el boton | |
| $(`#gradcamBtn${zoneNumber}`).prop('disabled', true) | |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Generando...'); | |
| console.log(`Generando Grad-CAM para zona ${zoneNumber}:`, fileName); | |
| if (false) { /* removed */ } | |
| // Llamar a la funcion de backend | |
| api.gradcam(imageData, fileName, lastResult).then(function(response) { | |
| // Restaurar boton | |
| $(`#gradcamBtn${zoneNumber}`).prop('disabled', false) | |
| .html('<i data-feather="eye" class="fe-16 mr-2"></i>Ver Grad-CAM'); | |
| if (response && response.success) { | |
| console.log('Grad-CAM generado exitosamente'); | |
| showGradCAMModal(response, eyeName, zoneNumber); | |
| } else { | |
| const errorMsg = response?.error || 'Error generando analisis Grad-CAM'; | |
| showAlert('error', errorMsg); | |
| console.error('Error Grad-CAM:', response); | |
| } | |
| }); | |
| } | |
| function showGradCAMModal(gradcamData, eyeName, zoneNumber) { | |
| const analysis = gradcamData.analysis || {}; | |
| // Campos con valores por defecto para compatibilidad | |
| const maxAct = (analysis.max_activation || 0).toFixed(3); | |
| const avgAct = (analysis.avg_activation || 0).toFixed(3); | |
| const highPct = (analysis.high_activation_pct || analysis.zoom_high_activation_pct || 0).toFixed(1); | |
| const clinical = analysis.clinical_info || 'Análisis completado'; | |
| const region = analysis.zoom_region_hd || [0,0,0,0]; | |
| const alertType = parseFloat(highPct) > 20 ? 'danger' : parseFloat(highPct) > 10 ? 'warning' : 'info'; | |
| const modalHtml = ` | |
| <div class="modal fade gradcam-modal" id="gradcamModal" tabindex="-1" role="dialog"> | |
| <div class="modal-dialog modal-lg" role="document"> | |
| <div class="modal-content"> | |
| <div class="modal-header bg-danger text-white"> | |
| <h5 class="modal-title"> | |
| <i data-feather="eye" class="mr-2"></i>Zoom HD - ${eyeName} | |
| <span class="badge badge-light text-danger ml-2">Retinopatía Detectada</span> | |
| </h5> | |
| <button type="button" class="close text-white" data-dismiss="modal"> | |
| <span>×</span> | |
| </button> | |
| </div> | |
| <div class="modal-body p-0"> | |
| <div class="text-center bg-white p-4"> | |
| <img src="${gradcamData.gradcam_image}" | |
| class="img-fluid" | |
| style="max-width: 100%; border-radius: 8px; border: 2px solid #dc3545;" | |
| alt="Zoom HD - Región Crítica"> | |
| <div class="mt-3"> | |
| <h6 class="text-danger mb-2"> | |
| <i data-feather="zoom-in" class="mr-2"></i>Zona Crítica en Alta Resolución | |
| </h6> | |
| <p class="text-muted mb-0"> | |
| <strong>Activación máxima:</strong> ${maxAct} | | |
| <strong>Activación promedio:</strong> ${avgAct} | |
| </p> | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <div class="alert alert-info"> | |
| <h6 class="text-info mb-2">Coordenadas en imagen original:</h6> | |
| <p class="mb-1">Región HD: [x: ${region[0]}–${region[2]}, y: ${region[1]}–${region[3]}]</p> | |
| <p class="mb-0"><strong>Área de alta activación:</strong> ${highPct}% del total</p> | |
| </div> | |
| <div class="alert alert-${alertType}"> | |
| <small> | |
| <i data-feather="info" class="mr-1"></i> | |
| ${clinical} | |
| </small> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-outline-primary" onclick="downloadGradCAMImage(${zoneNumber})"> | |
| <i data-feather="download" class="mr-2"></i>Descargar Imagen | |
| </button> | |
| <button type="button" class="btn btn-secondary" data-dismiss="modal"> | |
| <i data-feather="x" class="mr-2"></i>Cerrar | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Remover modal existente si lo hay | |
| $('#gradcamModal').remove(); | |
| // Agregar y mostrar nuevo modal | |
| $('body').append(modalHtml); | |
| $('#gradcamModal').modal('show').on('hidden.bs.modal', function() { | |
| $(this).remove(); | |
| }); | |
| // Guardar datos para descarga | |
| window.currentGradCAMData = gradcamData; | |
| } | |
| // === FUNCIONES DE UTILIDAD === | |
| function clearImagePreview(zoneNumber) { | |
| $(`#imagePreview${zoneNumber}`).hide(); | |
| $(`#analysisResults${zoneNumber}`).hide(); | |
| $(`#uploadZone${zoneNumber}`).show(); | |
| $(`#imageInput${zoneNumber}`).val(''); | |
| $(`#analyzeBtn${zoneNumber}`).prop('disabled', false); | |
| $(`#gradcamBtn${zoneNumber}`).hide().removeClass('pulse-animation'); | |
| if (zoneNumber === 1) { | |
| lastAnalysisResult1 = null; | |
| } else { | |
| lastAnalysisResult2 = null; | |
| } | |
| // Verificar si debe deshabilitar guardar | |
| enableSaveButton(); | |
| } | |
| function loadTodayStats() { | |
| api.getConsultations(1, 50).then(function(r) { | |
| const history = (r.consultations || []).map(c => ({ | |
| timestamp: c.consultationDate, | |
| prediction: { class_index: c.diabeticRetinopathy ? 1 : 0 } | |
| })); | |
| if (history.length > 0) { | |
| const today = moment ? moment().format('YYYY-MM-DD') : new Date().toISOString().split('T')[0]; | |
| const todayAnalyses = history.filter(item => | |
| (item.timestamp || '').substring(0, 10) === today | |
| ); | |
| todayStats.analysis = todayAnalyses.length; | |
| todayStats.positive = todayAnalyses.filter(i => i.prediction.class_index === 1).length; | |
| updateStatsDisplay(); | |
| } | |
| }); | |
| } | |
| function updateTodayStats() { | |
| if (lastAnalysisResult1 || lastAnalysisResult2) { | |
| todayStats.analysis++; | |
| const hasPositive = (lastAnalysisResult1 && lastAnalysisResult1.prediction.class_index === 1) || | |
| (lastAnalysisResult2 && lastAnalysisResult2.prediction.class_index === 1); | |
| if (hasPositive) { | |
| todayStats.positive++; | |
| } | |
| updateStatsDisplay(); | |
| } | |
| } | |
| function updateStatsDisplay() { | |
| $('#todayAnalysis').text(todayStats.analysis); | |
| $('#todayPositive').text(todayStats.positive); | |
| } | |
| function updateLastUpdate() { | |
| const now = moment ? moment().format('HH:mm') : new Date().toLocaleTimeString().slice(0,5); | |
| $('#lastUpdate').text(now); | |
| } | |
| function showAlert(type, message) { | |
| if (!document.getElementById('toast-container')) { | |
| const container = document.createElement('div'); | |
| container.id = 'toast-container'; | |
| document.body.appendChild(container); | |
| } | |
| const container = document.getElementById('toast-container'); | |
| const icons = { | |
| success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>', | |
| error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', | |
| warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>', | |
| info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>' | |
| }; | |
| const titles = { success: 'Éxito', error: 'Error', warning: 'Advertencia', info: 'Información' }; | |
| const duration = type === 'error' ? 6000 : 4000; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast-notify toast-${type}`; | |
| toast.innerHTML = ` | |
| <div class="toast-icon">${icons[type] || icons.info}</div> | |
| <div class="toast-body"> | |
| <div class="toast-title">${titles[type] || 'Aviso'}</div> | |
| <div class="toast-msg">${message}</div> | |
| </div> | |
| <button class="toast-close" onclick="this.closest('.toast-notify').remove()">×</button> | |
| <div class="toast-progress" style="width:100%;transition:width ${duration}ms linear;"></div> | |
| `; | |
| container.appendChild(toast); | |
| requestAnimationFrame(() => requestAnimationFrame(() => { | |
| toast.classList.add('show'); | |
| const bar = toast.querySelector('.toast-progress'); | |
| if (bar) requestAnimationFrame(() => { bar.style.width = '0%'; }); | |
| })); | |
| const timer = setTimeout(() => { | |
| toast.classList.add('hide'); | |
| toast.addEventListener('transitionend', () => toast.remove(), { once: true }); | |
| }, duration); | |
| toast.querySelector('.toast-close').addEventListener('click', () => clearTimeout(timer)); | |
| } | |
| // === FUNCIONES GLOBALES === | |
| window.downloadGradCAMImage = function(zoneNumber) { | |
| if (!window.currentGradCAMData) { | |
| showAlert('error', 'No hay imagen Grad-CAM para descargar'); | |
| return; | |
| } | |
| try { | |
| const eyeName = zoneNumber === 1 ? 'izquierdo' : 'derecho'; | |
| const filename = `gradcam_${eyeName}_${window.currentGradCAMData.filename}_${new Date().getTime()}.png`; | |
| // Crear enlace de descarga | |
| const link = document.createElement('a'); | |
| link.href = window.currentGradCAMData.gradcam_image; | |
| link.download = filename; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| showAlert('success', `Imagen Grad-CAM descargada: ${filename}`); | |
| } catch (error) { | |
| showAlert('error', 'Error al descargar la imagen'); | |
| console.error('Error descarga:', error); | |
| } | |
| }; | |
| console.log('Sistema de diagnostico completamente inicializado'); | |
| }); | |
| </script> | |
| <script> | |
| function confirmLogout() { | |
| executeLogout(); | |
| } | |
| function executeLogout() { | |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| </script> | |
| <script> | |
| // Centrar ventana al cargar pagina | |
| (function() { | |
| function centerWindow() { | |
| try { | |
| const screenWidth = window.screen.availWidth; | |
| const screenHeight = window.screen.availHeight; | |
| const x = Math.floor((screenWidth - 1200) / 2); | |
| const y = Math.floor((screenHeight - 800) / 2); | |
| if (window.moveTo) { | |
| window.moveTo(Math.max(0, x), Math.max(0, y)); | |
| } | |
| } catch (e) { | |
| console.warn('No se pudo centrar ventana:', e); | |
| } | |
| } | |
| // Centrar al cargar | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', centerWindow); | |
| } else { | |
| centerWindow(); | |
| } | |
| window.addEventListener('load', centerWindow); | |
| window.addEventListener('focus', centerWindow); | |
| })(); | |
| </script> | |
| <script> | |
| function checkUserRoleAndHideNavigation() { | |
| api.getSession().then(function(response) { | |
| if (response.success && response.user) { | |
| const userRole = response.user.role; | |
| const usersNavItem = document.querySelector('a[href="./users.html"]'); | |
| if (usersNavItem) { | |
| const navItemContainer = usersNavItem.closest('li.nav-item'); | |
| if (userRole === 'Admin') { | |
| // Usuario es Admin - mostrar elemento | |
| if (navItemContainer) { | |
| navItemContainer.style.display = 'block'; | |
| } | |
| console.log('Navegacion de usuarios visible para administrador:', response.user.username); | |
| } else { | |
| // Usuario NO es Admin - ocultar elemento | |
| if (navItemContainer) { | |
| navItemContainer.style.display = 'none'; | |
| } | |
| console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')'); | |
| } | |
| } else { | |
| console.warn('No se encontró el elemento de navegación de usuarios'); | |
| } | |
| } else { | |
| console.warn('No se pudo obtener información del usuario actual'); | |
| } | |
| }); | |
| } | |
| // Funcion para verificar si el usuario actual esta intentando acceder a usuarios.html | |
| function checkUsersPageAccess() { | |
| // Solo ejecutar en la pagina de usuarios | |
| if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) { | |
| api.getSession().then(function(response) { | |
| if (!response.success) { window.location.href = 'auth-login.html'; return; } | |
| if (response.user.role !== 'Admin') { window.location.href = 'index.html'; return; | |
| } | |
| }); | |
| } | |
| } | |
| // Ejecutar al cargar la pagina | |
| $(document).ready(function() { | |
| // Pequeno delay para asegurar que Eel este listo | |
| setTimeout(() => { | |
| checkUserRoleAndHideNavigation(); | |
| checkUsersPageAccess(); | |
| }, 100); | |
| }); | |
| // Tambien ejecutar cuando cambie el estado de autenticacion | |
| function updateNavigationForUser() { | |
| checkUserRoleAndHideNavigation(); | |
| } | |
| // Funcion para mostrar informacion del usuario en el dropdown (opcional) | |
| function updateUserInfo() { | |
| api.getSession().then(function(response) { | |
| if (response.success && response.user) { | |
| const userDropdown = document.querySelector('#navbarDropdownMenuLink'); | |
| if (userDropdown) { | |
| const roleBadge = response.user.role === 'Admin' ? | |
| '<span class="badge badge-primary badge-sm ml-1">Admin</span>' : | |
| '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>'; | |
| if (!userDropdown.querySelector('.badge')) userDropdown.innerHTML += roleBadge; | |
| } | |
| } | |
| }); | |
| } | |
| // Llamar tambien a updateUserInfo al cargar | |
| $(document).ready(function() { | |
| setTimeout(() => { | |
| updateUserInfo(); | |
| }, 200); | |
| }); | |
| </script> | |
| <script src="js/gauge.min.js"></script> | |
| <script src="js/jquery.sparkline.min.js"></script> | |
| <script src="js/apps.js"></script> | |
| <script> | |
| function _initFeather() { | |
| if (typeof feather !== 'undefined') feather.replace({ 'stroke-width': 1.8 }); | |
| } | |
| document.addEventListener('DOMContentLoaded', _initFeather); | |
| var _fObserver = new MutationObserver(function(mutations) { | |
| if (mutations.some(function(m){ return m.addedNodes.length > 0; })) _initFeather(); | |
| }); | |
| document.addEventListener('DOMContentLoaded', function() { | |
| _fObserver.observe(document.body, { childList: true, subtree: true }); | |
| }); | |
| </script> | |
| <script> | |
| (function () { | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'theme-overlay'; | |
| overlay.style.cssText = [ | |
| 'position:fixed','inset:0','z-index:99999', | |
| 'pointer-events:none','opacity:0', | |
| 'transition:opacity 0.18s ease','background:#fff' | |
| ].join(';'); | |
| document.body.appendChild(overlay); | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const btn = document.getElementById('modeSwitcher'); | |
| if (!btn) return; | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| switchTheme(); | |
| }); | |
| }); | |
| })(); | |
| </script> | |
| <script> | |
| (function () { | |
| function applyTheme(theme) { | |
| const lightTheme = document.getElementById('lightTheme'); | |
| const darkTheme = document.getElementById('darkTheme'); | |
| if (theme === 'dark') { | |
| lightTheme.disabled = true; | |
| darkTheme.disabled = false; | |
| document.documentElement.style.backgroundColor = '#1a1a2e'; | |
| } else { | |
| lightTheme.disabled = false; | |
| darkTheme.disabled = true; | |
| document.documentElement.style.backgroundColor = ''; | |
| } | |
| document.documentElement.style.visibility = 'visible'; | |
| } | |
| applyTheme(localStorage.getItem('theme') || 'light'); | |
| window.addEventListener('storage', function(e) { | |
| if (e.key === 'theme') applyTheme(e.newValue); | |
| }); | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'theme-overlay'; | |
| overlay.style.cssText = [ | |
| 'position:fixed','inset:0','z-index:99999', | |
| 'pointer-events:none','opacity:0', | |
| 'transition:opacity 0.18s ease','background:#fff' | |
| ].join(';'); | |
| document.body.appendChild(overlay); | |
| function switchTheme() { | |
| const darkTheme = document.getElementById('darkTheme'); | |
| const goingDark = darkTheme.disabled; | |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; | |
| overlay.style.opacity = '1'; | |
| setTimeout(function () { | |
| const newTheme = goingDark ? 'dark' : 'light'; | |
| applyTheme(newTheme); | |
| localStorage.setItem('theme', newTheme); | |
| setTimeout(function () { overlay.style.opacity = '0'; }, 50); | |
| }, 180); | |
| } | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const btn = document.getElementById('modeSwitcher'); | |
| if (!btn) return; | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| switchTheme(); | |
| }); | |
| }); | |
| })(); | |
| (function() { | |
| var sidebar = document.getElementById('leftSidebar'); | |
| var logo = document.getElementById('sidebarLogo'); | |
| var logoMini = document.getElementById('sidebarLogoMini'); | |
| if (!sidebar || !logo || !logoMini) return; | |
| function updateLogo() { | |
| var isCollapsed = sidebar.classList.contains('collapsed') | |
| || document.body.classList.contains('collapsed') | |
| || sidebar.offsetWidth < 100; | |
| logo.style.display = isCollapsed ? 'none' : ''; | |
| logoMini.style.display = isCollapsed ? 'block' : 'none'; | |
| } | |
| var observer = new MutationObserver(updateLogo); | |
| observer.observe(sidebar, { attributes: true, attributeFilter: ['class', 'style'] }); | |
| observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); | |
| updateLogo(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |