| <!doctype html> |
| <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-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 !important; |
| } |
| |
| .alert-light small, |
| .alert-light strong { |
| color: #212529 !important; |
| } |
| |
| .patient-results { |
| color: #212529 !important; |
| } |
| |
| .patient-item, |
| .patient-item strong, |
| .patient-item small { |
| color: #212529 !important; |
| } |
| </style> |
| </head> |
| <body class="vertical light"> |
| <div class="wrapper"> |
| |
| <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"> |
| |
| <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> |
|
|
| |
| <main role="main" class="main-content"> |
| <div class="container-fluid"> |
| <div class="row justify-content-center"> |
| <div class="col-12"> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <div class="analysis-column"> |
| <h6 class="text-center mb-3">Ojo Derecho</h6> |
|
|
| |
| <input type="file" id="imageInput2" class="hidden-file-input" accept="image/*"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="analysis-column"> |
| <h6 class="text-center mb-3">Ojo Izquierdo</h6> |
|
|
| |
| <input type="file" id="imageInput1" class="hidden-file-input" accept="image/*"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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'); |
| |
| |
| let currentUser = null; |
| let selectedPatient = null; |
| let lastAnalysisResult1 = null; |
| let lastAnalysisResult2 = null; |
| let todayStats = { analysis: 0, positive: 0 }; |
| |
| |
| initializeDiagnosisApp(); |
| |
| |
| 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`); |
| } |
| |
| |
| $(document).ready(function() { |
| autoSelectPatientFromURL(); |
| }); |
| |
| |
| function initializeDiagnosisApp() { |
| console.log('Inicializando aplicacion de diagnostico...'); |
| |
| checkAuthentication(); |
| |
| checkModelStatus(); |
| |
| setupEventListeners(); |
| |
| loadTodayStats(); |
| |
| updateLastUpdate(); |
| setInterval(updateLastUpdate, 60000); |
| } |
| |
| |
| 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'; }); |
| } |
| |
| |
| 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.'); |
| } |
| }); |
| } |
| |
| |
| 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(); |
| }); |
| |
| |
| $(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); |
| }); |
| |
| |
| setupDragAndDrop(1); |
| setupDragAndDrop(2); |
| |
| |
| $(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); }); |
| |
| |
| $(document).on('click', '#gradcamBtn1', function() { showGradCAMAnalysis(1); }); |
| $(document).on('click', '#gradcamBtn2', function() { showGradCAMAnalysis(2); }); |
| |
| |
| $(document).on('input', '#patientSearch', handlePatientSearch); |
| $(document).on('click', '.patient-item', handlePatientSelection); |
| |
| |
| $(document).on('click', '#saveConsultationBtn', function(e) { |
| e.preventDefault(); |
| console.log('Boton guardar consulta clickeado'); |
| handleSaveConsultation(); |
| }); |
| |
| |
| $(document).on('change', '#diabeticRetinopathy_right, #diabeticRetinopathy_left', function() { |
| console.log('Diagnostico manual cambiado'); |
| enableSaveButton(); |
| }); |
| |
| |
| $(document).on('click', function(e) { |
| if (!$(e.target).closest('.patient-search').length) { |
| $('#patientResults').hide(); |
| } |
| }); |
| |
| console.log('Event listeners configurados correctamente'); |
| } |
| |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| 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) { |
| |
| 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; |
| } |
| |
| |
| 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.`); |
| } |
| |
| |
| function handleImageAnalysis(zoneNumber) { |
| if (!$(`#previewImg${zoneNumber}`).attr('src')) { |
| showAlert('error', 'No hay imagen para analizar'); |
| return; |
| } |
| |
| |
| $(`#analyzeBtn${zoneNumber}`).prop('disabled', true) |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Analizando...') |
| .addClass('pulse-animation'); |
| |
| |
| 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) { |
| 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) |
| }, |
| 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; |
| } |
| |
| |
| api.predict(imageData, fileName).then(function(result) { |
| |
| $(`#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(); |
| |
| |
| 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); |
| } |
| }); |
| } |
| |
| |
| function enableSaveButton() { |
| |
| 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'); |
| } |
| } |
| |
| |
| function showAnalysisResults(result, zoneNumber) { |
| const prediction = result.prediction; |
| const isPositive = prediction.class_index === 1; |
| const confidence = prediction.confidence; |
| |
| |
| 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'); |
| |
| |
| $(`#analysisResults${zoneNumber} .analysis-card`).css({ |
| 'background': '#f8f9fa', |
| 'color': '#495057' |
| }); |
| |
| |
| $('html, body').animate({ |
| scrollTop: $(`#analysisResults${zoneNumber}`).offset().top - 100 |
| }, 800); |
| |
| |
| if(zoneNumber === 1) { |
| |
| document.getElementById('diabeticRetinopathy_left').value = isPositive.toString(); |
| } else { |
| |
| document.getElementById('diabeticRetinopathy_right').value = isPositive.toString(); |
| } |
| |
| |
| if (isPositive) { |
| $(`#gradcamBtn${zoneNumber}`).show().addClass('pulse-animation'); |
| console.log(`Retinopatia detectada en zona ${zoneNumber} - Grad-CAM disponible`); |
| } else { |
| $(`#gradcamBtn${zoneNumber}`).hide(); |
| } |
| |
| |
| enableSaveButton(); |
| } |
| |
| |
| function handlePatientSearch() { |
| const searchTerm = $('#patientSearch').val().trim(); |
| |
| if (searchTerm.length < 1) { |
| |
| 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(); |
| |
| |
| 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`); |
| |
| |
| enableSaveButton(); |
| } |
| |
| |
| function handleSaveConsultation() { |
| console.log('Iniciando guardado de consulta...'); |
| |
| if (!selectedPatient) { |
| showAlert('error', 'Selecciona un paciente antes de guardar'); |
| return; |
| } |
| |
| const notes = $('#consultationNotes').val().trim(); |
| |
| |
| const consultationData = { |
| patientId: selectedPatient.patientID, |
| notes: notes, |
| rightEye: { |
| hasAnalysis: lastAnalysisResult2 !== null, |
| 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, |
| diagnosis: lastAnalysisResult1 ? |
| (lastAnalysisResult1.prediction.class_index === 1) : |
| ($('#diabeticRetinopathy_left').val() === 'true'), |
| confidence: lastAnalysisResult1 ? lastAnalysisResult1.prediction.confidence : null, |
| imagePath: lastAnalysisResult1 ? lastAnalysisResult1.filename : null |
| } |
| }; |
| |
| |
| console.log('Datos de consulta a guardar:', { |
| paciente: selectedPatient.name, |
| rightEye: consultationData.rightEye, |
| leftEye: consultationData.leftEye, |
| notes: notes |
| }); |
| |
| |
| $('#saveConsultationBtn').prop('disabled', true) |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Guardando...'); |
| |
| if (false) { |
| 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; |
| } |
| |
| |
| api.saveConsultation(consultationData).then(function(response) { |
| console.log('Respuesta del servidor:', response); |
| |
| |
| $('#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); |
| } |
| }); |
| } |
| |
| |
| function showGradCAMAnalysis(zoneNumber) { |
| const lastResult = zoneNumber === 1 ? lastAnalysisResult1 : lastAnalysisResult2; |
| |
| if (!lastResult) { |
| showAlert('error', 'No hay analisis disponible para generar Grad-CAM'); |
| return; |
| } |
| |
| |
| 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'; |
| |
| |
| $(`#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) { } |
| |
| |
| api.gradcam(imageData, fileName, lastResult).then(function(response) { |
| |
| $(`#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 || {}; |
| |
| |
| 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> |
| `; |
| |
| |
| $('#gradcamModal').remove(); |
| |
| |
| $('body').append(modalHtml); |
| $('#gradcamModal').modal('show').on('hidden.bs.modal', function() { |
| $(this).remove(); |
| }); |
| |
| |
| window.currentGradCAMData = gradcamData; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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)); |
| } |
| |
| |
| 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`; |
| |
| |
| 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> |
| |
| (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); |
| } |
| } |
| |
| |
| 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') { |
| |
| if (navItemContainer) { |
| navItemContainer.style.display = 'block'; |
| } |
| console.log('Navegacion de usuarios visible para administrador:', response.user.username); |
| } else { |
| |
| 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'); |
| } |
| }); |
| } |
| |
| |
| function checkUsersPageAccess() { |
| |
| 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; |
| } |
| }); |
| } |
| } |
| |
| |
| $(document).ready(function() { |
| |
| setTimeout(() => { |
| checkUserRoleAndHideNavigation(); |
| checkUsersPageAccess(); |
| }, 100); |
| }); |
| |
| |
| function updateNavigationForUser() { |
| checkUserRoleAndHideNavigation(); |
| } |
| |
| |
| 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; |
| } |
| } |
| }); |
| } |
| |
| |
| $(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> |