Spaces:
Sleeping
Sleeping
Jose Salazar Claude Opus 5 commited on
Commit ·
2ab057f
1
Parent(s): a50e2a1
Retirar el stack legacy PHP/Apache
Browse filesjs/*.js y api/*.php ya no se cargaban: index.html apunta al bundle TS y el
Dockerfile no los copia a la imagen. Eran código muerto (2528 líneas).
.htaccess se retira también: era config sólo-Apache y el runtime nuevo es
uvicorn. Además protegía secretos por FilesMatch, algo en lo que el plan de
seguridad decidió explícitamente no confiar.
El código sigue disponible en el tag v1.0-legacy-php y en la rama
legacy/php-apache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- .htaccess +0 -32
- api/auth.php +0 -120
- api/conexion.php +0 -47
- api/hf_proxy.php +0 -116
- api/papers_proxy.php +0 -113
- api/setup.php +0 -37
- js/analisis.js +0 -817
- js/auth.js +0 -249
- js/ia.js +0 -289
- js/main.js +0 -205
- js/papers.js +0 -286
- js/pdf-parser.js +0 -477
- js/tooltip.js +0 -59
- js/ui.js +0 -479
.htaccess
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
# Deny access to sensitive files
|
| 2 |
-
<FilesMatch "^(\.env|setup\.php)$">
|
| 3 |
-
Require all denied
|
| 4 |
-
</FilesMatch>
|
| 5 |
-
|
| 6 |
-
# Gzip compression
|
| 7 |
-
<IfModule mod_deflate.c>
|
| 8 |
-
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json font/ttf font/woff font/woff2
|
| 9 |
-
</IfModule>
|
| 10 |
-
|
| 11 |
-
<IfModule mod_headers.c>
|
| 12 |
-
# JS and HTML — always revalidate so code changes are visible immediately.
|
| 13 |
-
# The script tag ?v= param busts the cache on intentional deploys.
|
| 14 |
-
<FilesMatch "\.(js|html)$">
|
| 15 |
-
Header set Cache-Control "no-cache, must-revalidate"
|
| 16 |
-
</FilesMatch>
|
| 17 |
-
|
| 18 |
-
# JSON data files — same: revalidate on every request
|
| 19 |
-
<FilesMatch "\.json$">
|
| 20 |
-
Header set Cache-Control "no-cache, must-revalidate"
|
| 21 |
-
</FilesMatch>
|
| 22 |
-
|
| 23 |
-
# CSS — revalidate (style changes need to show immediately too)
|
| 24 |
-
<FilesMatch "\.css$">
|
| 25 |
-
Header set Cache-Control "no-cache, must-revalidate"
|
| 26 |
-
</FilesMatch>
|
| 27 |
-
|
| 28 |
-
# Fonts — truly immutable; safe to cache for 1 year
|
| 29 |
-
<FilesMatch "\.(ttf|woff|woff2)$">
|
| 30 |
-
Header set Cache-Control "public, max-age=31536000, immutable"
|
| 31 |
-
</FilesMatch>
|
| 32 |
-
</IfModule>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/auth.php
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
<?php
|
| 2 |
-
|
| 3 |
-
session_start();
|
| 4 |
-
header('Content-Type: application/json; charset=utf-8');
|
| 5 |
-
header('Access-Control-Allow-Origin: *');
|
| 6 |
-
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
| 7 |
-
header('Access-Control-Allow-Headers: Content-Type');
|
| 8 |
-
|
| 9 |
-
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
| 10 |
-
|
| 11 |
-
// Verificar estado de sesión (GET)
|
| 12 |
-
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
| 13 |
-
echo json_encode([
|
| 14 |
-
'autenticado' => isset($_SESSION['morphos_usuario']),
|
| 15 |
-
'nombre' => $_SESSION['morphos_nombre'] ?? null,
|
| 16 |
-
]);
|
| 17 |
-
exit;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
| 21 |
-
http_response_code(405);
|
| 22 |
-
echo json_encode(['error' => 'Método no permitido.']);
|
| 23 |
-
exit;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
require __DIR__ . '/conexion.php';
|
| 27 |
-
|
| 28 |
-
if (!$conexion) {
|
| 29 |
-
http_response_code(503);
|
| 30 |
-
echo json_encode(['error' => 'Error de conexión con la base de datos.']);
|
| 31 |
-
exit;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
$cuerpo = json_decode(file_get_contents('php://input'), true) ?? [];
|
| 35 |
-
$accion = $cuerpo['accion'] ?? '';
|
| 36 |
-
|
| 37 |
-
switch ($accion) {
|
| 38 |
-
|
| 39 |
-
case 'login':
|
| 40 |
-
$email = trim($cuerpo['email'] ?? '');
|
| 41 |
-
$password = $cuerpo['password'] ?? '';
|
| 42 |
-
|
| 43 |
-
if (!$email || !$password) {
|
| 44 |
-
http_response_code(422);
|
| 45 |
-
echo json_encode(['error' => 'Email y contraseña son requeridos.']);
|
| 46 |
-
exit;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
$stmt = $conexion->prepare("SELECT id, nombre, email, password FROM usuarios WHERE email = :email LIMIT 1");
|
| 50 |
-
$stmt->bindParam(':email', $email);
|
| 51 |
-
$stmt->execute();
|
| 52 |
-
$usuario = $stmt->fetch(PDO::FETCH_ASSOC);
|
| 53 |
-
|
| 54 |
-
if ($usuario && password_verify($password, $usuario['password'])) {
|
| 55 |
-
$_SESSION['morphos_usuario'] = $usuario['email'];
|
| 56 |
-
$_SESSION['morphos_nombre'] = $usuario['nombre'];
|
| 57 |
-
echo json_encode(['ok' => true, 'nombre' => $usuario['nombre']]);
|
| 58 |
-
} else {
|
| 59 |
-
http_response_code(401);
|
| 60 |
-
echo json_encode(['error' => 'Email o contraseña incorrectos.']);
|
| 61 |
-
}
|
| 62 |
-
break;
|
| 63 |
-
|
| 64 |
-
case 'registro':
|
| 65 |
-
$nombre = trim($cuerpo['nombre'] ?? '');
|
| 66 |
-
$apellido = trim($cuerpo['apellido'] ?? '');
|
| 67 |
-
$email = trim($cuerpo['email'] ?? '');
|
| 68 |
-
$password = $cuerpo['password'] ?? '';
|
| 69 |
-
|
| 70 |
-
if (!$nombre || !$apellido || !$email || !$password) {
|
| 71 |
-
http_response_code(422);
|
| 72 |
-
echo json_encode(['error' => 'Todos los campos son requeridos.']);
|
| 73 |
-
exit;
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
| 77 |
-
http_response_code(422);
|
| 78 |
-
echo json_encode(['error' => 'El email no es válido.']);
|
| 79 |
-
exit;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
if (strlen($password) < 6) {
|
| 83 |
-
http_response_code(422);
|
| 84 |
-
echo json_encode(['error' => 'La contraseña debe tener al menos 6 caracteres.']);
|
| 85 |
-
exit;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
$stmt = $conexion->prepare("SELECT id FROM usuarios WHERE email = :email LIMIT 1");
|
| 89 |
-
$stmt->bindParam(':email', $email);
|
| 90 |
-
$stmt->execute();
|
| 91 |
-
if ($stmt->fetch()) {
|
| 92 |
-
http_response_code(409);
|
| 93 |
-
echo json_encode(['error' => 'Ya existe una cuenta con ese email.']);
|
| 94 |
-
exit;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
$hash = password_hash($password, PASSWORD_DEFAULT);
|
| 98 |
-
$stmt = $conexion->prepare(
|
| 99 |
-
"INSERT INTO usuarios (nombre, apellido, email, password) VALUES (:nombre, :apellido, :email, :password)"
|
| 100 |
-
);
|
| 101 |
-
$stmt->bindParam(':nombre', $nombre);
|
| 102 |
-
$stmt->bindParam(':apellido', $apellido);
|
| 103 |
-
$stmt->bindParam(':email', $email);
|
| 104 |
-
$stmt->bindParam(':password', $hash);
|
| 105 |
-
$stmt->execute();
|
| 106 |
-
|
| 107 |
-
$_SESSION['morphos_usuario'] = $email;
|
| 108 |
-
$_SESSION['morphos_nombre'] = $nombre;
|
| 109 |
-
echo json_encode(['ok' => true, 'nombre' => $nombre]);
|
| 110 |
-
break;
|
| 111 |
-
|
| 112 |
-
case 'logout':
|
| 113 |
-
session_destroy();
|
| 114 |
-
echo json_encode(['ok' => true]);
|
| 115 |
-
break;
|
| 116 |
-
|
| 117 |
-
default:
|
| 118 |
-
http_response_code(400);
|
| 119 |
-
echo json_encode(['error' => 'Acción no válida.']);
|
| 120 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/conexion.php
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
<?php
|
| 2 |
-
|
| 3 |
-
// Primero intenta conectar con MySQL (XAMPP), fallback a SQLite (Docker / HF Spaces)
|
| 4 |
-
$dbHost = '127.0.0.1';
|
| 5 |
-
$dbUsuario = 'root';
|
| 6 |
-
$dbClave = '';
|
| 7 |
-
$dbNombre = 'morphos_db';
|
| 8 |
-
$dbPort = 3306;
|
| 9 |
-
$dbPath = __DIR__ . '/../data/morphos.db';
|
| 10 |
-
|
| 11 |
-
if (file_exists(__DIR__ . '/.env')) {
|
| 12 |
-
foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
| 13 |
-
if (str_starts_with($line, 'DB_PORT=')) $dbPort = (int) trim(substr($line, 8));
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
$conexion = null;
|
| 18 |
-
|
| 19 |
-
$useSqlite = getenv('DB_FORCE_SQLITE') === '1';
|
| 20 |
-
|
| 21 |
-
if (!$useSqlite) {
|
| 22 |
-
try {
|
| 23 |
-
$conexion = new PDO("mysql:host=$dbHost;port=$dbPort;dbname=$dbNombre;charset=utf8mb4", $dbUsuario, $dbClave);
|
| 24 |
-
$conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
| 25 |
-
} catch (PDOException $e) {
|
| 26 |
-
$conexion = null;
|
| 27 |
-
}
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
// Fallback de SQLite
|
| 31 |
-
if (!$conexion) {
|
| 32 |
-
try {
|
| 33 |
-
$conexion = new PDO("sqlite:$dbPath");
|
| 34 |
-
$conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
| 35 |
-
|
| 36 |
-
$conexion->exec("CREATE TABLE IF NOT EXISTS usuarios (
|
| 37 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 38 |
-
nombre TEXT NOT NULL,
|
| 39 |
-
apellido TEXT NOT NULL,
|
| 40 |
-
email TEXT NOT NULL UNIQUE,
|
| 41 |
-
password TEXT NOT NULL,
|
| 42 |
-
creado_en DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 43 |
-
)");
|
| 44 |
-
} catch (PDOException $e) {
|
| 45 |
-
$conexion = null;
|
| 46 |
-
}
|
| 47 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/hf_proxy.php
DELETED
|
@@ -1,116 +0,0 @@
|
|
| 1 |
-
<?php
|
| 2 |
-
header('Content-Type: application/json; charset=utf-8');
|
| 3 |
-
header('Access-Control-Allow-Origin: *');
|
| 4 |
-
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
| 5 |
-
header('Access-Control-Allow-Headers: Content-Type');
|
| 6 |
-
|
| 7 |
-
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
| 8 |
-
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit; }
|
| 9 |
-
|
| 10 |
-
$hfKey = $_ENV['HF_API_KEY'] ?? $_SERVER['HF_API_KEY'] ?? getenv('HF_API_KEY') ?? '';
|
| 11 |
-
|
| 12 |
-
// Fallback a archivo .env si la variable de entorno no esta definida
|
| 13 |
-
if (!$hfKey && file_exists(__DIR__ . '/.env')) {
|
| 14 |
-
foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
| 15 |
-
if (str_starts_with($line, 'HF_API_KEY=')) { $hfKey = trim(substr($line, 11)); break; }
|
| 16 |
-
}
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
if (!$hfKey) { http_response_code(503); echo json_encode(['error' => 'Servicio no configurado.']); exit; }
|
| 20 |
-
|
| 21 |
-
$body = json_decode(file_get_contents('php://input'), true);
|
| 22 |
-
$SPACE = 'https://blackmistcode-morphos-medgemma.hf.space/gradio_api';
|
| 23 |
-
$auth = ['Content-Type: application/json', "Authorization: Bearer $hfKey"];
|
| 24 |
-
|
| 25 |
-
set_time_limit(120);
|
| 26 |
-
|
| 27 |
-
function hf_get(string $url, array $headers, ?string $post = null): array {
|
| 28 |
-
$ch = curl_init($url);
|
| 29 |
-
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 120]);
|
| 30 |
-
if ($post !== null) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); }
|
| 31 |
-
return [curl_exec($ch), curl_getinfo($ch, CURLINFO_HTTP_CODE)];
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
function uploadImagen(string $space, string $hfKey, string $dataUrl): ?array {
|
| 35 |
-
if (!preg_match('/^data:(image\/[\w+]+);base64,(.+)$/s', $dataUrl, $m)) return null;
|
| 36 |
-
$mimeType = $m[1];
|
| 37 |
-
$ext = explode('/', $mimeType)[1] ?? 'jpg';
|
| 38 |
-
$binary = base64_decode($m[2]);
|
| 39 |
-
if ($binary === false) return null;
|
| 40 |
-
|
| 41 |
-
// Construye manualmente el cuerpo multipart para enviar la imagen al upload endpoint de Gradio
|
| 42 |
-
$boundary = bin2hex(random_bytes(16));
|
| 43 |
-
$body = "--$boundary\r\n"
|
| 44 |
-
. "Content-Disposition: form-data; name=\"files\"; filename=\"image.$ext\"\r\n"
|
| 45 |
-
. "Content-Type: $mimeType\r\n\r\n"
|
| 46 |
-
. $binary
|
| 47 |
-
. "\r\n--$boundary--\r\n";
|
| 48 |
-
|
| 49 |
-
$ch = curl_init("$space/upload");
|
| 50 |
-
curl_setopt_array($ch, [
|
| 51 |
-
CURLOPT_RETURNTRANSFER => true,
|
| 52 |
-
CURLOPT_POST => true,
|
| 53 |
-
CURLOPT_POSTFIELDS => $body,
|
| 54 |
-
CURLOPT_HTTPHEADER => [
|
| 55 |
-
"Authorization: Bearer $hfKey",
|
| 56 |
-
"Content-Type: multipart/form-data; boundary=$boundary",
|
| 57 |
-
],
|
| 58 |
-
CURLOPT_TIMEOUT => 60,
|
| 59 |
-
]);
|
| 60 |
-
$result = curl_exec($ch);
|
| 61 |
-
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
| 62 |
-
curl_close($ch);
|
| 63 |
-
|
| 64 |
-
// Si el upload falla, devuelve la imagen inline para que el modelo la procese igualmente
|
| 65 |
-
if ($code >= 400 || !$result) {
|
| 66 |
-
return ['url' => $dataUrl, 'orig_name' => "image.$ext", 'mime_type' => $mimeType];
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
$files = json_decode($result, true);
|
| 70 |
-
$path = is_array($files) && isset($files[0]) ? $files[0] : null;
|
| 71 |
-
if (!$path) {
|
| 72 |
-
return ['url' => $dataUrl, 'orig_name' => "image.$ext", 'mime_type' => $mimeType];
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
return [
|
| 76 |
-
'path' => $path,
|
| 77 |
-
'url' => "$space/file=" . $path,
|
| 78 |
-
'orig_name' => "image.$ext",
|
| 79 |
-
'mime_type' => $mimeType,
|
| 80 |
-
];
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
$rawImages = array_slice(array_values($body['images'] ?? []), 0, 4);
|
| 84 |
-
$data = [];
|
| 85 |
-
foreach ($rawImages as $img) {
|
| 86 |
-
$data[] = $img ? uploadImagen($SPACE, $hfKey, $img) : null;
|
| 87 |
-
}
|
| 88 |
-
while (count($data) < 4) $data[] = null;
|
| 89 |
-
$data[] = $body['prompt'] ?? '';
|
| 90 |
-
|
| 91 |
-
[$submitBody, $code] = hf_get("$SPACE/call/analyze", $auth, json_encode(['data' => $data]));
|
| 92 |
-
|
| 93 |
-
if ($code >= 400) { http_response_code(502); echo json_encode(['error' => "Error Space: HTTP $code"]); exit; }
|
| 94 |
-
|
| 95 |
-
$eventId = json_decode($submitBody, true)['event_id'] ?? null;
|
| 96 |
-
if (!$eventId) { http_response_code(502); echo json_encode(['error' => 'No se obtuvo event_id.']); exit; }
|
| 97 |
-
|
| 98 |
-
[$stream] = hf_get("$SPACE/call/analyze/$eventId", ["Authorization: Bearer $hfKey"]);
|
| 99 |
-
|
| 100 |
-
$result = $error = null; $lastEvent = '';
|
| 101 |
-
// Parsea el stream SSE de Gradio buscando el evento 'complete' o 'process_completed'
|
| 102 |
-
foreach (explode("\n", $stream) as $raw) {
|
| 103 |
-
$line = rtrim($raw, "\r");
|
| 104 |
-
if (str_starts_with($line, 'event:')) $lastEvent = trim(substr($line, 6));
|
| 105 |
-
elseif (str_starts_with($line, 'data:')) {
|
| 106 |
-
$parsed = json_decode(trim(substr($line, 5)), true);
|
| 107 |
-
if (in_array($lastEvent, ['complete', 'process_completed']))
|
| 108 |
-
$result = is_array($parsed) ? $parsed[0] : ($parsed['output'] ?? $parsed);
|
| 109 |
-
elseif ($lastEvent === 'error')
|
| 110 |
-
$error = $parsed['error'] ?? 'Error del modelo.';
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
if ($error) { http_response_code(503); echo json_encode(['error' => $error]); }
|
| 115 |
-
elseif ($result !== null) { echo json_encode(['text' => $result]); }
|
| 116 |
-
else { http_response_code(502); echo json_encode(['error' => 'Sin respuesta del modelo.']); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/papers_proxy.php
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
<?php
|
| 2 |
-
header('Content-Type: application/json');
|
| 3 |
-
header('Access-Control-Allow-Origin: *');
|
| 4 |
-
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
| 5 |
-
header('Access-Control-Allow-Headers: Content-Type');
|
| 6 |
-
|
| 7 |
-
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
| 8 |
-
|
| 9 |
-
$consulta = trim($_GET['query'] ?? '');
|
| 10 |
-
if ($consulta === '') {
|
| 11 |
-
http_response_code(400);
|
| 12 |
-
echo json_encode(['error' => 'query requerido']);
|
| 13 |
-
exit;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
$dirCache = sys_get_temp_dir() . '/morphos_papers_cache';
|
| 17 |
-
if (!is_dir($dirCache)) mkdir($dirCache, 0700, true);
|
| 18 |
-
|
| 19 |
-
function leerCache(string $clave, int $ttl): string|false {
|
| 20 |
-
global $dirCache;
|
| 21 |
-
$archivo = $dirCache . '/' . md5($clave) . '.json';
|
| 22 |
-
if (file_exists($archivo) && (time() - filemtime($archivo)) < $ttl) {
|
| 23 |
-
return file_get_contents($archivo);
|
| 24 |
-
}
|
| 25 |
-
return false;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
function escribirCache(string $clave, string $contenido): void {
|
| 29 |
-
global $dirCache;
|
| 30 |
-
file_put_contents($dirCache . '/' . md5($clave) . '.json', $contenido);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
$claveCache = 'pm:' . $consulta;
|
| 34 |
-
$cached = leerCache($claveCache, 1800);
|
| 35 |
-
if ($cached) { echo $cached; exit; }
|
| 36 |
-
|
| 37 |
-
function fetchHttp(string $url, array $cabeceras, int $timeout = 15): array {
|
| 38 |
-
$ctx = stream_context_create(['http' => [
|
| 39 |
-
'method' => 'GET',
|
| 40 |
-
'header' => implode("\r\n", $cabeceras),
|
| 41 |
-
'timeout' => $timeout,
|
| 42 |
-
'ignore_errors' => true,
|
| 43 |
-
]]);
|
| 44 |
-
$body = @file_get_contents($url, false, $ctx);
|
| 45 |
-
$codigo = 0;
|
| 46 |
-
foreach ($http_response_header ?? [] as $h) {
|
| 47 |
-
if (preg_match('#HTTP/\S+\s+(\d+)#', $h, $m)) $codigo = (int)$m[1];
|
| 48 |
-
}
|
| 49 |
-
return ['body' => $body, 'codigo' => $codigo];
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
$cabeceras = ['User-Agent: Morphos/1.0 (mailto:ceo@equipamed.net)', 'Accept: application/json'];
|
| 53 |
-
|
| 54 |
-
$urlBusqueda = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi'
|
| 55 |
-
. '?db=pubmed&retmode=json&retmax=100&term=' . urlencode($consulta);
|
| 56 |
-
|
| 57 |
-
$resp = fetchHttp($urlBusqueda, $cabeceras);
|
| 58 |
-
if ($resp['body'] === false || $resp['codigo'] >= 400) {
|
| 59 |
-
http_response_code(502);
|
| 60 |
-
echo json_encode(['error' => 'No se pudo contactar PubMed.']);
|
| 61 |
-
exit;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
$busqueda = json_decode($resp['body'], true);
|
| 65 |
-
$ids = $busqueda['esearchresult']['idlist'] ?? [];
|
| 66 |
-
|
| 67 |
-
if (empty($ids)) {
|
| 68 |
-
$salida = json_encode(['total' => 0, 'data' => []]);
|
| 69 |
-
escribirCache($claveCache, $salida);
|
| 70 |
-
echo $salida;
|
| 71 |
-
exit;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
$urlResumen = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi'
|
| 75 |
-
. '?db=pubmed&retmode=json&id=' . implode(',', $ids);
|
| 76 |
-
|
| 77 |
-
$resp2 = fetchHttp($urlResumen, $cabeceras);
|
| 78 |
-
if ($resp2['body'] === false || $resp2['codigo'] >= 400) {
|
| 79 |
-
http_response_code(502);
|
| 80 |
-
echo json_encode(['error' => 'No se pudo obtener los resultados de PubMed.']);
|
| 81 |
-
exit;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
$resumen = json_decode($resp2['body'], true);
|
| 85 |
-
$resultado = $resumen['result'] ?? [];
|
| 86 |
-
$uids = $resultado['uids'] ?? $ids;
|
| 87 |
-
|
| 88 |
-
$papers = [];
|
| 89 |
-
foreach ($uids as $uid) {
|
| 90 |
-
$p = $resultado[$uid] ?? null;
|
| 91 |
-
if (!$p) continue;
|
| 92 |
-
|
| 93 |
-
$anio = '';
|
| 94 |
-
if (!empty($p['pubdate'])) { preg_match('/\d{4}/', $p['pubdate'], $m); $anio = $m[0] ?? ''; }
|
| 95 |
-
|
| 96 |
-
$doi = '';
|
| 97 |
-
foreach ($p['articleids'] ?? [] as $aid) {
|
| 98 |
-
if ($aid['idtype'] === 'doi') { $doi = $aid['value']; break; }
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
$papers[] = [
|
| 102 |
-
'pmid' => $uid,
|
| 103 |
-
'title' => $p['title'] ?? 'Sin título',
|
| 104 |
-
'authors' => array_map(fn($a) => ['name' => $a['name']], $p['authors'] ?? []),
|
| 105 |
-
'year' => $anio,
|
| 106 |
-
'doi' => $doi,
|
| 107 |
-
'journal' => $p['source'] ?? '',
|
| 108 |
-
];
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
$salida = json_encode(['total' => count($papers), 'data' => $papers]);
|
| 112 |
-
escribirCache($claveCache, $salida);
|
| 113 |
-
echo $salida;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/setup.php
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
<?php
|
| 2 |
-
|
| 3 |
-
header('Content-Type: text/html; charset=utf-8');
|
| 4 |
-
|
| 5 |
-
$dbHost = '127.0.0.1';
|
| 6 |
-
$dbUsuario = 'root';
|
| 7 |
-
$dbClave = '';
|
| 8 |
-
$dbNombre = 'morphos_db';
|
| 9 |
-
$dbPort = 3306;
|
| 10 |
-
|
| 11 |
-
foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
| 12 |
-
if (str_starts_with($line, 'DB_PORT=')) $dbPort = (int) trim(substr($line, 8));
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
try {
|
| 16 |
-
$conexion = new PDO("mysql:host=$dbHost;port=$dbPort;charset=utf8mb4", $dbUsuario, $dbClave);
|
| 17 |
-
$conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
| 18 |
-
|
| 19 |
-
$conexion->exec("CREATE DATABASE IF NOT EXISTS `$dbNombre`");
|
| 20 |
-
$conexion->exec("USE `$dbNombre`");
|
| 21 |
-
$conexion->exec("
|
| 22 |
-
CREATE TABLE IF NOT EXISTS usuarios (
|
| 23 |
-
id INT AUTO_INCREMENT PRIMARY KEY,
|
| 24 |
-
nombre VARCHAR(100) NOT NULL,
|
| 25 |
-
apellido VARCHAR(100) NOT NULL,
|
| 26 |
-
email VARCHAR(150) NOT NULL UNIQUE,
|
| 27 |
-
password VARCHAR(255) NOT NULL,
|
| 28 |
-
creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 29 |
-
)
|
| 30 |
-
");
|
| 31 |
-
|
| 32 |
-
echo "<p style='font-family:sans-serif;color:green'>✓ Base de datos <strong>$dbNombre</strong> y tabla <strong>usuarios</strong> listas.</p>";
|
| 33 |
-
echo "<p style='font-family:sans-serif'><a href='../index.html'>← Volver a Morphos</a></p>";
|
| 34 |
-
|
| 35 |
-
} catch (PDOException $e) {
|
| 36 |
-
echo "<p style='font-family:sans-serif;color:red'>Error: " . htmlspecialchars($e->getMessage()) . "</p>";
|
| 37 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/analisis.js
DELETED
|
@@ -1,817 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
// Gravedad
|
| 3 |
-
// La desviación se mide en múltiplos del ancho del rango de referencia.
|
| 4 |
-
// Ej: rango WBC 6-17 (ancho = 11). WBC = 28 → desviación = 11/11 = 1.0 → moderado.
|
| 5 |
-
|
| 6 |
-
const UMBRALES_GRAVEDAD = { leve: 0.5, moderado: 1.5 };
|
| 7 |
-
|
| 8 |
-
const clasificarGravedad = (valor, ref) => {
|
| 9 |
-
// Mide cuantos anchos de rango de referencia se desvia el valor
|
| 10 |
-
const rango = ref.superior - ref.inferior;
|
| 11 |
-
const desviacion = valor > ref.superior
|
| 12 |
-
? (valor - ref.superior) / rango
|
| 13 |
-
: (ref.inferior - valor) / rango;
|
| 14 |
-
|
| 15 |
-
if (desviacion <= UMBRALES_GRAVEDAD.leve) return 'leve';
|
| 16 |
-
if (desviacion <= UMBRALES_GRAVEDAD.moderado) return 'moderado';
|
| 17 |
-
return 'grave';
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
// Edad
|
| 21 |
-
|
| 22 |
-
const categorizarEdad = (edadMeses, especie) => {
|
| 23 |
-
if (edadMeses === null) return 'adulto';
|
| 24 |
-
|
| 25 |
-
if (especie === 'canino') {
|
| 26 |
-
if (edadMeses < 12) return 'cachorro';
|
| 27 |
-
if (edadMeses < 84) return 'adulto';
|
| 28 |
-
if (edadMeses < 120) return 'senior';
|
| 29 |
-
return 'geriatrico';
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
// felino
|
| 33 |
-
if (edadMeses < 12) return 'cachorro';
|
| 34 |
-
if (edadMeses < 120) return 'adulto';
|
| 35 |
-
return 'senior';
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
// Ajustes por edad
|
| 40 |
-
|
| 41 |
-
const AJUSTES_EDAD = {
|
| 42 |
-
canino: {
|
| 43 |
-
cachorro: { fal: { superior: 3.0 }, wbc: { superior: 1.25 } },
|
| 44 |
-
adulto: {},
|
| 45 |
-
senior: { bun: { superior: 1.15 }, creat: { superior: 1.15 } },
|
| 46 |
-
geriatrico: { bun: { superior: 1.25 }, creat: { superior: 1.25 }, fal: { superior: 1.40 } }
|
| 47 |
-
},
|
| 48 |
-
felino: {
|
| 49 |
-
cachorro: { fal: { superior: 2.0 }, wbc: { superior: 1.20 } },
|
| 50 |
-
adulto: {},
|
| 51 |
-
senior: { bun: { superior: 1.20 }, creat: { superior: 1.20 } }
|
| 52 |
-
}
|
| 53 |
-
};
|
| 54 |
-
|
| 55 |
-
// Ajustes por raza
|
| 56 |
-
|
| 57 |
-
const AJUSTES_RAZA = {
|
| 58 |
-
canino: [
|
| 59 |
-
{
|
| 60 |
-
razas: ['galgo', 'greyhound', 'whippet', 'lebrel'],
|
| 61 |
-
ajustes: {
|
| 62 |
-
rbc: { inferior: 1.15, superior: 1.15 },
|
| 63 |
-
hgb: { inferior: 1.12, superior: 1.12 },
|
| 64 |
-
hct: { inferior: 1.12, superior: 1.12 },
|
| 65 |
-
plt: { inferior: 0.75, superior: 0.75 }
|
| 66 |
-
}
|
| 67 |
-
},
|
| 68 |
-
{
|
| 69 |
-
razas: ['shiba', 'akita'],
|
| 70 |
-
ajustes: {
|
| 71 |
-
rbc: { inferior: 1.10, superior: 1.10 },
|
| 72 |
-
hct: { inferior: 1.08, superior: 1.08 },
|
| 73 |
-
hgb: { inferior: 1.08, superior: 1.08 }
|
| 74 |
-
}
|
| 75 |
-
}
|
| 76 |
-
]
|
| 77 |
-
};
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
// Ajustes por sexo
|
| 81 |
-
|
| 82 |
-
const AJUSTES_SEXO = {
|
| 83 |
-
felino: {
|
| 84 |
-
Macho: { creat: { superior: 1.15 } }
|
| 85 |
-
}
|
| 86 |
-
};
|
| 87 |
-
|
| 88 |
-
const obtenerAjustesRaza = (raza, especie) => {
|
| 89 |
-
const razaNorm = raza?.toLowerCase().trim() ?? '';
|
| 90 |
-
const grupos = AJUSTES_RAZA[especie] ?? [];
|
| 91 |
-
return grupos.find(g => g.razas.some(r => razaNorm.includes(r)))?.ajustes ?? {};
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
// Ajuste de referencias
|
| 95 |
-
|
| 96 |
-
const ajustarReferencias = (refsEspecie, paciente) => {
|
| 97 |
-
const catEdad = categorizarEdad(paciente.edadMeses, paciente.especie);
|
| 98 |
-
const ajEdad = AJUSTES_EDAD[paciente.especie]?.[catEdad] ?? {};
|
| 99 |
-
const ajRaza = obtenerAjustesRaza(paciente.raza, paciente.especie);
|
| 100 |
-
const ajSexo = AJUSTES_SEXO[paciente.especie]?.[paciente.sexo] ?? {};
|
| 101 |
-
|
| 102 |
-
// Multiplica los limites inferiores y superiores por los factores de edad, raza y sexo
|
| 103 |
-
return Object.entries(refsEspecie).reduce((acc, [clave, ref]) => {
|
| 104 |
-
const factorEdad = ajEdad[clave] ?? {};
|
| 105 |
-
const factorRaza = ajRaza[clave] ?? {};
|
| 106 |
-
const factorSexo = ajSexo[clave] ?? {};
|
| 107 |
-
|
| 108 |
-
acc[clave] = {
|
| 109 |
-
...ref,
|
| 110 |
-
inferior: ref.inferior * (factorEdad.inferior ?? 1) * (factorRaza.inferior ?? 1) * (factorSexo.inferior ?? 1),
|
| 111 |
-
superior: ref.superior * (factorEdad.superior ?? 1) * (factorRaza.superior ?? 1) * (factorSexo.superior ?? 1)
|
| 112 |
-
};
|
| 113 |
-
return acc;
|
| 114 |
-
}, {});
|
| 115 |
-
};
|
| 116 |
-
|
| 117 |
-
// Detección de patrones clínicos
|
| 118 |
-
|
| 119 |
-
const detectarPatrones = (hallazgos, especie, alt) => {
|
| 120 |
-
const mapa = hallazgos.reduce((acc, h) => { acc[h.clave] = h; return acc; }, {});
|
| 121 |
-
|
| 122 |
-
const esAlto = (clave) => mapa[clave]?.direccion === 'alto';
|
| 123 |
-
const esBajo = (clave) => mapa[clave]?.direccion === 'bajo';
|
| 124 |
-
const presente = (clave) => clave in mapa;
|
| 125 |
-
const valor = (clave) => mapa[clave]?.valor ?? null;
|
| 126 |
-
|
| 127 |
-
const gravedadDe = (...claves) => {
|
| 128 |
-
const clave = claves.find(c => mapa[c]);
|
| 129 |
-
return mapa[clave]?.gravedad ?? 'leve';
|
| 130 |
-
};
|
| 131 |
-
|
| 132 |
-
const patrones = [];
|
| 133 |
-
const agregar = (patron) => patrones.push(patron);
|
| 134 |
-
|
| 135 |
-
// Serie roja
|
| 136 |
-
|
| 137 |
-
if (esBajo('hct') || esBajo('hgb') || esBajo('rbc')) {
|
| 138 |
-
// Clasifica el tipo de anemia segun el VCM para sugerir la etiologia mas probable
|
| 139 |
-
const tipoPorVcm = !presente('vcm') ? '' :
|
| 140 |
-
esBajo('vcm') ? 'microcítica' :
|
| 141 |
-
esAlto('vcm') ? 'macrocítica' : 'normocítica';
|
| 142 |
-
|
| 143 |
-
const claveEtiologia = esBajo('vcm') ? 'ferropenia' :
|
| 144 |
-
esAlto('vcm') ? 'macrocitica' :
|
| 145 |
-
tipoPorVcm === 'normocítica' ? 'normocitica' : null;
|
| 146 |
-
const etiologia = claveEtiologia ? alt.anemia.etiologias?.[claveEtiologia] ?? '' : '';
|
| 147 |
-
|
| 148 |
-
agregar({
|
| 149 |
-
nombre: `${alt.anemia.nombre}${tipoPorVcm ? ` ${tipoPorVcm}` : ''}`,
|
| 150 |
-
descripcion: [alt.anemia.prefijo, etiologia].filter(Boolean).join(' '),
|
| 151 |
-
gravedad: gravedadDe('hct', 'hgb', 'rbc'),
|
| 152 |
-
parametros: ['hct', 'hgb', 'rbc', 'vcm'].filter(presente)
|
| 153 |
-
});
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
if (esAlto('hct') || esAlto('rbc')) agregar({
|
| 157 |
-
nombre: alt.eritrocitosis.nombre,
|
| 158 |
-
descripcion: alt.eritrocitosis.descripcion,
|
| 159 |
-
gravedad: gravedadDe('hct', 'rbc'),
|
| 160 |
-
parametros: ['hct', 'rbc', 'hgb'].filter(presente)
|
| 161 |
-
});
|
| 162 |
-
|
| 163 |
-
// Serie blanca
|
| 164 |
-
|
| 165 |
-
if (esAlto('wbc')) {
|
| 166 |
-
// Diferencia leucocitosis neutrofilica de linfocitica; si no hay diferencial, informa generico
|
| 167 |
-
const neutrofilia = esAlto('neutro');
|
| 168 |
-
const linfocitosis = esAlto('linfo');
|
| 169 |
-
|
| 170 |
-
if (neutrofilia) agregar({
|
| 171 |
-
nombre: alt.leucocitosis_neutrofilica.nombre,
|
| 172 |
-
descripcion: alt.leucocitosis_neutrofilica.descripcion,
|
| 173 |
-
gravedad: gravedadDe('wbc', 'neutro'),
|
| 174 |
-
parametros: ['wbc', 'neutro'].filter(presente)
|
| 175 |
-
});
|
| 176 |
-
|
| 177 |
-
if (linfocitosis) agregar({
|
| 178 |
-
nombre: alt.leucocitosis_linfocitica.nombre,
|
| 179 |
-
descripcion: alt.leucocitosis_linfocitica.descripcion,
|
| 180 |
-
gravedad: gravedadDe('wbc', 'linfo'),
|
| 181 |
-
parametros: ['wbc', 'linfo'].filter(presente)
|
| 182 |
-
});
|
| 183 |
-
|
| 184 |
-
if (!neutrofilia && !linfocitosis) agregar({
|
| 185 |
-
nombre: alt.leucocitosis.nombre,
|
| 186 |
-
descripcion: alt.leucocitosis.descripcion,
|
| 187 |
-
gravedad: gravedadDe('wbc'),
|
| 188 |
-
parametros: ['wbc']
|
| 189 |
-
});
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
if (esBajo('wbc')) agregar({
|
| 193 |
-
nombre: alt.leucopenia.nombre,
|
| 194 |
-
descripcion: alt.leucopenia.descripcion,
|
| 195 |
-
gravedad: gravedadDe('wbc'),
|
| 196 |
-
parametros: ['wbc']
|
| 197 |
-
});
|
| 198 |
-
|
| 199 |
-
if (esBajo('neutro')) agregar({
|
| 200 |
-
nombre: alt.neutropenia.nombre,
|
| 201 |
-
descripcion: alt.neutropenia.descripcion,
|
| 202 |
-
gravedad: gravedadDe('neutro'),
|
| 203 |
-
parametros: ['neutro']
|
| 204 |
-
});
|
| 205 |
-
|
| 206 |
-
if (esBajo('linfo')) agregar({
|
| 207 |
-
nombre: alt.linfopenia.nombre,
|
| 208 |
-
descripcion: alt.linfopenia.descripcion,
|
| 209 |
-
gravedad: gravedadDe('linfo'),
|
| 210 |
-
parametros: ['linfo']
|
| 211 |
-
});
|
| 212 |
-
|
| 213 |
-
if (esAlto('eosino')) agregar({
|
| 214 |
-
nombre: alt.eosinofilia.nombre,
|
| 215 |
-
descripcion: alt.eosinofilia.descripcion,
|
| 216 |
-
gravedad: gravedadDe('eosino'),
|
| 217 |
-
parametros: ['eosino']
|
| 218 |
-
});
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
// Plaquetas
|
| 222 |
-
|
| 223 |
-
if (esBajo('plt')) agregar({
|
| 224 |
-
nombre: alt.trombocitopenia.nombre,
|
| 225 |
-
descripcion: alt.trombocitopenia.descripcion,
|
| 226 |
-
gravedad: gravedadDe('plt'),
|
| 227 |
-
parametros: ['plt']
|
| 228 |
-
});
|
| 229 |
-
|
| 230 |
-
if (esAlto('plt')) agregar({
|
| 231 |
-
nombre: alt.trombocitosis.nombre,
|
| 232 |
-
descripcion: alt.trombocitosis.descripcion,
|
| 233 |
-
gravedad: gravedadDe('plt'),
|
| 234 |
-
parametros: ['plt']
|
| 235 |
-
});
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
// Hígado
|
| 239 |
-
|
| 240 |
-
if (esAlto('alt') && esAlto('ast')) agregar({
|
| 241 |
-
nombre: alt.dano_hepatocelular.nombre,
|
| 242 |
-
descripcion: alt.dano_hepatocelular.descripcion,
|
| 243 |
-
gravedad: gravedadDe('alt', 'ast'),
|
| 244 |
-
parametros: ['alt', 'ast'].filter(presente)
|
| 245 |
-
});
|
| 246 |
-
else if (esAlto('alt')) agregar({
|
| 247 |
-
nombre: alt.alt_aislada.nombre,
|
| 248 |
-
descripcion: alt.alt_aislada.descripcion,
|
| 249 |
-
gravedad: gravedadDe('alt'),
|
| 250 |
-
parametros: ['alt']
|
| 251 |
-
});
|
| 252 |
-
|
| 253 |
-
if (esAlto('fal')) agregar({
|
| 254 |
-
nombre: alt.patron_colestasico.nombre,
|
| 255 |
-
descripcion: alt.patron_colestasico.descripcion[especie] ?? alt.patron_colestasico.descripcion.canino,
|
| 256 |
-
gravedad: gravedadDe('fal'),
|
| 257 |
-
parametros: ['fal']
|
| 258 |
-
});
|
| 259 |
-
|
| 260 |
-
if (esAlto('bili')) agregar({
|
| 261 |
-
nombre: alt.hiperbilirrubinemia.nombre,
|
| 262 |
-
descripcion: alt.hiperbilirrubinemia.descripcion,
|
| 263 |
-
gravedad: gravedadDe('bili'),
|
| 264 |
-
parametros: ['bili']
|
| 265 |
-
});
|
| 266 |
-
|
| 267 |
-
// Riñón
|
| 268 |
-
|
| 269 |
-
if (esAlto('bun') && esAlto('creat')) agregar({
|
| 270 |
-
nombre: alt.azotemia.nombre,
|
| 271 |
-
descripcion: alt.azotemia.descripcion,
|
| 272 |
-
gravedad: gravedadDe('creat', 'bun'),
|
| 273 |
-
parametros: ['bun', 'creat'].filter(presente)
|
| 274 |
-
});
|
| 275 |
-
else if (esAlto('bun')) agregar({
|
| 276 |
-
nombre: alt.hiperuremia_bun.nombre,
|
| 277 |
-
descripcion: alt.hiperuremia_bun.descripcion,
|
| 278 |
-
gravedad: gravedadDe('bun'),
|
| 279 |
-
parametros: ['bun']
|
| 280 |
-
});
|
| 281 |
-
else if (esAlto('creat')) agregar({
|
| 282 |
-
nombre: alt.creatinina_aislada.nombre,
|
| 283 |
-
descripcion: alt.creatinina_aislada.descripcion,
|
| 284 |
-
gravedad: gravedadDe('creat'),
|
| 285 |
-
parametros: ['creat']
|
| 286 |
-
});
|
| 287 |
-
|
| 288 |
-
if (esBajo('bun')) agregar({
|
| 289 |
-
nombre: alt.bun_disminuido.nombre,
|
| 290 |
-
descripcion: alt.bun_disminuido.descripcion,
|
| 291 |
-
gravedad: gravedadDe('bun'),
|
| 292 |
-
parametros: ['bun']
|
| 293 |
-
});
|
| 294 |
-
|
| 295 |
-
// Glucosa
|
| 296 |
-
|
| 297 |
-
if (esAlto('gluc')) agregar({
|
| 298 |
-
nombre: alt.hiperglucemia.nombre,
|
| 299 |
-
descripcion: alt.hiperglucemia.descripcion[especie] ?? alt.hiperglucemia.descripcion.canino,
|
| 300 |
-
gravedad: gravedadDe('gluc'),
|
| 301 |
-
parametros: ['gluc']
|
| 302 |
-
});
|
| 303 |
-
|
| 304 |
-
if (esBajo('gluc')) agregar({
|
| 305 |
-
nombre: alt.hipoglucemia.nombre,
|
| 306 |
-
descripcion: alt.hipoglucemia.descripcion,
|
| 307 |
-
gravedad: gravedadDe('gluc'),
|
| 308 |
-
parametros: ['gluc']
|
| 309 |
-
});
|
| 310 |
-
|
| 311 |
-
// Proteínas
|
| 312 |
-
|
| 313 |
-
if (esAlto('prot')) agregar({
|
| 314 |
-
nombre: alt.hiperproteinemia.nombre,
|
| 315 |
-
descripcion: alt.hiperproteinemia.descripcion,
|
| 316 |
-
gravedad: gravedadDe('prot'),
|
| 317 |
-
parametros: ['prot']
|
| 318 |
-
});
|
| 319 |
-
|
| 320 |
-
if (esBajo('alb')) {
|
| 321 |
-
const hipoproteinemia = esBajo('prot');
|
| 322 |
-
const claveAlteracion = hipoproteinemia ? 'hipoproteinemia_hipoalbuminemia' : 'hipoalbuminemia';
|
| 323 |
-
agregar({
|
| 324 |
-
nombre: alt[claveAlteracion].nombre,
|
| 325 |
-
descripcion: alt[claveAlteracion].descripcion,
|
| 326 |
-
gravedad: gravedadDe('alb'),
|
| 327 |
-
parametros: ['alb', ...(hipoproteinemia ? ['prot'] : [])].filter(presente)
|
| 328 |
-
});
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
// Electrolitos
|
| 332 |
-
|
| 333 |
-
const valSodio = valor('sodio');
|
| 334 |
-
const valPotasio = valor('potasio');
|
| 335 |
-
|
| 336 |
-
// Ratio Na/K < 27 es sugestivo de hipoadrenocorticismo; la gravedad aumenta a menor ratio
|
| 337 |
-
if (valSodio !== null && valPotasio !== null && valPotasio > 0) {
|
| 338 |
-
const ratioNaK = valSodio / valPotasio;
|
| 339 |
-
if (ratioNaK < 27) agregar({
|
| 340 |
-
nombre: alt.ratio_nak.nombre,
|
| 341 |
-
descripcion: alt.ratio_nak.descripcion.replace('{ratio}', ratioNaK.toFixed(1)),
|
| 342 |
-
gravedad: ratioNaK < 20 ? 'grave' : ratioNaK < 24 ? 'moderado' : 'leve',
|
| 343 |
-
parametros: ['sodio', 'potasio'].filter(presente)
|
| 344 |
-
});
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
if (esAlto('sodio')) agregar({
|
| 348 |
-
nombre: alt.hipernatremia.nombre,
|
| 349 |
-
descripcion: alt.hipernatremia.descripcion,
|
| 350 |
-
gravedad: gravedadDe('sodio'),
|
| 351 |
-
parametros: ['sodio']
|
| 352 |
-
});
|
| 353 |
-
|
| 354 |
-
if (esBajo('sodio')) agregar({
|
| 355 |
-
nombre: alt.hiponatremia.nombre,
|
| 356 |
-
descripcion: alt.hiponatremia.descripcion,
|
| 357 |
-
gravedad: gravedadDe('sodio'),
|
| 358 |
-
parametros: ['sodio']
|
| 359 |
-
});
|
| 360 |
-
|
| 361 |
-
if (esAlto('calc')) agregar({
|
| 362 |
-
nombre: alt.hipercalcemia.nombre,
|
| 363 |
-
descripcion: alt.hipercalcemia.descripcion,
|
| 364 |
-
gravedad: gravedadDe('calc'),
|
| 365 |
-
parametros: ['calc']
|
| 366 |
-
});
|
| 367 |
-
|
| 368 |
-
if (esBajo('calc')) agregar({
|
| 369 |
-
nombre: alt.hipocalcemia.nombre,
|
| 370 |
-
descripcion: alt.hipocalcemia.descripcion,
|
| 371 |
-
gravedad: gravedadDe('calc'),
|
| 372 |
-
parametros: ['calc']
|
| 373 |
-
});
|
| 374 |
-
|
| 375 |
-
if (esBajo('potasio')) agregar({
|
| 376 |
-
nombre: alt.hipopotasemia.nombre,
|
| 377 |
-
descripcion: alt.hipopotasemia.descripcion,
|
| 378 |
-
gravedad: gravedadDe('potasio'),
|
| 379 |
-
parametros: ['potasio']
|
| 380 |
-
});
|
| 381 |
-
|
| 382 |
-
if (esAlto('potasio')) agregar({
|
| 383 |
-
nombre: alt.hiperpotasemia.nombre,
|
| 384 |
-
descripcion: alt.hiperpotasemia.descripcion,
|
| 385 |
-
gravedad: gravedadDe('potasio'),
|
| 386 |
-
parametros: ['potasio']
|
| 387 |
-
});
|
| 388 |
-
|
| 389 |
-
if (esAlto('fosf')) agregar({
|
| 390 |
-
nombre: alt.hiperfosforemia.nombre,
|
| 391 |
-
descripcion: alt.hiperfosforemia.descripcion,
|
| 392 |
-
gravedad: gravedadDe('fosf'),
|
| 393 |
-
parametros: ['fosf']
|
| 394 |
-
});
|
| 395 |
-
|
| 396 |
-
// Urianálisis
|
| 397 |
-
|
| 398 |
-
const valUsg = valor('usg');
|
| 399 |
-
if (valUsg !== null && valUsg < 1.008) agregar({
|
| 400 |
-
nombre: alt.hiposthenuria.nombre,
|
| 401 |
-
descripcion: alt.hiposthenuria.descripcion,
|
| 402 |
-
gravedad: valUsg < 1.005 ? 'grave' : 'moderado',
|
| 403 |
-
parametros: ['usg']
|
| 404 |
-
});
|
| 405 |
-
else if (valUsg !== null && valUsg < 1.013) agregar({
|
| 406 |
-
nombre: alt.isosthenuria.nombre,
|
| 407 |
-
descripcion: alt.isosthenuria.descripcion,
|
| 408 |
-
gravedad: 'leve',
|
| 409 |
-
parametros: ['usg']
|
| 410 |
-
});
|
| 411 |
-
|
| 412 |
-
// Tiroides
|
| 413 |
-
|
| 414 |
-
if (especie === 'canino' && esBajo('t4_total')) agregar({
|
| 415 |
-
nombre: alt.hipotiroidismo.nombre,
|
| 416 |
-
descripcion: alt.hipotiroidismo.descripcion.canino,
|
| 417 |
-
gravedad: gravedadDe('t4_total'),
|
| 418 |
-
parametros: ['t4_total'].filter(presente)
|
| 419 |
-
});
|
| 420 |
-
|
| 421 |
-
if (esAlto('t4_total')) agregar({
|
| 422 |
-
nombre: alt.hipertiroidismo.nombre,
|
| 423 |
-
descripcion: alt.hipertiroidismo.descripcion[especie] ?? alt.hipertiroidismo.descripcion.felino,
|
| 424 |
-
gravedad: gravedadDe('t4_total'),
|
| 425 |
-
parametros: ['t4_total'].filter(presente)
|
| 426 |
-
});
|
| 427 |
-
|
| 428 |
-
// Suprarrenal / Cortisol
|
| 429 |
-
|
| 430 |
-
if (esAlto('cortisol_acth')) agregar({
|
| 431 |
-
nombre: alt.hiperadrenocorticismo.nombre,
|
| 432 |
-
descripcion: alt.hiperadrenocorticismo.descripcion[especie] ?? alt.hiperadrenocorticismo.descripcion.canino,
|
| 433 |
-
gravedad: gravedadDe('cortisol_acth'),
|
| 434 |
-
parametros: ['cortisol_acth', ...(presente('cortisol_bas') ? ['cortisol_bas'] : [])]
|
| 435 |
-
});
|
| 436 |
-
|
| 437 |
-
if (esBajo('cortisol_acth')) agregar({
|
| 438 |
-
nombre: alt.hipoadrenocorticismo_cortisol.nombre,
|
| 439 |
-
descripcion: alt.hipoadrenocorticismo_cortisol.descripcion,
|
| 440 |
-
gravedad: gravedadDe('cortisol_acth'),
|
| 441 |
-
parametros: ['cortisol_acth', ...(presente('cortisol_bas') ? ['cortisol_bas'] : [])]
|
| 442 |
-
});
|
| 443 |
-
|
| 444 |
-
if (esBajo('cortisol_bas') && !presente('cortisol_acth')) agregar({
|
| 445 |
-
nombre: alt.cortisol_basal_bajo.nombre,
|
| 446 |
-
descripcion: alt.cortisol_basal_bajo.descripcion,
|
| 447 |
-
gravedad: 'moderado',
|
| 448 |
-
parametros: ['cortisol_bas']
|
| 449 |
-
});
|
| 450 |
-
|
| 451 |
-
// Insulina
|
| 452 |
-
|
| 453 |
-
if (esBajo('insulina') && esAlto('gluc')) agregar({
|
| 454 |
-
nombre: alt.deficit_insulina.nombre,
|
| 455 |
-
descripcion: alt.deficit_insulina.descripcion,
|
| 456 |
-
gravedad: 'moderado',
|
| 457 |
-
parametros: ['insulina', 'gluc'].filter(presente)
|
| 458 |
-
});
|
| 459 |
-
|
| 460 |
-
// Páncreas exocrino (PLI)
|
| 461 |
-
|
| 462 |
-
if (esAlto('pli')) agregar({
|
| 463 |
-
nombre: alt.pancreatitis.nombre,
|
| 464 |
-
descripcion: alt.pancreatitis.descripcion[especie] ?? alt.pancreatitis.descripcion.canino,
|
| 465 |
-
gravedad: gravedadDe('pli'),
|
| 466 |
-
parametros: ['pli', ...(presente('lipasa') ? ['lipasa'] : []), ...(presente('amylasa') ? ['amylasa'] : [])].filter(presente)
|
| 467 |
-
});
|
| 468 |
-
|
| 469 |
-
if (esAlto('amylasa') && !presente('pli')) agregar({
|
| 470 |
-
nombre: alt.hiperamylasemia.nombre,
|
| 471 |
-
descripcion: alt.hiperamylasemia.descripcion,
|
| 472 |
-
gravedad: gravedadDe('amylasa'),
|
| 473 |
-
parametros: ['amylasa']
|
| 474 |
-
});
|
| 475 |
-
|
| 476 |
-
// Tiroides — TSH
|
| 477 |
-
|
| 478 |
-
if (esAlto('tsh')) agregar({
|
| 479 |
-
nombre: alt.tsh_elevado.nombre,
|
| 480 |
-
descripcion: alt.tsh_elevado.descripcion[especie] ?? alt.tsh_elevado.descripcion.canino,
|
| 481 |
-
gravedad: gravedadDe('tsh'),
|
| 482 |
-
parametros: ['tsh', ...(presente('t4_total') ? ['t4_total'] : []), ...(presente('t4_libre') ? ['t4_libre'] : [])].filter(presente)
|
| 483 |
-
});
|
| 484 |
-
|
| 485 |
-
if (esBajo('tsh')) agregar({
|
| 486 |
-
nombre: alt.tsh_suprimido.nombre,
|
| 487 |
-
descripcion: alt.tsh_suprimido.descripcion[especie] ?? alt.tsh_suprimido.descripcion.canino,
|
| 488 |
-
gravedad: gravedadDe('tsh'),
|
| 489 |
-
parametros: ['tsh', ...(presente('t4_total') ? ['t4_total'] : [])].filter(presente)
|
| 490 |
-
});
|
| 491 |
-
|
| 492 |
-
if (esBajo('t4_libre') && !presente('tsh')) agregar({
|
| 493 |
-
nombre: alt.t4_libre_baja.nombre,
|
| 494 |
-
descripcion: alt.t4_libre_baja.descripcion[especie] ?? alt.t4_libre_baja.descripcion.canino,
|
| 495 |
-
gravedad: gravedadDe('t4_libre'),
|
| 496 |
-
parametros: ['t4_libre', ...(presente('t4_total') ? ['t4_total'] : [])].filter(presente)
|
| 497 |
-
});
|
| 498 |
-
|
| 499 |
-
// Biomarcadores cardíacos
|
| 500 |
-
|
| 501 |
-
if (esAlto('ctni')) agregar({
|
| 502 |
-
nombre: alt.dano_miocardico.nombre,
|
| 503 |
-
descripcion: alt.dano_miocardico.descripcion,
|
| 504 |
-
gravedad: gravedadDe('ctni'),
|
| 505 |
-
parametros: ['ctni', ...(presente('nt_probnp') ? ['nt_probnp'] : [])].filter(presente)
|
| 506 |
-
});
|
| 507 |
-
|
| 508 |
-
if (esAlto('nt_probnp')) agregar({
|
| 509 |
-
nombre: alt.cardiopatia_bnp.nombre,
|
| 510 |
-
descripcion: alt.cardiopatia_bnp.descripcion[especie] ?? alt.cardiopatia_bnp.descripcion.canino,
|
| 511 |
-
gravedad: gravedadDe('nt_probnp'),
|
| 512 |
-
parametros: ['nt_probnp', ...(presente('ctni') ? ['ctni'] : [])].filter(presente)
|
| 513 |
-
});
|
| 514 |
-
|
| 515 |
-
// Proteínas de fase aguda
|
| 516 |
-
|
| 517 |
-
if (esAlto('crp') || esAlto('saa')) agregar({
|
| 518 |
-
nombre: alt.inflamacion_aguda.nombre,
|
| 519 |
-
descripcion: alt.inflamacion_aguda.descripcion[especie] ?? alt.inflamacion_aguda.descripcion.canino,
|
| 520 |
-
gravedad: gravedadDe('crp', 'saa'),
|
| 521 |
-
parametros: ['crp', 'saa'].filter(presente)
|
| 522 |
-
});
|
| 523 |
-
|
| 524 |
-
// Progesterona
|
| 525 |
-
|
| 526 |
-
if (esAlto('progesterona')) agregar({
|
| 527 |
-
nombre: alt.progesterona_elevada.nombre,
|
| 528 |
-
descripcion: alt.progesterona_elevada.descripcion[especie] ?? alt.progesterona_elevada.descripcion.canino,
|
| 529 |
-
gravedad: gravedadDe('progesterona'),
|
| 530 |
-
parametros: ['progesterona']
|
| 531 |
-
});
|
| 532 |
-
|
| 533 |
-
// Magnesio
|
| 534 |
-
|
| 535 |
-
if (esBajo('magnesio')) agregar({
|
| 536 |
-
nombre: alt.hipomagnesemia.nombre,
|
| 537 |
-
descripcion: alt.hipomagnesemia.descripcion,
|
| 538 |
-
gravedad: gravedadDe('magnesio'),
|
| 539 |
-
parametros: ['magnesio']
|
| 540 |
-
});
|
| 541 |
-
|
| 542 |
-
if (esAlto('magnesio')) agregar({
|
| 543 |
-
nombre: alt.hipermagnesemia.nombre,
|
| 544 |
-
descripcion: alt.hipermagnesemia.descripcion,
|
| 545 |
-
gravedad: gravedadDe('magnesio'),
|
| 546 |
-
parametros: ['magnesio']
|
| 547 |
-
});
|
| 548 |
-
|
| 549 |
-
// Hierro
|
| 550 |
-
|
| 551 |
-
if (esBajo('hierro')) agregar({
|
| 552 |
-
nombre: alt.ferropenia_hierro.nombre,
|
| 553 |
-
descripcion: alt.ferropenia_hierro.descripcion,
|
| 554 |
-
gravedad: gravedadDe('hierro'),
|
| 555 |
-
parametros: ['hierro']
|
| 556 |
-
});
|
| 557 |
-
|
| 558 |
-
// Ácido úrico
|
| 559 |
-
|
| 560 |
-
if (esAlto('ac_urico')) agregar({
|
| 561 |
-
nombre: alt.ac_urico_elevado.nombre,
|
| 562 |
-
descripcion: alt.ac_urico_elevado.descripcion,
|
| 563 |
-
gravedad: gravedadDe('ac_urico'),
|
| 564 |
-
parametros: ['ac_urico']
|
| 565 |
-
});
|
| 566 |
-
|
| 567 |
-
// LDH
|
| 568 |
-
|
| 569 |
-
if (esAlto('ldh')) agregar({
|
| 570 |
-
nombre: alt.ldh_elevada.nombre,
|
| 571 |
-
descripcion: alt.ldh_elevada.descripcion,
|
| 572 |
-
gravedad: gravedadDe('ldh'),
|
| 573 |
-
parametros: ['ldh']
|
| 574 |
-
});
|
| 575 |
-
|
| 576 |
-
// Monitorización de fármacos (TDM)
|
| 577 |
-
|
| 578 |
-
if (esBajo('fenobarbital')) agregar({
|
| 579 |
-
nombre: alt.fenobarbital_subterapeutico.nombre,
|
| 580 |
-
descripcion: alt.fenobarbital_subterapeutico.descripcion,
|
| 581 |
-
gravedad: gravedadDe('fenobarbital'),
|
| 582 |
-
parametros: ['fenobarbital']
|
| 583 |
-
});
|
| 584 |
-
|
| 585 |
-
if (esAlto('fenobarbital')) agregar({
|
| 586 |
-
nombre: alt.fenobarbital_toxico.nombre,
|
| 587 |
-
descripcion: alt.fenobarbital_toxico.descripcion,
|
| 588 |
-
gravedad: gravedadDe('fenobarbital'),
|
| 589 |
-
parametros: ['fenobarbital']
|
| 590 |
-
});
|
| 591 |
-
|
| 592 |
-
if (esBajo('ciclosporina')) agregar({
|
| 593 |
-
nombre: alt.ciclosporina_subterapeutica.nombre,
|
| 594 |
-
descripcion: alt.ciclosporina_subterapeutica.descripcion,
|
| 595 |
-
gravedad: gravedadDe('ciclosporina'),
|
| 596 |
-
parametros: ['ciclosporina']
|
| 597 |
-
});
|
| 598 |
-
|
| 599 |
-
if (esAlto('ciclosporina')) agregar({
|
| 600 |
-
nombre: alt.ciclosporina_toxica.nombre,
|
| 601 |
-
descripcion: alt.ciclosporina_toxica.descripcion,
|
| 602 |
-
gravedad: gravedadDe('ciclosporina'),
|
| 603 |
-
parametros: ['ciclosporina']
|
| 604 |
-
});
|
| 605 |
-
|
| 606 |
-
// Coagulación
|
| 607 |
-
|
| 608 |
-
if (esAlto('pt') && !esAlto('aptt')) agregar({
|
| 609 |
-
nombre: alt.coagulopatia_extrinseca.nombre,
|
| 610 |
-
descripcion: alt.coagulopatia_extrinseca.descripcion,
|
| 611 |
-
gravedad: gravedadDe('pt'),
|
| 612 |
-
parametros: ['pt']
|
| 613 |
-
});
|
| 614 |
-
|
| 615 |
-
if (esAlto('aptt') && !esAlto('pt')) agregar({
|
| 616 |
-
nombre: alt.coagulopatia_intrinseca.nombre,
|
| 617 |
-
descripcion: alt.coagulopatia_intrinseca.descripcion,
|
| 618 |
-
gravedad: gravedadDe('aptt'),
|
| 619 |
-
parametros: ['aptt']
|
| 620 |
-
});
|
| 621 |
-
|
| 622 |
-
if (esAlto('pt') && esAlto('aptt')) agregar({
|
| 623 |
-
nombre: alt.coagulopatia_mixta.nombre,
|
| 624 |
-
descripcion: alt.coagulopatia_mixta.descripcion,
|
| 625 |
-
gravedad: gravedadDe('pt', 'aptt', 'act'),
|
| 626 |
-
parametros: ['pt', 'aptt', ...(presente('act') ? ['act'] : [])].filter(presente)
|
| 627 |
-
});
|
| 628 |
-
|
| 629 |
-
if ((esAlto('ddimeros') || esAlto('fdp')) && esBajo('fibrinogeno')) agregar({
|
| 630 |
-
nombre: alt.cid.nombre,
|
| 631 |
-
descripcion: alt.cid.descripcion,
|
| 632 |
-
gravedad: 'grave',
|
| 633 |
-
parametros: ['ddimeros', 'fdp', 'fibrinogeno', 'plt'].filter(presente)
|
| 634 |
-
});
|
| 635 |
-
|
| 636 |
-
if (esAlto('fibrinogeno') && !esAlto('ddimeros') && !esAlto('fdp')) agregar({
|
| 637 |
-
nombre: alt.hiperfibrinogenemia.nombre,
|
| 638 |
-
descripcion: alt.hiperfibrinogenemia.descripcion,
|
| 639 |
-
gravedad: gravedadDe('fibrinogeno'),
|
| 640 |
-
parametros: ['fibrinogeno']
|
| 641 |
-
});
|
| 642 |
-
|
| 643 |
-
if (esBajo('fibrinogeno') && !esAlto('ddimeros') && !esAlto('fdp')) agregar({
|
| 644 |
-
nombre: alt.hipofibrinogenemia.nombre,
|
| 645 |
-
descripcion: alt.hipofibrinogenemia.descripcion,
|
| 646 |
-
gravedad: gravedadDe('fibrinogeno'),
|
| 647 |
-
parametros: ['fibrinogeno']
|
| 648 |
-
});
|
| 649 |
-
|
| 650 |
-
if (esBajo('vwf')) agregar({
|
| 651 |
-
nombre: alt.deficit_vwf.nombre,
|
| 652 |
-
descripcion: alt.deficit_vwf.descripcion,
|
| 653 |
-
gravedad: gravedadDe('vwf'),
|
| 654 |
-
parametros: ['vwf', ...(presente('aptt') ? ['aptt'] : [])].filter(presente)
|
| 655 |
-
});
|
| 656 |
-
|
| 657 |
-
if (esBajo('antitrombina')) agregar({
|
| 658 |
-
nombre: alt.antitrombina_baja.nombre,
|
| 659 |
-
descripcion: alt.antitrombina_baja.descripcion,
|
| 660 |
-
gravedad: gravedadDe('antitrombina'),
|
| 661 |
-
parametros: ['antitrombina']
|
| 662 |
-
});
|
| 663 |
-
|
| 664 |
-
// Urianálisis — sedimento / UPC
|
| 665 |
-
|
| 666 |
-
if (esAlto('rbc_uri')) agregar({
|
| 667 |
-
nombre: alt.hematuria_uri.nombre,
|
| 668 |
-
descripcion: alt.hematuria_uri.descripcion,
|
| 669 |
-
gravedad: gravedadDe('rbc_uri'),
|
| 670 |
-
parametros: ['rbc_uri']
|
| 671 |
-
});
|
| 672 |
-
|
| 673 |
-
if (esAlto('wbc_uri')) agregar({
|
| 674 |
-
nombre: alt.piuria.nombre,
|
| 675 |
-
descripcion: alt.piuria.descripcion,
|
| 676 |
-
gravedad: gravedadDe('wbc_uri'),
|
| 677 |
-
parametros: ['wbc_uri']
|
| 678 |
-
});
|
| 679 |
-
|
| 680 |
-
if (esAlto('upc')) agregar({
|
| 681 |
-
nombre: alt.proteinuria_upc.nombre,
|
| 682 |
-
descripcion: alt.proteinuria_upc.descripcion,
|
| 683 |
-
gravedad: gravedadDe('upc'),
|
| 684 |
-
parametros: ['upc']
|
| 685 |
-
});
|
| 686 |
-
|
| 687 |
-
// Gasometría — ácido-base
|
| 688 |
-
|
| 689 |
-
if (presente('ph_sangre')) {
|
| 690 |
-
const phBajo = esBajo('ph_sangre');
|
| 691 |
-
const phAlto = esAlto('ph_sangre');
|
| 692 |
-
const hipercarbia = esAlto('pco2');
|
| 693 |
-
const hipocarbia = esBajo('pco2');
|
| 694 |
-
const componenteAcidMet = esBajo('hco3') || esBajo('exceso_base');
|
| 695 |
-
const componenteAlcalMet = esAlto('hco3') || esAlto('exceso_base');
|
| 696 |
-
|
| 697 |
-
if (phBajo) {
|
| 698 |
-
if (hipercarbia && componenteAcidMet) {
|
| 699 |
-
agregar({
|
| 700 |
-
nombre: alt.acidosis_respiratoria.nombre + ' + ' + alt.acidosis_metabolica.nombre,
|
| 701 |
-
descripcion: alt.acidosis_metabolica.descripcion,
|
| 702 |
-
gravedad: 'grave',
|
| 703 |
-
parametros: ['ph_sangre', 'pco2', 'hco3', 'exceso_base'].filter(presente)
|
| 704 |
-
});
|
| 705 |
-
} else if (hipercarbia) {
|
| 706 |
-
agregar({
|
| 707 |
-
nombre: alt.acidosis_respiratoria.nombre,
|
| 708 |
-
descripcion: alt.acidosis_respiratoria.descripcion,
|
| 709 |
-
gravedad: gravedadDe('ph_sangre', 'pco2'),
|
| 710 |
-
parametros: ['ph_sangre', 'pco2'].filter(presente)
|
| 711 |
-
});
|
| 712 |
-
} else if (componenteAcidMet) {
|
| 713 |
-
agregar({
|
| 714 |
-
nombre: alt.acidosis_metabolica.nombre,
|
| 715 |
-
descripcion: alt.acidosis_metabolica.descripcion,
|
| 716 |
-
gravedad: gravedadDe('ph_sangre', 'hco3', 'exceso_base'),
|
| 717 |
-
parametros: ['ph_sangre', 'hco3', 'exceso_base', 'anion_gap'].filter(presente)
|
| 718 |
-
});
|
| 719 |
-
}
|
| 720 |
-
}
|
| 721 |
-
|
| 722 |
-
if (phAlto) {
|
| 723 |
-
if (hipocarbia && componenteAlcalMet) {
|
| 724 |
-
agregar({
|
| 725 |
-
nombre: alt.alcalosis_respiratoria.nombre + ' + ' + alt.alcalosis_metabolica.nombre,
|
| 726 |
-
descripcion: alt.alcalosis_metabolica.descripcion,
|
| 727 |
-
gravedad: 'grave',
|
| 728 |
-
parametros: ['ph_sangre', 'pco2', 'hco3', 'exceso_base'].filter(presente)
|
| 729 |
-
});
|
| 730 |
-
} else if (hipocarbia) {
|
| 731 |
-
agregar({
|
| 732 |
-
nombre: alt.alcalosis_respiratoria.nombre,
|
| 733 |
-
descripcion: alt.alcalosis_respiratoria.descripcion,
|
| 734 |
-
gravedad: gravedadDe('ph_sangre', 'pco2'),
|
| 735 |
-
parametros: ['ph_sangre', 'pco2'].filter(presente)
|
| 736 |
-
});
|
| 737 |
-
} else if (componenteAlcalMet) {
|
| 738 |
-
agregar({
|
| 739 |
-
nombre: alt.alcalosis_metabolica.nombre,
|
| 740 |
-
descripcion: alt.alcalosis_metabolica.descripcion,
|
| 741 |
-
gravedad: gravedadDe('ph_sangre', 'hco3', 'exceso_base'),
|
| 742 |
-
parametros: ['ph_sangre', 'hco3', 'exceso_base'].filter(presente)
|
| 743 |
-
});
|
| 744 |
-
}
|
| 745 |
-
}
|
| 746 |
-
}
|
| 747 |
-
|
| 748 |
-
if (esBajo('po2')) agregar({
|
| 749 |
-
nombre: alt.hipoxemia.nombre,
|
| 750 |
-
descripcion: alt.hipoxemia.descripcion,
|
| 751 |
-
gravedad: gravedadDe('po2', 'so2'),
|
| 752 |
-
parametros: ['po2', ...(presente('so2') ? ['so2'] : [])].filter(presente)
|
| 753 |
-
});
|
| 754 |
-
|
| 755 |
-
if (esAlto('lactato')) agregar({
|
| 756 |
-
nombre: alt.hiperlactatemia.nombre,
|
| 757 |
-
descripcion: alt.hiperlactatemia.descripcion,
|
| 758 |
-
gravedad: gravedadDe('lactato'),
|
| 759 |
-
parametros: ['lactato']
|
| 760 |
-
});
|
| 761 |
-
|
| 762 |
-
if (esBajo('ca_ion')) agregar({
|
| 763 |
-
nombre: alt.ca_ionizado_bajo.nombre,
|
| 764 |
-
descripcion: alt.ca_ionizado_bajo.descripcion,
|
| 765 |
-
gravedad: gravedadDe('ca_ion'),
|
| 766 |
-
parametros: ['ca_ion']
|
| 767 |
-
});
|
| 768 |
-
|
| 769 |
-
if (esAlto('ca_ion')) agregar({
|
| 770 |
-
nombre: alt.ca_ionizado_alto.nombre,
|
| 771 |
-
descripcion: alt.ca_ionizado_alto.descripcion,
|
| 772 |
-
gravedad: gravedadDe('ca_ion'),
|
| 773 |
-
parametros: ['ca_ion']
|
| 774 |
-
});
|
| 775 |
-
|
| 776 |
-
if (esAlto('anion_gap')) agregar({
|
| 777 |
-
nombre: alt.anion_gap_elevado.nombre,
|
| 778 |
-
descripcion: alt.anion_gap_elevado.descripcion,
|
| 779 |
-
gravedad: gravedadDe('anion_gap'),
|
| 780 |
-
parametros: ['anion_gap', ...(presente('lactato') ? ['lactato'] : [])].filter(presente)
|
| 781 |
-
});
|
| 782 |
-
|
| 783 |
-
return patrones;
|
| 784 |
-
};
|
| 785 |
-
|
| 786 |
-
// Exportación principal
|
| 787 |
-
|
| 788 |
-
export const analizarResultados = (valoresInput, paciente, referencias, alteraciones) => {
|
| 789 |
-
const refsEspecie = referencias[paciente.especie];
|
| 790 |
-
if (!refsEspecie) return { hallazgos: [], patrones: [] };
|
| 791 |
-
|
| 792 |
-
// Ajusta los rangos segun edad, raza y sexo antes de comparar
|
| 793 |
-
const refsAjustadas = ajustarReferencias(refsEspecie, paciente);
|
| 794 |
-
const hallazgos = [];
|
| 795 |
-
|
| 796 |
-
for (const [clave, ref] of Object.entries(refsAjustadas)) {
|
| 797 |
-
const crudo = valoresInput[clave];
|
| 798 |
-
if (crudo === null || crudo === undefined || crudo === '') continue;
|
| 799 |
-
|
| 800 |
-
const valorNum = parseFloat(crudo);
|
| 801 |
-
if (isNaN(valorNum)) continue;
|
| 802 |
-
|
| 803 |
-
if (valorNum > ref.superior) {
|
| 804 |
-
hallazgos.push({
|
| 805 |
-
clave, nombre: ref.nombre, valor: valorNum, unidad: ref.unidad,
|
| 806 |
-
direccion: 'alto', gravedad: clasificarGravedad(valorNum, ref)
|
| 807 |
-
});
|
| 808 |
-
} else if (valorNum < ref.inferior) {
|
| 809 |
-
hallazgos.push({
|
| 810 |
-
clave, nombre: ref.nombre, valor: valorNum, unidad: ref.unidad,
|
| 811 |
-
direccion: 'bajo', gravedad: clasificarGravedad(valorNum, ref)
|
| 812 |
-
});
|
| 813 |
-
}
|
| 814 |
-
}
|
| 815 |
-
|
| 816 |
-
return { hallazgos, patrones: detectarPatrones(hallazgos, paciente.especie, alteraciones) };
|
| 817 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/auth.js
DELETED
|
@@ -1,249 +0,0 @@
|
|
| 1 |
-
let estadoAuth = null;
|
| 2 |
-
let accionPendiente = null;
|
| 3 |
-
|
| 4 |
-
// Verificación de sesión
|
| 5 |
-
|
| 6 |
-
export async function verificarAuth() {
|
| 7 |
-
if (estadoAuth !== null) return estadoAuth;
|
| 8 |
-
|
| 9 |
-
try {
|
| 10 |
-
const resp = await fetch('api/auth.php');
|
| 11 |
-
const datos = await resp.json();
|
| 12 |
-
estadoAuth = datos.autenticado;
|
| 13 |
-
if (estadoAuth) actualizarBtnUsuario(datos.nombre);
|
| 14 |
-
} catch {
|
| 15 |
-
estadoAuth = false;
|
| 16 |
-
}
|
| 17 |
-
return estadoAuth;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
// Modal
|
| 21 |
-
|
| 22 |
-
const modal = document.getElementById('modal-auth');
|
| 23 |
-
const overlay = document.getElementById('modal-auth-overlay');
|
| 24 |
-
const btnCerrar = document.getElementById('modal-auth-cerrar');
|
| 25 |
-
const tabLogin = document.getElementById('auth-tab-login');
|
| 26 |
-
const tabRegistro = document.getElementById('auth-tab-registro');
|
| 27 |
-
const panelLogin = document.getElementById('auth-panel-login');
|
| 28 |
-
const panelRegistro = document.getElementById('auth-panel-registro');
|
| 29 |
-
const formLogin = document.getElementById('form-login');
|
| 30 |
-
const formRegistro = document.getElementById('form-registro');
|
| 31 |
-
const errorLogin = document.getElementById('auth-error-login');
|
| 32 |
-
const errorRegistro = document.getElementById('auth-error-registro');
|
| 33 |
-
|
| 34 |
-
function abrirModal() {
|
| 35 |
-
modal.hidden = false;
|
| 36 |
-
requestAnimationFrame(() => {
|
| 37 |
-
modal.classList.add('visible');
|
| 38 |
-
overlay.classList.add('activo');
|
| 39 |
-
});
|
| 40 |
-
formLogin.reset();
|
| 41 |
-
formRegistro.reset();
|
| 42 |
-
[formLogin, formRegistro].forEach(f =>
|
| 43 |
-
f.querySelectorAll('input').forEach(limpiarCampo)
|
| 44 |
-
);
|
| 45 |
-
errorLogin.textContent = '';
|
| 46 |
-
errorRegistro.textContent = '';
|
| 47 |
-
activarTab('login');
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
function cerrarModal() {
|
| 51 |
-
modal.classList.remove('visible');
|
| 52 |
-
overlay.classList.remove('activo');
|
| 53 |
-
modal.addEventListener('transitionend', () => { modal.hidden = true; }, { once: true });
|
| 54 |
-
accionPendiente = null;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
function activarTab(cual) {
|
| 58 |
-
const esLogin = cual === 'login';
|
| 59 |
-
tabLogin.classList.toggle('activo', esLogin);
|
| 60 |
-
tabRegistro.classList.toggle('activo', !esLogin);
|
| 61 |
-
panelLogin.hidden = !esLogin;
|
| 62 |
-
panelRegistro.hidden = esLogin;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
export function abrirModalAuth(callbackExito) {
|
| 66 |
-
accionPendiente = callbackExito ?? null;
|
| 67 |
-
abrirModal();
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
// Botón de usuario en header
|
| 71 |
-
|
| 72 |
-
const btnUsuario = document.getElementById('btn-usuario');
|
| 73 |
-
|
| 74 |
-
const SVG_LOGIN = `<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="currentColor" aria-hidden="true"><path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/></svg>`;
|
| 75 |
-
const SVG_LOGOUT = `<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="currentColor" aria-hidden="true"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h280v80H200Zm440-160-55-58 102-102H360v-80h327L585-622l55-58 200 200-200 200Z"/></svg>`;
|
| 76 |
-
|
| 77 |
-
function actualizarBtnUsuario(nombre) {
|
| 78 |
-
if (!btnUsuario) return;
|
| 79 |
-
btnUsuario.innerHTML = SVG_LOGOUT;
|
| 80 |
-
btnUsuario.append(' ', nombre ?? 'Usuario');
|
| 81 |
-
btnUsuario.dataset.tooltip = 'Cerrar sesión';
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
function resetearBtnUsuario() {
|
| 85 |
-
if (!btnUsuario) return;
|
| 86 |
-
btnUsuario.innerHTML = `${SVG_LOGIN} Login`;
|
| 87 |
-
btnUsuario.dataset.tooltip = 'Iniciar sesión';
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
// Validación en tiempo real
|
| 91 |
-
|
| 92 |
-
function esEmailValido(v) {
|
| 93 |
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
function marcarCampo(input, valido) {
|
| 97 |
-
input.classList.toggle('campo-valido', valido);
|
| 98 |
-
input.classList.toggle('campo-invalido', !valido);
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
function limpiarCampo(input) {
|
| 102 |
-
input.classList.remove('campo-valido', 'campo-invalido');
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
function activarValidacionCampo(input, reglaDeFalso) {
|
| 106 |
-
// Solo marca despues del primer blur para no agobiar al usuario mientras escribe
|
| 107 |
-
let tocado = false;
|
| 108 |
-
input.addEventListener('blur', () => { tocado = true; marcarCampo(input, !reglaDeFalso()); });
|
| 109 |
-
input.addEventListener('input', () => { if (tocado) marcarCampo(input, !reglaDeFalso()); });
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
function inicializarValidacionLogin() {
|
| 113 |
-
const email = formLogin.querySelector('[name="email"]');
|
| 114 |
-
const password = formLogin.querySelector('[name="password"]');
|
| 115 |
-
activarValidacionCampo(email, () => !esEmailValido(email.value));
|
| 116 |
-
activarValidacionCampo(password, () => password.value.length < 1);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
function inicializarValidacionRegistro() {
|
| 120 |
-
const nombre = formRegistro.querySelector('[name="nombre"]');
|
| 121 |
-
const apellido = formRegistro.querySelector('[name="apellido"]');
|
| 122 |
-
const email = formRegistro.querySelector('[name="email"]');
|
| 123 |
-
const password = formRegistro.querySelector('[name="password"]');
|
| 124 |
-
const password2 = formRegistro.querySelector('[name="password2"]');
|
| 125 |
-
|
| 126 |
-
activarValidacionCampo(nombre, () => nombre.value.trim().length < 1);
|
| 127 |
-
activarValidacionCampo(apellido, () => apellido.value.trim().length < 1);
|
| 128 |
-
activarValidacionCampo(email, () => !esEmailValido(email.value));
|
| 129 |
-
activarValidacionCampo(password, () => password.value.length < 6);
|
| 130 |
-
|
| 131 |
-
// password2 depende del valor de password, se re-evalua en ambos campos para dar feedback inmediato
|
| 132 |
-
let tocadoP2 = false;
|
| 133 |
-
const validarP2 = () => password.value === password2.value && password2.value.length > 0;
|
| 134 |
-
password2.addEventListener('blur', () => { tocadoP2 = true; marcarCampo(password2, validarP2()); });
|
| 135 |
-
password2.addEventListener('input', () => { if (tocadoP2) marcarCampo(password2, validarP2()); });
|
| 136 |
-
password.addEventListener('input', () => { if (tocadoP2) marcarCampo(password2, validarP2()); });
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
inicializarValidacionLogin();
|
| 140 |
-
inicializarValidacionRegistro();
|
| 141 |
-
|
| 142 |
-
// Manejo de formularios
|
| 143 |
-
|
| 144 |
-
async function enviarFormAuth(accion, campos) {
|
| 145 |
-
try {
|
| 146 |
-
const resp = await fetch('api/auth.php', {
|
| 147 |
-
method: 'POST',
|
| 148 |
-
headers: { 'Content-Type': 'application/json' },
|
| 149 |
-
body: JSON.stringify({ accion, ...campos }),
|
| 150 |
-
});
|
| 151 |
-
return await resp.json();
|
| 152 |
-
} catch {
|
| 153 |
-
return { error: 'Error de conexión. Verifica tu red.' };
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
formLogin.addEventListener('submit', async e => {
|
| 158 |
-
e.preventDefault();
|
| 159 |
-
errorLogin.textContent = '';
|
| 160 |
-
const btn = formLogin.querySelector('button[type="submit"]');
|
| 161 |
-
btn.disabled = true;
|
| 162 |
-
btn.textContent = 'Ingresando…';
|
| 163 |
-
|
| 164 |
-
const datos = await enviarFormAuth('login', {
|
| 165 |
-
email: formLogin.querySelector('[name="email"]').value,
|
| 166 |
-
password: formLogin.querySelector('[name="password"]').value,
|
| 167 |
-
});
|
| 168 |
-
|
| 169 |
-
btn.disabled = false;
|
| 170 |
-
btn.textContent = 'Ingresar';
|
| 171 |
-
|
| 172 |
-
if (datos.error) { errorLogin.textContent = datos.error; return; }
|
| 173 |
-
|
| 174 |
-
estadoAuth = true;
|
| 175 |
-
actualizarBtnUsuario(datos.nombre);
|
| 176 |
-
cerrarModal();
|
| 177 |
-
// Ejecuta la accion que el usuario intento hacer antes de loguearse (ej. analisis IA)
|
| 178 |
-
accionPendiente?.();
|
| 179 |
-
accionPendiente = null;
|
| 180 |
-
});
|
| 181 |
-
|
| 182 |
-
formRegistro.addEventListener('submit', async e => {
|
| 183 |
-
e.preventDefault();
|
| 184 |
-
errorRegistro.textContent = '';
|
| 185 |
-
const btn = formRegistro.querySelector('button[type="submit"]');
|
| 186 |
-
btn.disabled = true;
|
| 187 |
-
btn.textContent = 'Registrando…';
|
| 188 |
-
|
| 189 |
-
const password = formRegistro.querySelector('[name="password"]').value;
|
| 190 |
-
const password2 = formRegistro.querySelector('[name="password2"]').value;
|
| 191 |
-
|
| 192 |
-
if (!formRegistro.querySelector('[name="aviso-legal"]').checked) {
|
| 193 |
-
errorRegistro.textContent = 'Debes aceptar el aviso antes de crear una cuenta.';
|
| 194 |
-
btn.disabled = false;
|
| 195 |
-
btn.textContent = 'Crear cuenta';
|
| 196 |
-
return;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
if (password !== password2) {
|
| 200 |
-
errorRegistro.textContent = 'Las contraseñas no coinciden.';
|
| 201 |
-
btn.disabled = false;
|
| 202 |
-
btn.textContent = 'Crear cuenta';
|
| 203 |
-
return;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
const datos = await enviarFormAuth('registro', {
|
| 207 |
-
nombre: formRegistro.querySelector('[name="nombre"]').value,
|
| 208 |
-
apellido: formRegistro.querySelector('[name="apellido"]').value,
|
| 209 |
-
email: formRegistro.querySelector('[name="email"]').value,
|
| 210 |
-
password,
|
| 211 |
-
});
|
| 212 |
-
|
| 213 |
-
btn.disabled = false;
|
| 214 |
-
btn.textContent = 'Crear cuenta';
|
| 215 |
-
|
| 216 |
-
if (datos.error) { errorRegistro.textContent = datos.error; return; }
|
| 217 |
-
|
| 218 |
-
estadoAuth = true;
|
| 219 |
-
actualizarBtnUsuario(datos.nombre);
|
| 220 |
-
cerrarModal();
|
| 221 |
-
// Ejecuta la accion que el usuario intento hacer antes de registrarse (ej. analisis IA)
|
| 222 |
-
accionPendiente?.();
|
| 223 |
-
accionPendiente = null;
|
| 224 |
-
});
|
| 225 |
-
|
| 226 |
-
// Eventos de UI
|
| 227 |
-
|
| 228 |
-
tabLogin.addEventListener('click', () => activarTab('login'));
|
| 229 |
-
tabRegistro.addEventListener('click', () => activarTab('registro'));
|
| 230 |
-
btnCerrar.addEventListener('click', cerrarModal);
|
| 231 |
-
overlay.addEventListener('click', cerrarModal);
|
| 232 |
-
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !modal.hidden) cerrarModal(); });
|
| 233 |
-
|
| 234 |
-
btnUsuario?.addEventListener('click', async () => {
|
| 235 |
-
const autenticado = await verificarAuth();
|
| 236 |
-
if (!autenticado) {
|
| 237 |
-
abrirModal();
|
| 238 |
-
} else {
|
| 239 |
-
await fetch('api/auth.php', {
|
| 240 |
-
method: 'POST',
|
| 241 |
-
headers: { 'Content-Type': 'application/json' },
|
| 242 |
-
body: JSON.stringify({ accion: 'logout' }),
|
| 243 |
-
});
|
| 244 |
-
estadoAuth = false;
|
| 245 |
-
resetearBtnUsuario();
|
| 246 |
-
}
|
| 247 |
-
});
|
| 248 |
-
|
| 249 |
-
verificarAuth();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/ia.js
DELETED
|
@@ -1,289 +0,0 @@
|
|
| 1 |
-
import { imagenesDataUrl, capturasMicroscopio } from './ui.js';
|
| 2 |
-
|
| 3 |
-
const BACKEND_KEY = 'mx-ia-backend';
|
| 4 |
-
const OLLAMA_URL_KEY = 'mx-ia-ollama-url';
|
| 5 |
-
const OLLAMA_MOD_KEY = 'mx-ia-ollama-model';
|
| 6 |
-
|
| 7 |
-
export function inicializarConfigBackend() {
|
| 8 |
-
const radioLocal = document.getElementById('ia-backend-local');
|
| 9 |
-
const radioHF = document.getElementById('ia-backend-hf');
|
| 10 |
-
const urlInput = document.getElementById('ia-ollama-url');
|
| 11 |
-
const modelInput = document.getElementById('ia-ollama-model');
|
| 12 |
-
const camposOllama = document.getElementById('ia-ollama-fields');
|
| 13 |
-
|
| 14 |
-
const backendGuardado = localStorage.getItem(BACKEND_KEY) ?? 'hf';
|
| 15 |
-
if (backendGuardado === 'local') radioLocal.checked = true;
|
| 16 |
-
else radioHF.checked = true;
|
| 17 |
-
|
| 18 |
-
const urlGuardada = localStorage.getItem(OLLAMA_URL_KEY);
|
| 19 |
-
let modeloGuardado = localStorage.getItem(OLLAMA_MOD_KEY);
|
| 20 |
-
if (modeloGuardado === 'medgemma:latest') {
|
| 21 |
-
modeloGuardado = 'medgemma1.5:latest';
|
| 22 |
-
localStorage.setItem(OLLAMA_MOD_KEY, modeloGuardado);
|
| 23 |
-
}
|
| 24 |
-
if (urlGuardada) urlInput.value = urlGuardada;
|
| 25 |
-
if (modeloGuardado) modelInput.value = modeloGuardado;
|
| 26 |
-
|
| 27 |
-
function aplicarBackend(val) {
|
| 28 |
-
camposOllama.hidden = val !== 'local';
|
| 29 |
-
}
|
| 30 |
-
aplicarBackend(backendGuardado);
|
| 31 |
-
|
| 32 |
-
[radioLocal, radioHF].forEach(r => r.addEventListener('change', () => {
|
| 33 |
-
const val = document.querySelector('input[name="ia-backend"]:checked').value;
|
| 34 |
-
localStorage.setItem(BACKEND_KEY, val);
|
| 35 |
-
aplicarBackend(val);
|
| 36 |
-
}));
|
| 37 |
-
|
| 38 |
-
urlInput.addEventListener('input', () => localStorage.setItem(OLLAMA_URL_KEY, urlInput.value.trim()));
|
| 39 |
-
modelInput.addEventListener('input', () => localStorage.setItem(OLLAMA_MOD_KEY, modelInput.value.trim()));
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
// Prompt
|
| 43 |
-
|
| 44 |
-
function construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias) {
|
| 45 |
-
const paciente = obtenerDatosPaciente();
|
| 46 |
-
const valores = obtenerValoresFormulario();
|
| 47 |
-
const { hallazgos, patrones } = getUltimoAnalisis();
|
| 48 |
-
const signosText = document.getElementById('signos-clinicos').value.trim();
|
| 49 |
-
const refEspecie = paciente.especie ? (getReferencias()[paciente.especie] || {}) : {};
|
| 50 |
-
|
| 51 |
-
const totalImagenes = imagenesDataUrl.filter(Boolean).length + capturasMicroscopio.length;
|
| 52 |
-
const hayImagenes = totalImagenes > 0;
|
| 53 |
-
|
| 54 |
-
const lineasValores = Object.entries(valores).map(([clave, valor]) => {
|
| 55 |
-
const ref = refEspecie[clave];
|
| 56 |
-
const nombre = ref?.nombre || clave;
|
| 57 |
-
const unidad = ref?.unidad || '';
|
| 58 |
-
const rango = ref ? ` [ref: ${ref.inferior}-${ref.superior}]` : '';
|
| 59 |
-
const h = hallazgos.find(h => h.clave === clave);
|
| 60 |
-
const flag = h ? ` ← ${h.direccion === 'alto' ? 'ELEVADO' : 'BAJO'} (${h.gravedad})` : '';
|
| 61 |
-
return ` ${nombre}: ${valor} ${unidad}${rango}${flag}`;
|
| 62 |
-
}).join('\n') || 'Sin valores ingresados';
|
| 63 |
-
|
| 64 |
-
const lineasPatrones = patrones.length > 0
|
| 65 |
-
? patrones.map(p => ` - ${p.nombre}: ${p.descripcion}`).join('\n')
|
| 66 |
-
: ' Ninguno detectado';
|
| 67 |
-
|
| 68 |
-
const edadTexto = paciente.edadMeses != null
|
| 69 |
-
? (paciente.edadMeses < 24 ? `${Math.round(paciente.edadMeses)} meses` : `${(paciente.edadMeses / 12).toFixed(1)} años`)
|
| 70 |
-
: 'desconocida';
|
| 71 |
-
|
| 72 |
-
if (hayImagenes) {
|
| 73 |
-
const lineasHallazgos = hallazgos.length > 0
|
| 74 |
-
? hallazgos.map(h => ` ${h.nombre}: ${h.valor} ${h.unidad || ''} (${h.direccion} · ${h.gravedad})`).join('\n')
|
| 75 |
-
: ' Todos los valores normales';
|
| 76 |
-
|
| 77 |
-
// Prompt enfocado en citologia cuando hay imagenes adjuntas
|
| 78 |
-
return `Eres médico veterinario especialista en patología clínica.
|
| 79 |
-
|
| 80 |
-
Paciente: ${paciente.especie || 'desconocido'}, ${paciente.raza || 'raza desconocida'}, ${edadTexto}, ${paciente.sexo || 'sexo desconocido'}
|
| 81 |
-
|
| 82 |
-
Hallazgos de laboratorio:
|
| 83 |
-
${lineasHallazgos}
|
| 84 |
-
${signosText ? `\nSignos clínicos: ${signosText}` : ''}
|
| 85 |
-
|
| 86 |
-
DATOS ADJUNTOS: ${totalImagenes} imagen${totalImagenes > 1 ? 'es' : ''} de citología.
|
| 87 |
-
|
| 88 |
-
¿Qué observas en las imágenes? Describe la morfología celular, identifica lesiones, patrones anormales, hemoparásitos (Anaplasma, Babesia, Ehrlichia, Hepatozoon, Piroplasma, Mycoplasma) e inclusiones citoplasmáticas. Luego integra con los datos de laboratorio. Responde en español.`;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
const lineasHallazgos = hallazgos.length > 0
|
| 92 |
-
? hallazgos.map(h => ` ${h.nombre}: ${h.valor} ${h.unidad || ''} (${h.direccion} · ${h.gravedad})`).join('\n')
|
| 93 |
-
: ' Todos los valores dentro de rangos normales';
|
| 94 |
-
|
| 95 |
-
return `Responde en español.
|
| 96 |
-
|
| 97 |
-
Eres médico veterinario especialista en patología clínica.
|
| 98 |
-
|
| 99 |
-
Paciente: ${paciente.especie || 'desconocido'}, raza: ${paciente.raza || 'NE'}, edad: ${edadTexto}, sexo: ${paciente.sexo || 'NE'}
|
| 100 |
-
|
| 101 |
-
Hallazgos de laboratorio:
|
| 102 |
-
${lineasHallazgos}
|
| 103 |
-
${signosText ? `\nSignos clínicos: ${signosText}` : ''}
|
| 104 |
-
|
| 105 |
-
Proporciona una interpretación clínica breve (6-8 oraciones) destacando los hallazgos más significativos y las recomendaciones diagnósticas inmediatas.`;
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
function limpiarRespuesta(text) {
|
| 109 |
-
// Elimina tokens especiales del modelo medGemma y otros artefactos de generacion
|
| 110 |
-
if (text.includes('<start_of_turn>model')) {
|
| 111 |
-
text = text.split('<start_of_turn>model').pop();
|
| 112 |
-
}
|
| 113 |
-
if (text.includes('<end_of_turn>')) {
|
| 114 |
-
text = text.slice(0, text.indexOf('<end_of_turn>'));
|
| 115 |
-
}
|
| 116 |
-
if (text.includes('<unused95>')) {
|
| 117 |
-
// Formato normal: <unused94>pensamiento<unused95>respuesta
|
| 118 |
-
text = text.split('<unused95>').pop();
|
| 119 |
-
} else if (text.includes('<unused94>')) {
|
| 120 |
-
// El modelo agoto tokens en el razonamiento; muestra el pensamiento como respuesta
|
| 121 |
-
text = text.split('<unused94>').slice(1).join('').trim();
|
| 122 |
-
}
|
| 123 |
-
text = text.replace(/<unused\d+>/g, '');
|
| 124 |
-
text = text.replace(/<start_of_turn>\w+\n?/g, '');
|
| 125 |
-
|
| 126 |
-
// Quitar prefijos de rol que el modelo a veces antepone
|
| 127 |
-
text = text.replace(/^\d+\s+(medical assistant|assistant|model)\s*/i, '');
|
| 128 |
-
|
| 129 |
-
// Quitar bloques de razonamiento / thinking process
|
| 130 |
-
text = text.replace(/^thought\s*\n?/i, '');
|
| 131 |
-
|
| 132 |
-
// Detectar si el modelo solo genero razonamiento en ingles sin respuesta clinica
|
| 133 |
-
const tieneRazonamiento = /Here'?s a thinking process|Understand the Role|Analyze the Request|Review the Lab Results|Synthesize Findings|Formulate Clinical Interpretation/i.test(text);
|
| 134 |
-
const tieneEspanol = /[áéíóúñÁÉÍÓÚÑ]{2,}/.test(text) || /\b(paciente|hallazgos|interpretación|recomendaciones|análisis|resultados|clínica|diagnóstico|evaluación|hepatopatía|nefropatía|anemia|leucocitosis|neutrofilia|linfopenia|hiperglucemia|hipoglucemia|pancreatitis|hepatitis|cirrosis|insuficiencia)\b/i.test(text);
|
| 135 |
-
if (tieneRazonamiento && !tieneEspanol) {
|
| 136 |
-
return 'El modelo generó un proceso de razonamiento interno en lugar de una interpretación clínica. Esto suele deberse a que el modelo está configurado en modo "pensamiento". Intenta nuevamente o contacta al administrador del espacio para desactivar el modo de razonamiento.';
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
// Si hay razonamiento mezclado con español, intentar extraer solo la respuesta
|
| 140 |
-
if (tieneRazonamiento) {
|
| 141 |
-
// Buscar la primera linea que parezca español clinico
|
| 142 |
-
const lineas = text.split('\n');
|
| 143 |
-
let inicioRespuesta = -1;
|
| 144 |
-
for (let i = 0; i < lineas.length; i++) {
|
| 145 |
-
const linea = lineas[i].trim();
|
| 146 |
-
if (linea.length > 20 && /[áéíóúñÁÉÍÓÚÑ]/.test(linea) && !/\*\*[^*]+\*\*/.test(linea) && !/^\d+\./.test(linea) && !/Here'?s a thinking process/i.test(linea)) {
|
| 147 |
-
inicioRespuesta = i;
|
| 148 |
-
break;
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
if (inicioRespuesta > 0) {
|
| 152 |
-
text = lineas.slice(inicioRespuesta).join('\n');
|
| 153 |
-
}
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
// Quitar LaTeX
|
| 157 |
-
text = text.replace(/\$\\boxed\{[^}]*\}\$/g, '');
|
| 158 |
-
text = text.replace(/\\begin\{[^}]+\}[\s\S]*?\\end\{[^}]+\}/g, '');
|
| 159 |
-
text = text.replace(/\\[a-zA-Z]+(\{[^}]*\})?/g, '');
|
| 160 |
-
text = text.replace(/\$[^$]*\$/g, '');
|
| 161 |
-
|
| 162 |
-
// Colapsar lineas vacias multiples
|
| 163 |
-
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
| 164 |
-
|
| 165 |
-
// Cortar al primer parrafo que se repite (loop del modelo)
|
| 166 |
-
const parrafos = text.split(/\n\n+/);
|
| 167 |
-
const vistos = new Set();
|
| 168 |
-
const sinRepetidos = [];
|
| 169 |
-
for (const p of parrafos) {
|
| 170 |
-
const clave = p.trim().slice(0, 80);
|
| 171 |
-
if (vistos.has(clave)) break;
|
| 172 |
-
vistos.add(clave);
|
| 173 |
-
sinRepetidos.push(p);
|
| 174 |
-
}
|
| 175 |
-
text = sinRepetidos.join('\n\n');
|
| 176 |
-
|
| 177 |
-
return text.trim() || 'Sin respuesta del modelo.';
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
// Llamado a IA
|
| 181 |
-
|
| 182 |
-
export async function llamarIA(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias) {
|
| 183 |
-
const salidaEl = document.getElementById('salida-ia');
|
| 184 |
-
const backend = document.querySelector('input[name="ia-backend"]:checked')?.value ?? 'hf';
|
| 185 |
-
|
| 186 |
-
salidaEl.textContent = 'Consultando al modelo de I.A…';
|
| 187 |
-
salidaEl.classList.add('cargando');
|
| 188 |
-
|
| 189 |
-
try {
|
| 190 |
-
if (backend === 'local') {
|
| 191 |
-
await _llamarOllama(salidaEl, obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 192 |
-
} else {
|
| 193 |
-
await _llamarSpace(salidaEl, obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 194 |
-
}
|
| 195 |
-
} finally {
|
| 196 |
-
salidaEl.classList.remove('cargando');
|
| 197 |
-
}
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
// Ollama
|
| 201 |
-
|
| 202 |
-
async function _llamarOllama(salidaEl, obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias) {
|
| 203 |
-
const urlBase = (document.getElementById('ia-ollama-url')?.value ?? 'http://localhost:11434').replace(/\/$/, '');
|
| 204 |
-
const model = document.getElementById('ia-ollama-model')?.value?.trim() || 'medgemma1.5:latest';
|
| 205 |
-
const prompt = construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 206 |
-
const imagenes = [...imagenesDataUrl.filter(Boolean), ...capturasMicroscopio];
|
| 207 |
-
|
| 208 |
-
// Construye el payload compatible con OpenAI vision: imagenes primero, luego el texto
|
| 209 |
-
const contenido = [];
|
| 210 |
-
for (const img of imagenes) {
|
| 211 |
-
if (typeof img === 'string' && img.startsWith('data:image/'))
|
| 212 |
-
contenido.push({ type: 'image_url', image_url: { url: img } });
|
| 213 |
-
}
|
| 214 |
-
contenido.push({ type: 'text', text: prompt });
|
| 215 |
-
|
| 216 |
-
try {
|
| 217 |
-
const res = await fetch(`${urlBase}/v1/chat/completions`, {
|
| 218 |
-
method: 'POST',
|
| 219 |
-
headers: { 'Content-Type': 'application/json' },
|
| 220 |
-
body: JSON.stringify({
|
| 221 |
-
model,
|
| 222 |
-
messages: [{ role: 'user', content: contenido.length === 1 ? prompt : contenido }],
|
| 223 |
-
max_tokens: imagenes.length > 0 ? 1200 : 600,
|
| 224 |
-
stream: false,
|
| 225 |
-
think: false,
|
| 226 |
-
}),
|
| 227 |
-
});
|
| 228 |
-
|
| 229 |
-
let data;
|
| 230 |
-
try { data = await res.json(); } catch {
|
| 231 |
-
salidaEl.textContent = `Error del servidor Ollama (HTTP ${res.status}). Verifica que esté ejecutándose.`;
|
| 232 |
-
return;
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
if (!res.ok) {
|
| 236 |
-
salidaEl.textContent = `Error Ollama: ${data?.error?.message ?? data?.error ?? `HTTP ${res.status}`}`;
|
| 237 |
-
} else {
|
| 238 |
-
salidaEl.textContent = limpiarRespuesta(data?.choices?.[0]?.message?.content ?? 'Sin respuesta del modelo.');
|
| 239 |
-
}
|
| 240 |
-
} catch {
|
| 241 |
-
salidaEl.textContent = `No se pudo conectar con Ollama en ${urlBase}. Verifica que esté ejecutándose con "ollama serve".`;
|
| 242 |
-
}
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
// Morphos AI Space
|
| 246 |
-
|
| 247 |
-
async function _llamarSpace(salidaEl, obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias) {
|
| 248 |
-
let prompt = construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 249 |
-
|
| 250 |
-
// El modelo medGemma espera el token <unused95> al inicio del prompt para modo respuesta directa
|
| 251 |
-
if (!prompt.includes('<unused95>')) {
|
| 252 |
-
prompt = '<unused95>' + prompt;
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
// Filtra y limita a 4 imagenes por restriccion del backend de HuggingFace
|
| 256 |
-
const imagenes = [...imagenesDataUrl.filter(Boolean), ...capturasMicroscopio]
|
| 257 |
-
.filter(img => typeof img === 'string' && /^data:image\/(jpeg|png|gif|webp);base64,/.test(img))
|
| 258 |
-
.slice(0, 4);
|
| 259 |
-
|
| 260 |
-
console.log('=== MORPHOS AI REQUEST ===');
|
| 261 |
-
console.log('Images count:', imagenes.length);
|
| 262 |
-
console.log('Prompt length:', prompt.length);
|
| 263 |
-
console.log('Prompt preview:', prompt.substring(0, 200) + '...');
|
| 264 |
-
console.log('==========================');
|
| 265 |
-
|
| 266 |
-
try {
|
| 267 |
-
const res = await fetch('api/hf_proxy.php', {
|
| 268 |
-
method: 'POST',
|
| 269 |
-
headers: { 'Content-Type': 'application/json' },
|
| 270 |
-
body: JSON.stringify({ images: imagenes, prompt }),
|
| 271 |
-
});
|
| 272 |
-
|
| 273 |
-
const data = await res.json();
|
| 274 |
-
|
| 275 |
-
console.log('=== MORPHOS AI RESPONSE ===');
|
| 276 |
-
console.log('Status:', res.status);
|
| 277 |
-
console.log('Raw text preview:', (data.text ?? 'NO TEXT').substring(0, 300));
|
| 278 |
-
console.log('===========================');
|
| 279 |
-
|
| 280 |
-
if (!res.ok) {
|
| 281 |
-
salidaEl.textContent = `Error: ${data?.error ?? `HTTP ${res.status}`}`;
|
| 282 |
-
} else {
|
| 283 |
-
salidaEl.textContent = limpiarRespuesta(data.text ?? 'Sin respuesta del modelo.');
|
| 284 |
-
}
|
| 285 |
-
} catch (e) {
|
| 286 |
-
console.error('Network error:', e);
|
| 287 |
-
salidaEl.textContent = `Error de red: ${e.message}`;
|
| 288 |
-
}
|
| 289 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/main.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
| 1 |
-
import './tooltip.js';
|
| 2 |
-
import { analizarResultados } from './analisis.js';
|
| 3 |
-
import { colapsarPatrones, inicializarSincMob } from './ui.js';
|
| 4 |
-
import { llamarIA, inicializarConfigBackend } from './ia.js';
|
| 5 |
-
import { inicializarParserPdf } from './pdf-parser.js';
|
| 6 |
-
import { verificarAuth, abrirModalAuth } from './auth.js';
|
| 7 |
-
import { abrirModalPapers, inicializarModalPapers } from './papers.js';
|
| 8 |
-
|
| 9 |
-
// Tema oscuro/claro
|
| 10 |
-
|
| 11 |
-
const temaGuardado = localStorage.getItem('mx-theme');
|
| 12 |
-
const temaPreferido = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
| 13 |
-
document.documentElement.dataset.theme = temaGuardado || temaPreferido;
|
| 14 |
-
|
| 15 |
-
const btnTema = document.getElementById('btn-tema');
|
| 16 |
-
if (btnTema) {
|
| 17 |
-
btnTema.addEventListener('click', () => {
|
| 18 |
-
const siguienteTema = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
| 19 |
-
document.documentElement.dataset.theme = siguienteTema;
|
| 20 |
-
localStorage.setItem('mx-theme', siguienteTema);
|
| 21 |
-
});
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
// Data
|
| 25 |
-
|
| 26 |
-
let referencias = [];
|
| 27 |
-
let alteraciones = {};
|
| 28 |
-
let ultimoAnalisis = { hallazgos: [], patrones: [] };
|
| 29 |
-
|
| 30 |
-
const cargarReferencias = async () => {
|
| 31 |
-
try {
|
| 32 |
-
const response = await fetch('data/valores_referencia.json');
|
| 33 |
-
if (!response.ok) throw new Error(`Error HTTP: ${response.status}`);
|
| 34 |
-
referencias = await response.json();
|
| 35 |
-
} catch (error) {
|
| 36 |
-
console.error('Error cargando valores de referencia:', error);
|
| 37 |
-
}
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
const cargarAlteraciones = async () => {
|
| 41 |
-
try {
|
| 42 |
-
const response = await fetch('data/alteraciones.json');
|
| 43 |
-
if (!response.ok) throw new Error(`Error HTTP: ${response.status}`);
|
| 44 |
-
alteraciones = await response.json();
|
| 45 |
-
} catch (error) {
|
| 46 |
-
console.error('Error cargando alteraciones:', error);
|
| 47 |
-
}
|
| 48 |
-
};
|
| 49 |
-
|
| 50 |
-
cargarReferencias();
|
| 51 |
-
cargarAlteraciones();
|
| 52 |
-
|
| 53 |
-
// Colección de datos de formulario
|
| 54 |
-
|
| 55 |
-
const obtenerDatosPaciente = () => {
|
| 56 |
-
const especieCruda = document.getElementById('pt-especie').value;
|
| 57 |
-
const valorEdad = document.getElementById('pt-edad').value;
|
| 58 |
-
const edadUnidad = document.getElementById('pt-edad-unidad').value;
|
| 59 |
-
// Normaliza la edad siempre a meses para que analisis.js pueda aplicar ajustes por cachorro/adulto/senior
|
| 60 |
-
const edadMeses = valorEdad === '' ? null
|
| 61 |
-
: edadUnidad === 'meses' ? parseFloat(valorEdad)
|
| 62 |
-
: parseFloat(valorEdad) * 12;
|
| 63 |
-
return {
|
| 64 |
-
especie: especieCruda === 'Canino' ? 'canino' : especieCruda === 'Felino' ? 'felino' : null,
|
| 65 |
-
raza: document.getElementById('pt-raza').value,
|
| 66 |
-
edadMeses,
|
| 67 |
-
sexo: document.getElementById('pt-sexo').value
|
| 68 |
-
};
|
| 69 |
-
};
|
| 70 |
-
|
| 71 |
-
const obtenerValoresFormulario = () => {
|
| 72 |
-
const valores = {};
|
| 73 |
-
document.querySelectorAll('input[type="number"]').forEach(input => {
|
| 74 |
-
if (input.name && input.value !== '') valores[input.name] = parseFloat(input.value);
|
| 75 |
-
});
|
| 76 |
-
return valores;
|
| 77 |
-
};
|
| 78 |
-
|
| 79 |
-
// Renderizado
|
| 80 |
-
|
| 81 |
-
const ETIQUETA_GRAVEDAD = { leve: 'Leve', moderado: 'Moderado', grave: 'Grave' };
|
| 82 |
-
|
| 83 |
-
document.querySelectorAll('.fila-campo input[type="number"]').forEach(input => {
|
| 84 |
-
const span = document.createElement('span');
|
| 85 |
-
span.className = 'estado-campo';
|
| 86 |
-
input.before(span);
|
| 87 |
-
});
|
| 88 |
-
|
| 89 |
-
const actualizarClasesInputs = (hallazgos) => {
|
| 90 |
-
document.querySelectorAll('input[type="number"]').forEach(input => {
|
| 91 |
-
input.classList.remove('alto', 'bajo');
|
| 92 |
-
const span = input.previousElementSibling;
|
| 93 |
-
if (span?.classList.contains('estado-campo')) {
|
| 94 |
-
span.textContent = '';
|
| 95 |
-
span.className = 'estado-campo';
|
| 96 |
-
}
|
| 97 |
-
});
|
| 98 |
-
hallazgos.forEach(h => {
|
| 99 |
-
const input = document.querySelector(`input[name="${h.clave}"]`);
|
| 100 |
-
if (!input) return;
|
| 101 |
-
input.classList.add(h.direccion);
|
| 102 |
-
const span = input.previousElementSibling;
|
| 103 |
-
if (span?.classList.contains('estado-campo')) {
|
| 104 |
-
span.textContent = `${h.direccion === 'alto' ? 'Alto' : 'Bajo'} · ${ETIQUETA_GRAVEDAD[h.gravedad]}`;
|
| 105 |
-
span.className = `estado-campo estado-campo--${h.direccion}`;
|
| 106 |
-
}
|
| 107 |
-
});
|
| 108 |
-
};
|
| 109 |
-
|
| 110 |
-
const renderizarPatrones = (patrones) => {
|
| 111 |
-
const contenedor = document.getElementById('patrones-lista');
|
| 112 |
-
if (!contenedor) return;
|
| 113 |
-
|
| 114 |
-
contenedor.innerHTML = patrones.length === 0
|
| 115 |
-
? '<p class="sin-hallazgos">Sin patrones detectados.</p>'
|
| 116 |
-
: patrones.map(p => `
|
| 117 |
-
<div class="elemento-patron gravedad-${p.gravedad}">
|
| 118 |
-
<div class="titulo-patron">${p.nombre}</div>
|
| 119 |
-
<div class="cuerpo-patron">${p.descripcion}</div>
|
| 120 |
-
</div>`).join('');
|
| 121 |
-
};
|
| 122 |
-
|
| 123 |
-
// Evaluación
|
| 124 |
-
|
| 125 |
-
const evaluar = () => {
|
| 126 |
-
const paciente = obtenerDatosPaciente();
|
| 127 |
-
|
| 128 |
-
// Si no hay especie o aun no cargaron las referencias, limpia la UI para evitar falsos positivos
|
| 129 |
-
if (!paciente.especie || !referencias[paciente.especie]) {
|
| 130 |
-
actualizarClasesInputs([]);
|
| 131 |
-
renderizarPatrones([]);
|
| 132 |
-
return;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
const valores = obtenerValoresFormulario();
|
| 136 |
-
// Delega en analisis.js la comparacion contra rangos de referencia y la deteccion de patrones clinicos
|
| 137 |
-
const { hallazgos, patrones } = analizarResultados(valores, paciente, referencias, alteraciones);
|
| 138 |
-
ultimoAnalisis = { hallazgos, patrones };
|
| 139 |
-
|
| 140 |
-
actualizarClasesInputs(hallazgos);
|
| 141 |
-
renderizarPatrones(patrones);
|
| 142 |
-
};
|
| 143 |
-
|
| 144 |
-
// Eventos
|
| 145 |
-
|
| 146 |
-
document.addEventListener('input', e => {
|
| 147 |
-
if (e.target.type !== 'number') return;
|
| 148 |
-
// Impide valores por debajo del mínimo del campo; permite negativos cuando min lo indica
|
| 149 |
-
const minPermitido = e.target.min !== '' ? parseFloat(e.target.min) : 0;
|
| 150 |
-
if (parseFloat(e.target.value) < minPermitido) e.target.value = minPermitido;
|
| 151 |
-
if (e.target.value.replace('.', '').length > 4) e.target.value = e.target.value.slice(0, 4);
|
| 152 |
-
e.target.classList.toggle('max-chars', e.target.value.replace('.', '').length >= 4);
|
| 153 |
-
evaluar();
|
| 154 |
-
});
|
| 155 |
-
|
| 156 |
-
document.getElementById('pt-especie').addEventListener('change', evaluar);
|
| 157 |
-
document.getElementById('pt-raza').addEventListener('input', evaluar);
|
| 158 |
-
document.getElementById('pt-edad').addEventListener('input', evaluar);
|
| 159 |
-
document.getElementById('pt-edad-unidad').addEventListener('change', evaluar);
|
| 160 |
-
document.getElementById('pt-sexo').addEventListener('change', evaluar);
|
| 161 |
-
|
| 162 |
-
inicializarSincMob(evaluar);
|
| 163 |
-
inicializarConfigBackend();
|
| 164 |
-
inicializarParserPdf(evaluar);
|
| 165 |
-
inicializarModalPapers();
|
| 166 |
-
|
| 167 |
-
document.addEventListener('click', e => {
|
| 168 |
-
const btn = e.target.closest('.btn-limpiar-panel');
|
| 169 |
-
if (!btn) return;
|
| 170 |
-
const panel = document.getElementById(`panel-${btn.dataset.panel}`);
|
| 171 |
-
if (!panel) return;
|
| 172 |
-
// Recorre todos los campos editables del panel y los resetea, incluyendo indicadores visuales de estado
|
| 173 |
-
panel.querySelectorAll('input[type="number"], input[type="text"], input[type="url"], select').forEach(el => {
|
| 174 |
-
if (el.tagName === 'SELECT') {
|
| 175 |
-
el.selectedIndex = 0;
|
| 176 |
-
} else {
|
| 177 |
-
el.value = '';
|
| 178 |
-
el.classList.remove('alto', 'bajo', 'max-chars');
|
| 179 |
-
const span = el.previousElementSibling;
|
| 180 |
-
if (span?.classList.contains('estado-campo')) {
|
| 181 |
-
span.textContent = '';
|
| 182 |
-
span.className = 'estado-campo';
|
| 183 |
-
}
|
| 184 |
-
}
|
| 185 |
-
});
|
| 186 |
-
evaluar();
|
| 187 |
-
});
|
| 188 |
-
|
| 189 |
-
document.querySelector('.boton-analizar').addEventListener('click', async () => {
|
| 190 |
-
// Si el usuario no esta logueado, abre el modal de auth y encola la llamada a IA como callback
|
| 191 |
-
const autenticado = await verificarAuth();
|
| 192 |
-
if (!autenticado) {
|
| 193 |
-
abrirModalAuth(() => {
|
| 194 |
-
colapsarPatrones(true);
|
| 195 |
-
llamarIA(obtenerDatosPaciente, obtenerValoresFormulario, () => ultimoAnalisis, () => referencias);
|
| 196 |
-
});
|
| 197 |
-
return;
|
| 198 |
-
}
|
| 199 |
-
colapsarPatrones(true);
|
| 200 |
-
llamarIA(obtenerDatosPaciente, obtenerValoresFormulario, () => ultimoAnalisis, () => referencias);
|
| 201 |
-
});
|
| 202 |
-
|
| 203 |
-
document.querySelector('.boton-papers').addEventListener('click', () => {
|
| 204 |
-
abrirModalPapers(ultimoAnalisis.patrones);
|
| 205 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/papers.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
| 1 |
-
const PROXY_URL = 'api/papers_proxy.php';
|
| 2 |
-
const POR_PAGINA = 10;
|
| 3 |
-
|
| 4 |
-
let todosLosPapers = [];
|
| 5 |
-
let paginaActual = 0;
|
| 6 |
-
let consultaActual = '';
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
//Esto nos permite hacer búsquedas en la API de PubMed, sólo recibe texto en inglés
|
| 10 |
-
|
| 11 |
-
const TERMINOS_EN = {
|
| 12 |
-
'Anemia': 'anemia',
|
| 13 |
-
'Eritrocitosis': 'erythrocytosis polycythemia',
|
| 14 |
-
'Leucocitosis': 'leukocytosis',
|
| 15 |
-
'Leucocitosis neutrofílica': 'neutrophilic leukocytosis',
|
| 16 |
-
'Leucocitosis linfocítica': 'lymphocytic leukocytosis',
|
| 17 |
-
'Leucopenia': 'leukopenia',
|
| 18 |
-
'Eosinofilia': 'eosinophilia',
|
| 19 |
-
'Neutropenia': 'neutropenia',
|
| 20 |
-
'Linfopenia': 'lymphopenia',
|
| 21 |
-
'Trombocitopenia': 'thrombocytopenia',
|
| 22 |
-
'Trombocitosis': 'thrombocytosis',
|
| 23 |
-
'Daño hepatocelular': 'hepatocellular damage liver injury',
|
| 24 |
-
'Elevación de ALT aislada': 'ALT elevation liver',
|
| 25 |
-
'Patrón colestásico': 'cholestasis',
|
| 26 |
-
'Hiperbilirrubinemia': 'hyperbilirubinemia jaundice',
|
| 27 |
-
'Azotemia': 'azotemia renal failure',
|
| 28 |
-
'Hiperuremia aislada (BUN)': 'elevated BUN prerenal azotemia',
|
| 29 |
-
'BUN disminuido': 'low BUN hepatic failure',
|
| 30 |
-
'Creatinina elevada (BUN normal)': 'elevated creatinine kidney',
|
| 31 |
-
'Hiperglucemia': 'hyperglycemia diabetes mellitus',
|
| 32 |
-
'Hipoglucemia': 'hypoglycemia',
|
| 33 |
-
'Hiperproteinemia': 'hyperproteinemia',
|
| 34 |
-
'Hipoproteinemia / Hipoalbuminemia': 'hypoproteinemia hypoalbuminemia',
|
| 35 |
-
'Hipoalbuminemia': 'hypoalbuminemia',
|
| 36 |
-
'Hipercalcemia': 'hypercalcemia',
|
| 37 |
-
'Hipocalcemia': 'hypocalcemia',
|
| 38 |
-
'Hipernatremia': 'hypernatremia',
|
| 39 |
-
'Hiponatremia': 'hyponatremia',
|
| 40 |
-
'Hiperpotasemia': 'hyperkalemia',
|
| 41 |
-
'Hipopotasemia': 'hypokalemia',
|
| 42 |
-
'Hiperfosforemia': 'hyperphosphatemia',
|
| 43 |
-
'Hipotiroidismo': 'hypothyroidism',
|
| 44 |
-
'Hipertiroidismo': 'hyperthyroidism',
|
| 45 |
-
'Hiperadrenocorticismo (Cushing)': 'hyperadrenocorticism Cushing',
|
| 46 |
-
'Hipoadrenocorticismo (Addison)': 'hypoadrenocorticism Addison',
|
| 47 |
-
'Ratio Na:K reducido — sospecha de hipoadrenocorticismo': 'hypoadrenocorticism sodium potassium ratio',
|
| 48 |
-
'Cortisol basal bajo — posible hipoadrenocorticismo': 'low basal cortisol hypoadrenocorticism',
|
| 49 |
-
'Hiposthenuria': 'hyposthenuria urine specific gravity',
|
| 50 |
-
'Isosthenuria': 'isosthenuria urine concentration renal',
|
| 51 |
-
'Posible déficit de insulina': 'insulin deficiency hyperglycemia diabetes mellitus',
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
const traducirPatron = (nombre) => {
|
| 55 |
-
for (const [es, en] of Object.entries(TERMINOS_EN)) {
|
| 56 |
-
if (nombre.startsWith(es)) return en;
|
| 57 |
-
}
|
| 58 |
-
return nombre.replace(/[áéíóúñ]/g, c => ({á:'a',é:'e',í:'i',ó:'o',ú:'u',ñ:'n'})[c] || c);
|
| 59 |
-
};
|
| 60 |
-
|
| 61 |
-
const construirQuery = (patrones) => {
|
| 62 |
-
if (!patrones || patrones.length === 0) return 'veterinary clinical laboratory diagnosis canine feline';
|
| 63 |
-
// Limita a 3 terminos para mantener la query enfocada y evitar resultados irrelevantes
|
| 64 |
-
const terminos = [...new Set(patrones.map(p => traducirPatron(p.nombre)))].slice(0, 3);
|
| 65 |
-
return `${terminos.join(' ')} canine OR feline veterinary`;
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
const buscarPapers = async (query) => {
|
| 69 |
-
const respuesta = await fetch(`${PROXY_URL}?query=${encodeURIComponent(query)}`);
|
| 70 |
-
if (!respuesta.ok) {
|
| 71 |
-
const detalle = await respuesta.json().catch(() => ({}));
|
| 72 |
-
throw new Error(detalle.error || `Error ${respuesta.status}`);
|
| 73 |
-
}
|
| 74 |
-
const datos = await respuesta.json();
|
| 75 |
-
return datos.data || [];
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
const renderizarTarjetaPaper = (paper) => {
|
| 79 |
-
const titulo = paper.title || 'Sin título';
|
| 80 |
-
const autores = paper.authors?.slice(0, 3).map(a => a.name).join(', ') || 'Autores desconocidos';
|
| 81 |
-
const masAutores = (paper.authors?.length || 0) > 3 ? ' et al.' : '';
|
| 82 |
-
const anio = paper.year || '—';
|
| 83 |
-
const revista = paper.journal || '';
|
| 84 |
-
|
| 85 |
-
const urlDoi = paper.doi ? `https://doi.org/${paper.doi}` : null;
|
| 86 |
-
const urlPubmed = paper.pmid ? `https://pubmed.ncbi.nlm.nih.gov/${paper.pmid}/` : null;
|
| 87 |
-
const urlPrincipal = urlDoi || urlPubmed;
|
| 88 |
-
|
| 89 |
-
const articulo = document.createElement('article');
|
| 90 |
-
articulo.className = 'paper-tarjeta';
|
| 91 |
-
|
| 92 |
-
const meta = document.createElement('div');
|
| 93 |
-
meta.className = 'paper-meta';
|
| 94 |
-
|
| 95 |
-
const spanAnio = document.createElement('span');
|
| 96 |
-
spanAnio.className = 'paper-anio';
|
| 97 |
-
spanAnio.textContent = anio;
|
| 98 |
-
meta.append(spanAnio);
|
| 99 |
-
|
| 100 |
-
if (revista) {
|
| 101 |
-
const spanRevista = document.createElement('span');
|
| 102 |
-
spanRevista.className = 'paper-revista';
|
| 103 |
-
spanRevista.textContent = revista;
|
| 104 |
-
meta.append(spanRevista);
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
articulo.append(meta);
|
| 108 |
-
|
| 109 |
-
const h3 = document.createElement('h3');
|
| 110 |
-
h3.className = 'paper-titulo';
|
| 111 |
-
|
| 112 |
-
if (urlPrincipal) {
|
| 113 |
-
const link = document.createElement('a');
|
| 114 |
-
link.href = urlPrincipal;
|
| 115 |
-
link.target = '_blank';
|
| 116 |
-
link.rel = 'noopener noreferrer';
|
| 117 |
-
link.textContent = titulo;
|
| 118 |
-
h3.append(link);
|
| 119 |
-
} else {
|
| 120 |
-
h3.textContent = titulo;
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
articulo.append(h3);
|
| 124 |
-
|
| 125 |
-
const pAutores = document.createElement('p');
|
| 126 |
-
pAutores.className = 'paper-autores';
|
| 127 |
-
pAutores.textContent = autores + masAutores;
|
| 128 |
-
articulo.append(pAutores);
|
| 129 |
-
|
| 130 |
-
return articulo;
|
| 131 |
-
};
|
| 132 |
-
|
| 133 |
-
const renderizarPaginacion = () => {
|
| 134 |
-
const totalPaginas = Math.ceil(todosLosPapers.length / POR_PAGINA);
|
| 135 |
-
const contenedor = document.getElementById('papers-paginacion');
|
| 136 |
-
if (!contenedor) return;
|
| 137 |
-
|
| 138 |
-
if (totalPaginas <= 1) {
|
| 139 |
-
contenedor.innerHTML = '';
|
| 140 |
-
return;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
// Ventana deslizante de maximo 5 botones centrada en la pagina actual
|
| 144 |
-
const inicio = Math.max(0, paginaActual - 2);
|
| 145 |
-
const fin = Math.min(totalPaginas, inicio + 5);
|
| 146 |
-
|
| 147 |
-
let html = `<button class="papers-pag-btn" data-pagina="${paginaActual - 1}" ${paginaActual === 0 ? 'disabled' : ''} aria-label="Página anterior"><img src="assets/icons/anterior.svg" alt="" aria-hidden="true" width="16" height="16"></button>`;
|
| 148 |
-
for (let i = inicio; i < fin; i++) {
|
| 149 |
-
html += `<button class="papers-pag-btn ${i === paginaActual ? 'activo' : ''}" data-pagina="${i}">${i + 1}</button>`;
|
| 150 |
-
}
|
| 151 |
-
html += `<button class="papers-pag-btn" data-pagina="${paginaActual + 1}" ${paginaActual >= totalPaginas - 1 ? 'disabled' : ''} aria-label="Página siguiente"><img src="assets/icons/siguiente.svg" alt="" aria-hidden="true" width="16" height="16"></button>`;
|
| 152 |
-
|
| 153 |
-
contenedor.innerHTML = html;
|
| 154 |
-
};
|
| 155 |
-
|
| 156 |
-
const renderizarPaginaActual = () => {
|
| 157 |
-
const lista = document.getElementById('papers-lista');
|
| 158 |
-
if (!lista) return;
|
| 159 |
-
|
| 160 |
-
const inicio = paginaActual * POR_PAGINA;
|
| 161 |
-
const pagina = todosLosPapers.slice(inicio, inicio + POR_PAGINA);
|
| 162 |
-
|
| 163 |
-
lista.replaceChildren();
|
| 164 |
-
|
| 165 |
-
if (pagina.length === 0) {
|
| 166 |
-
const vacio = document.createElement('p');
|
| 167 |
-
vacio.className = 'papers-vacio';
|
| 168 |
-
vacio.textContent = 'No se encontraron artículos para esta búsqueda.';
|
| 169 |
-
lista.append(vacio);
|
| 170 |
-
} else {
|
| 171 |
-
lista.append(...pagina.map(renderizarTarjetaPaper));
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
renderizarPaginacion();
|
| 175 |
-
lista.scrollTop = 0;
|
| 176 |
-
};
|
| 177 |
-
|
| 178 |
-
const irAPagina = (numeroPagina) => {
|
| 179 |
-
const totalPaginas = Math.ceil(todosLosPapers.length / POR_PAGINA);
|
| 180 |
-
if (numeroPagina < 0 || numeroPagina >= totalPaginas) return;
|
| 181 |
-
paginaActual = numeroPagina;
|
| 182 |
-
renderizarPaginaActual();
|
| 183 |
-
};
|
| 184 |
-
|
| 185 |
-
const mostrarEstadoCarga = () => {
|
| 186 |
-
const lista = document.getElementById('papers-lista');
|
| 187 |
-
if (lista) lista.innerHTML = '<p class="papers-cargando">Buscando artículos científicos…</p>';
|
| 188 |
-
const paginacion = document.getElementById('papers-paginacion');
|
| 189 |
-
if (paginacion) paginacion.innerHTML = '';
|
| 190 |
-
};
|
| 191 |
-
|
| 192 |
-
const mostrarError = (mensaje) => {
|
| 193 |
-
const lista = document.getElementById('papers-lista');
|
| 194 |
-
if (!lista) return;
|
| 195 |
-
const p = document.createElement('p');
|
| 196 |
-
p.className = 'papers-error';
|
| 197 |
-
p.textContent = mensaje;
|
| 198 |
-
lista.replaceChildren(p);
|
| 199 |
-
};
|
| 200 |
-
|
| 201 |
-
export const abrirModalPapers = async (patrones) => {
|
| 202 |
-
const modal = document.getElementById('modal-papers');
|
| 203 |
-
const overlay = document.getElementById('modal-papers-overlay');
|
| 204 |
-
if (!modal || !overlay) return;
|
| 205 |
-
|
| 206 |
-
const nuevaConsulta = construirQuery(patrones);
|
| 207 |
-
|
| 208 |
-
modal.removeAttribute('hidden');
|
| 209 |
-
overlay.classList.add('activo');
|
| 210 |
-
document.body.style.overflow = 'hidden';
|
| 211 |
-
requestAnimationFrame(() => modal.classList.add('visible'));
|
| 212 |
-
|
| 213 |
-
const etiquetaConsulta = document.getElementById('papers-consulta');
|
| 214 |
-
if (etiquetaConsulta) etiquetaConsulta.textContent = `"${nuevaConsulta}"`;
|
| 215 |
-
|
| 216 |
-
// Reutiliza resultados si la consulta no cambio desde la ultima vez
|
| 217 |
-
if (nuevaConsulta === consultaActual && todosLosPapers.length > 0) {
|
| 218 |
-
renderizarPaginaActual();
|
| 219 |
-
return;
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
consultaActual = nuevaConsulta;
|
| 223 |
-
todosLosPapers = [];
|
| 224 |
-
paginaActual = 0;
|
| 225 |
-
mostrarEstadoCarga();
|
| 226 |
-
|
| 227 |
-
try {
|
| 228 |
-
todosLosPapers = await buscarPapers(nuevaConsulta);
|
| 229 |
-
renderizarPaginaActual();
|
| 230 |
-
} catch (error) {
|
| 231 |
-
mostrarError(error.message || 'No se pudo conectar con PubMed. Intenta de nuevo más tarde.');
|
| 232 |
-
console.error('Error buscando papers:', error);
|
| 233 |
-
}
|
| 234 |
-
};
|
| 235 |
-
|
| 236 |
-
const cerrarModalPapers = () => {
|
| 237 |
-
const modal = document.getElementById('modal-papers');
|
| 238 |
-
const overlay = document.getElementById('modal-papers-overlay');
|
| 239 |
-
if (!modal || !overlay) return;
|
| 240 |
-
|
| 241 |
-
modal.classList.remove('visible');
|
| 242 |
-
overlay.classList.remove('activo');
|
| 243 |
-
document.body.style.overflow = '';
|
| 244 |
-
setTimeout(() => modal.setAttribute('hidden', ''), 250);
|
| 245 |
-
};
|
| 246 |
-
|
| 247 |
-
export const inicializarModalPapers = () => {
|
| 248 |
-
document.getElementById('modal-papers-cerrar')?.addEventListener('click', cerrarModalPapers);
|
| 249 |
-
document.getElementById('modal-papers-overlay')?.addEventListener('click', cerrarModalPapers);
|
| 250 |
-
|
| 251 |
-
document.addEventListener('keydown', (e) => {
|
| 252 |
-
if (e.key === 'Escape') {
|
| 253 |
-
const modal = document.getElementById('modal-papers');
|
| 254 |
-
if (modal && !modal.hidden) cerrarModalPapers();
|
| 255 |
-
}
|
| 256 |
-
});
|
| 257 |
-
|
| 258 |
-
document.getElementById('papers-paginacion')?.addEventListener('click', (e) => {
|
| 259 |
-
const btn = e.target.closest('.papers-pag-btn');
|
| 260 |
-
if (!btn || btn.disabled) return;
|
| 261 |
-
irAPagina(parseInt(btn.dataset.pagina, 10));
|
| 262 |
-
});
|
| 263 |
-
|
| 264 |
-
document.getElementById('papers-busqueda-form')?.addEventListener('submit', async (e) => {
|
| 265 |
-
e.preventDefault();
|
| 266 |
-
const input = document.getElementById('papers-busqueda-input');
|
| 267 |
-
const termino = input?.value.trim();
|
| 268 |
-
if (!termino) return;
|
| 269 |
-
|
| 270 |
-
const etiquetaConsulta = document.getElementById('papers-consulta');
|
| 271 |
-
if (etiquetaConsulta) etiquetaConsulta.textContent = `"${termino}"`;
|
| 272 |
-
|
| 273 |
-
consultaActual = termino;
|
| 274 |
-
todosLosPapers = [];
|
| 275 |
-
paginaActual = 0;
|
| 276 |
-
mostrarEstadoCarga();
|
| 277 |
-
|
| 278 |
-
try {
|
| 279 |
-
todosLosPapers = await buscarPapers(termino);
|
| 280 |
-
renderizarPaginaActual();
|
| 281 |
-
} catch (error) {
|
| 282 |
-
mostrarError(error.message || 'No se pudo conectar con PubMed. Intenta de nuevo más tarde.');
|
| 283 |
-
console.error('Error buscando papers:', error);
|
| 284 |
-
}
|
| 285 |
-
});
|
| 286 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/pdf-parser.js
DELETED
|
@@ -1,477 +0,0 @@
|
|
| 1 |
-
// Extracción de texto de PDF y parseo de valores de laboratorio en el cliente (ningún dato sale del navegador)
|
| 2 |
-
|
| 3 |
-
const PDFJS_WORKER = 'assets/lib/pdfjs/pdf.worker.min.js';
|
| 4 |
-
|
| 5 |
-
// Cada entrada: nombre de campo (coincide con input[name="…"]) → regex + claveConv opcional
|
| 6 |
-
// claveConv → sobreescribe la clave de conversión (permite reglas distintas para el mismo campo)
|
| 7 |
-
const DEFS_ANALITOS = [
|
| 8 |
-
// Hematología: Serie Roja
|
| 9 |
-
{ campo: 'rbc', re: /\b(?:eritrocit\w*|gl[oó]bulos?\s+rojos?|r\.?b\.?c\.?|eri)\b/i },
|
| 10 |
-
{ campo: 'hgb', re: /\b(?:hemoglobin[ao]?\w*|hgb|hb)\b(?!a\d)/i },
|
| 11 |
-
{ campo: 'hct', re: /\b(?:hematocrit[oo]?\w*|hct|pcv)\b/i },
|
| 12 |
-
{ campo: 'vcm', re: /\b(?:v\.?c\.?m\.?|m\.?c\.?v\.?|vol(?:umen)?\s+corp\w*)\b/i },
|
| 13 |
-
// CHCM debe ir antes que HCM para evitar que MCH coincida con MCHC al usar lookahead negativo
|
| 14 |
-
{ campo: 'chcm', re: /\b(?:c\.?h\.?c\.?m\.?|m\.?c\.?h\.?c\.?|concentr\w+\s+hem\w+\s+corp\w*)\b/i },
|
| 15 |
-
{ campo: 'hcm', re: /\b(?:h\.?c\.?m\.?|m\.?c\.?h\.?)(?![cC]\.?)\b/i },
|
| 16 |
-
{ campo: 'rdw', re: /\b(?:r\.?d\.?w\.?(?:-cv)?|anch\w+\s+distrib\w+)\b/i },
|
| 17 |
-
{ campo: 'reti', re: /\b(?:reti\w*\s*%|ret\.?\s*%|ret[eé])\b/i },
|
| 18 |
-
{ campo: 'reti_abs', re: /\b(?:reti\w*\s*#|ret\.?\s*#)\b/i },
|
| 19 |
-
{ campo: 'nrbc', re: /\b(?:n\.?r\.?b\.?c\.?|eritrocit\w+\s+nucle\w+|nucleat\w+\s+r\.?b\.?c\.?|nrbc)\b/i },
|
| 20 |
-
|
| 21 |
-
// Hematología: Serie Blanca
|
| 22 |
-
{ campo: 'wbc', re: /\b(?:leucocit\w*|w\.?b\.?c\.?|white\s+blood\s+cell|leu)\b/i },
|
| 23 |
-
{ campo: 'neutro_abs', re: /\bgran?#/i },
|
| 24 |
-
// gran?(?!#) y similares: evitan coincidir con abreviaturas "GRA#"/"LYM#"/"EOS#"
|
| 25 |
-
// cuando se busca la forma porcentual — éstas aparecen antes en informes
|
| 26 |
-
// que listan conteos absolutos en una página y porcentajes en otra
|
| 27 |
-
{ campo: 'neutro', re: /\b(?:neutr[oó]fil\w*|neut\b|neu\b|gran?(?!#))\b/i },
|
| 28 |
-
{ campo: 'linfo_abs', re: /\blymp?#/i },
|
| 29 |
-
{ campo: 'linfo', re: /\b(?:linf[oa]cit\w*|lymph\w*|linf\b|lym(?!#))\b/i },
|
| 30 |
-
{ campo: 'mono_abs', re: /\bmon\w*#/i },
|
| 31 |
-
{ campo: 'mono', re: /\b(?:monocit\w*|mono\b|mon(?!#))\b/i },
|
| 32 |
-
{ campo: 'eosino_abs', re: /\beos\w*#/i },
|
| 33 |
-
{ campo: 'eosino', re: /\b(?:eosino\w*|eos(?!#))\b/i },
|
| 34 |
-
{ campo: 'baso_abs', re: /\bbas\w*#/i },
|
| 35 |
-
{ campo: 'baso', re: /\b(?:bas[oó]fil\w*|bas(?!#))\b/i },
|
| 36 |
-
|
| 37 |
-
// Hematología: Plaquetas
|
| 38 |
-
{ campo: 'plt', re: /\b(?:plaqueta\w*|platelet\w*|plt\b|trc\b)\b/i },
|
| 39 |
-
{ campo: 'mpv', re: /\b(?:m\.?p\.?v\.?|vol(?:umen)?\s+plaquetario\s+medio)\b/i },
|
| 40 |
-
{ campo: 'pct', re: /\b(?:p\.?c\.?t\.?\b|plaquetocrit\w*)\b/i },
|
| 41 |
-
|
| 42 |
-
// Bioquímica: Enzimas Hepáticas
|
| 43 |
-
{ campo: 'alt', re: /\b(?:alt\b|gpt\b|alanin[ao]?\s+amino\w*)\b/i },
|
| 44 |
-
{ campo: 'ast', re: /\b(?:ast\b|got\b|aspart\w*)\b/i },
|
| 45 |
-
{ campo: 'fal', re: /\b(?:fal\b|alp\b|fosfatasa\s+alcalin\w*|alkaline\s+phosph\w*)\b/i },
|
| 46 |
-
{ campo: 'ggt', re: /\b(?:g\.?g\.?t\.?|gamma\s*glutamil\w*|gama\s*glutamil\w*)\b/i },
|
| 47 |
-
|
| 48 |
-
// Bioquímica: Función Hepática
|
| 49 |
-
{ campo: 'bili', re: /\b(?:bilirrub\w*\s+total|total\s+bilirubin\w*|tbil\b)\b/i },
|
| 50 |
-
{ campo: 'bili', re: /\b(?:bilirrub\w*|bilirubin\w*|bili\b)\b/i }, // alternativa si no hubo coincidencia previa
|
| 51 |
-
{ campo: 'bili_dir', re: /\b(?:bilirrub\w*\s+direct\w*|direct\w*\s+bilirubin\w*|bili\s*dir\b)\b/i },
|
| 52 |
-
{ campo: 'acidos_bil', re: /\b(?:[aá]cid\w*\s+biliares?|bile\s+acids?|ácidos?\s+bil\w*)\b/i },
|
| 53 |
-
|
| 54 |
-
// Bioquímica: Función Renal
|
| 55 |
-
{ campo: 'bun', claveConv: 'bun', re: /\b(?:bun\b|nitr[oó]geno\s+ureico)\b/i },
|
| 56 |
-
{ campo: 'bun', claveConv: 'urea', re: /\burea\b/i },
|
| 57 |
-
{ campo: 'creat', re: /\b(?:creatinin[ao]?\w*|crea\b)\b/i },
|
| 58 |
-
{ campo: 'sdma', re: /\b(?:sdma\b|dimetilargin\w*|symmetric\s+dime\w*)\b/i },
|
| 59 |
-
|
| 60 |
-
// Bioquímica: Metabolitos
|
| 61 |
-
{ campo: 'gluc', re: /\b(?:gluco(?:sa|se)\b|glucemia\b|glu\b)\b/i },
|
| 62 |
-
{ campo: 'prot', re: /\b(?:prote[íi]nas?\s+totales?|prot\s+total|tp)\b/i },
|
| 63 |
-
{ campo: 'alb', re: /\b(?:alb[úu]min[ao]?\w*|alb\b)\b/i },
|
| 64 |
-
{ campo: 'glob', re: /\b(?:globulin\w*|glob\b)\b/i },
|
| 65 |
-
{ campo: 'fosf', re: /\b(?:f[oó]sforo\b|phosph\w*|phos\b)\b/i },
|
| 66 |
-
{ campo: 'calc', re: /\b(?:calcio\b|calcium\b|ca\b)\b/i },
|
| 67 |
-
{ campo: 'fruc', re: /\b(?:fructosamina\b|fructosamine\b|fruc\b)\b/i },
|
| 68 |
-
|
| 69 |
-
// Bioquímica: Electrolitos
|
| 70 |
-
{ campo: 'sodio', re: /\b(?:sodio\b|sodium\b)\b/i },
|
| 71 |
-
{ campo: 'potasio', re: /\b(?:potasio\b|potassium\b)\b/i },
|
| 72 |
-
{ campo: 'cloro', re: /\b(?:clor[ou]\w*|chloride\w*)\b/i },
|
| 73 |
-
{ campo: 'tco2', re: /\b(?:tco2\b|t\.?co\.?2\b|bicarbonat\w*|co2\s+total)\b/i },
|
| 74 |
-
|
| 75 |
-
// Bioquímica: Lípidos
|
| 76 |
-
{ campo: 'colest', re: /\b(?:colesterol\b|cholesterol\b|chol\b)\b/i },
|
| 77 |
-
{ campo: 'trigli', re: /\b(?:triglicérid\w*|triglic[eé]rid\w*|trig\b)\b/i },
|
| 78 |
-
|
| 79 |
-
// Bioquímica: Enzimas
|
| 80 |
-
{ campo: 'lipasa', re: /\b(?:lipas[ae]\b|lipa\b)\b/i },
|
| 81 |
-
{ campo: 'ck', re: /\b(?:c\.?k\.?\b|creatina?\s+kinas[ae]|creatine\s+kinas[ae])\b/i },
|
| 82 |
-
|
| 83 |
-
// Perfil Endocrino
|
| 84 |
-
{ campo: 'cortisol_bas', re: /\b(?:cortisol\s+bas[ae]?l?)\b/i },
|
| 85 |
-
{ campo: 'cortisol_acth', re: /\b(?:cortisol\s+(?:post[-\s]?acth|post)\b)/i },
|
| 86 |
-
{ campo: 't4_total', re: /\b(?:t4\s+total|t4\s+libre|tiroxin\w*|thyroxin\w*)\b/i },
|
| 87 |
-
{ campo: 'insulina', re: /\b(?:insulin[ao]?\w*)\b/i },
|
| 88 |
-
|
| 89 |
-
// Urianálisis
|
| 90 |
-
{ campo: 'usg', re: /\b(?:usg\b|densidad\s+(?:urin|orin)\w*|gravedad\s+esp\w*)\b/i },
|
| 91 |
-
{ campo: 'ph', re: /\b(?:ph\s+(?:urin|orin)\w*|ph\s+orina)\b/i },
|
| 92 |
-
];
|
| 93 |
-
|
| 94 |
-
// Campos select semicuantitativos
|
| 95 |
-
const DEFS_SEMICUANTITATIVOS = [
|
| 96 |
-
{ campo: 'uri-prot', re: /\b(?:prote[íi]nas?\s*(?:en\s*orina?|urin\w*)?|proteinuria)\b/i },
|
| 97 |
-
{ campo: 'uri-gluc', re: /\b(?:glucosuria\b|glucosa\s+(?:en\s*)?orina\w*)\b/i },
|
| 98 |
-
];
|
| 99 |
-
|
| 100 |
-
// Reglas de conversión de unidades por campo (o claveConv).
|
| 101 |
-
// Cada regla: { re → se evalúa contra los ~50 caracteres tras el valor, factor → número o fn(v)=>v }
|
| 102 |
-
// Gana la primera coincidencia. Sin coincidencia = valor usado tal cual (se asume unidad nativa de la app).
|
| 103 |
-
//
|
| 104 |
-
// Unidades de referencia de la app:
|
| 105 |
-
// rbc x10⁶/μL | hgb g/dL | hct % | vcm fL | chcm g/dL | hcm pg | rdw %
|
| 106 |
-
// wbc x10³/μL | plt x10³/μL | mpv fL | pct %
|
| 107 |
-
// reti % | reti_abs x10³/μL | nrbc /100WBC
|
| 108 |
-
// alt/ast/fal/ggt/lipasa/ck U/L
|
| 109 |
-
// bun mg/dL | creat mg/dL | sdma μg/dL
|
| 110 |
-
// gluc mg/dL | prot/alb/glob g/dL | bili/bili_dir mg/dL | fosf mg/dL | calc mg/dL
|
| 111 |
-
// fruc μmol/L | acidos_bil μmol/L
|
| 112 |
-
// sodio/potasio/cloro/tco2 mEq/L | colest/trigli mg/dL
|
| 113 |
-
// cortisol μg/dL | t4_total nmol/L | insulina μIU/mL
|
| 114 |
-
const CONVERSIONES_UNIDADES = {
|
| 115 |
-
hgb: [
|
| 116 |
-
{ re: /\bg\/L\b/i, factor: v => v / 10 },
|
| 117 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 1.6113 },
|
| 118 |
-
],
|
| 119 |
-
hct: [
|
| 120 |
-
{ re: /\bL\/L\b/i, factor: v => v < 1.5 ? v * 100 : v },
|
| 121 |
-
],
|
| 122 |
-
chcm: [
|
| 123 |
-
{ re: /\bg\/L\b/i, factor: v => v / 10 },
|
| 124 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 0.6206 },
|
| 125 |
-
],
|
| 126 |
-
pct: [
|
| 127 |
-
{ re: /\bL\/L\b/i, factor: v => v < 1.5 ? v * 100 : v },
|
| 128 |
-
],
|
| 129 |
-
wbc: [
|
| 130 |
-
// "/μL" sin prefijo ×10³ = conteo absoluto → dividir entre 1000
|
| 131 |
-
{ re: /^[\s]*\/[μuµ]?[Ll]\b/, factor: v => v > 100 ? v / 1000 : v },
|
| 132 |
-
],
|
| 133 |
-
plt: [
|
| 134 |
-
{ re: /^[\s]*\/[μuµ]?[Ll]\b/, factor: v => v > 1000 ? v / 1000 : v },
|
| 135 |
-
],
|
| 136 |
-
// claveConv "bun": etiquetado como BUN o nitrógeno ureico → ya es fracción nitrogenada
|
| 137 |
-
bun: [
|
| 138 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 2.8 },
|
| 139 |
-
],
|
| 140 |
-
// claveConv "urea": etiquetado como "Urea" → puede ser urea total, no fracción nitrogenada
|
| 141 |
-
urea: [
|
| 142 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 2.8 },
|
| 143 |
-
{ re: /\bmg\/dL\b/i, factor: v => v * 0.467 }, // urea total → BUN
|
| 144 |
-
],
|
| 145 |
-
creat: [
|
| 146 |
-
{ re: /\b[μuµ]mol\/L\b/i, factor: v => v / 88.4 },
|
| 147 |
-
],
|
| 148 |
-
sdma: [
|
| 149 |
-
{ re: /\bnmol\/L\b/i, factor: v => v / 5.899 },
|
| 150 |
-
{ re: /\b[μuµ]g\/L\b/i, factor: v => v / 10 },
|
| 151 |
-
],
|
| 152 |
-
gluc: [
|
| 153 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 18.016 },
|
| 154 |
-
],
|
| 155 |
-
prot: [
|
| 156 |
-
{ re: /\bg\/L\b/i, factor: v => v / 10 },
|
| 157 |
-
],
|
| 158 |
-
alb: [
|
| 159 |
-
{ re: /\bg\/L\b/i, factor: v => v / 10 },
|
| 160 |
-
],
|
| 161 |
-
glob: [
|
| 162 |
-
{ re: /\bg\/L\b/i, factor: v => v / 10 },
|
| 163 |
-
],
|
| 164 |
-
bili: [
|
| 165 |
-
{ re: /\b[μuµ]mol\/L\b/i, factor: v => v / 17.1 },
|
| 166 |
-
],
|
| 167 |
-
bili_dir: [
|
| 168 |
-
{ re: /\b[μuµ]mol\/L\b/i, factor: v => v / 17.1 },
|
| 169 |
-
],
|
| 170 |
-
fosf: [
|
| 171 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 3.097 },
|
| 172 |
-
],
|
| 173 |
-
calc: [
|
| 174 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 4.008 },
|
| 175 |
-
{ re: /\bm[Ee]q\/L\b/, factor: v => v * 2.004 },
|
| 176 |
-
],
|
| 177 |
-
colest: [
|
| 178 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 38.67 },
|
| 179 |
-
],
|
| 180 |
-
trigli: [
|
| 181 |
-
{ re: /\bmmol\/L\b/i, factor: v => v * 88.57 },
|
| 182 |
-
],
|
| 183 |
-
cortisol_bas: [
|
| 184 |
-
{ re: /\bnmol\/L\b/i, factor: v => v / 27.59 },
|
| 185 |
-
],
|
| 186 |
-
cortisol_acth: [
|
| 187 |
-
{ re: /\bnmol\/L\b/i, factor: v => v / 27.59 },
|
| 188 |
-
],
|
| 189 |
-
t4_total: [
|
| 190 |
-
// Unidad de la app: nmol/L
|
| 191 |
-
{ re: /\b[μuµ]g\/dL\b/i, factor: v => v * 12.87 },
|
| 192 |
-
{ re: /\bng\/dL\b/i, factor: v => v * 0.01287 },
|
| 193 |
-
{ re: /\bng\/mL\b/i, factor: v => v * 0.1287 },
|
| 194 |
-
],
|
| 195 |
-
insulina: [
|
| 196 |
-
{ re: /\bpmol\/L\b/i, factor: v => v / 6.945 },
|
| 197 |
-
],
|
| 198 |
-
};
|
| 199 |
-
|
| 200 |
-
function aplicarConversion(campo, claveConv, value, cadenaUnidad) {
|
| 201 |
-
const key = claveConv || campo;
|
| 202 |
-
const reglas = CONVERSIONES_UNIDADES[key];
|
| 203 |
-
if (!reglas) return value;
|
| 204 |
-
// Aplica la primera regla cuya regex coincida con la cadena de unidad detectada
|
| 205 |
-
for (const regla of reglas) {
|
| 206 |
-
if (regla.re.test(cadenaUnidad)) {
|
| 207 |
-
const f = regla.factor;
|
| 208 |
-
const convertido = typeof f === 'function' ? f(value) : value * f;
|
| 209 |
-
return Math.round(convertido * 10000) / 10000;
|
| 210 |
-
}
|
| 211 |
-
}
|
| 212 |
-
return value;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
// Retorna { num, unit } donde unit es la cadena de ~50 caracteres tras el valor numerico.
|
| 216 |
-
// Las reglas de conversion evaluan su regex contra esta cadena para decidir el factor.
|
| 217 |
-
function extraerValorYUnidad(contexto) {
|
| 218 |
-
const m = contexto.match(/[<>≤≥]?\s*(\d+(?:[.,]\d+)?)([\s\S]*)/);
|
| 219 |
-
if (!m) return { num: null, unit: '' };
|
| 220 |
-
const v = parseFloat(m[1].replace(',', '.'));
|
| 221 |
-
if (!isFinite(v) || v <= 0) return { num: null, unit: '' };
|
| 222 |
-
return { num: v, unit: m[2].slice(0, 50) };
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
function parsearSemiCuantitativo(text) {
|
| 226 |
-
const t = text.toLowerCase();
|
| 227 |
-
if (/negati|nég|neg\b|ausente|absent|no\s+detect/.test(t)) return 'neg';
|
| 228 |
-
if (/\+{3}/.test(t)) return '+++';
|
| 229 |
-
if (/\+{2}/.test(t)) return '++';
|
| 230 |
-
if (/\+/.test(t)) return '+';
|
| 231 |
-
if (/traz|trace/.test(t)) return '+';
|
| 232 |
-
return null;
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
function parsearTextoLab(textoCrudo) {
|
| 236 |
-
const resultados = {};
|
| 237 |
-
|
| 238 |
-
for (const def of DEFS_ANALITOS) {
|
| 239 |
-
if (resultados[def.campo] !== undefined) continue;
|
| 240 |
-
const match = def.re.exec(textoCrudo);
|
| 241 |
-
if (!match) continue;
|
| 242 |
-
const contexto = textoCrudo.slice(match.index + match[0].length, match.index + match[0].length + 150);
|
| 243 |
-
const { num, unit } = extraerValorYUnidad(contexto);
|
| 244 |
-
if (num === null) continue;
|
| 245 |
-
resultados[def.campo] = aplicarConversion(def.campo, def.claveConv, num, unit);
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
// Derivar % desde conteos absolutos si el % no se encontro directamente y se conoce el WBC
|
| 249 |
-
if (resultados.wbc && resultados.wbc > 0) {
|
| 250 |
-
for (const f of ['neutro', 'linfo', 'mono', 'eosino', 'baso']) {
|
| 251 |
-
if (resultados[f] === undefined && resultados[`${f}_abs`] !== undefined) {
|
| 252 |
-
const pct = Math.round((resultados[`${f}_abs`] / resultados.wbc) * 100);
|
| 253 |
-
if (pct >= 0 && pct <= 100) resultados[f] = pct;
|
| 254 |
-
}
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
// Derivar % de reticulocitos desde el conteo absoluto y RBC si no se encontro directamente
|
| 259 |
-
// reti_abs (x10³/uL) / (rbc (x10⁶/uL) * 10) = reti%
|
| 260 |
-
if (resultados.rbc && resultados.rbc > 0 && resultados.reti === undefined && resultados.reti_abs !== undefined) {
|
| 261 |
-
const pct = resultados.reti_abs / (resultados.rbc * 10);
|
| 262 |
-
if (pct >= 0 && pct <= 20) resultados.reti = Math.round(pct * 100) / 100;
|
| 263 |
-
}
|
| 264 |
-
|
| 265 |
-
for (const def of DEFS_SEMICUANTITATIVOS) {
|
| 266 |
-
if (resultados[def.campo] !== undefined) continue;
|
| 267 |
-
const match = def.re.exec(textoCrudo);
|
| 268 |
-
if (!match) continue;
|
| 269 |
-
const contexto = textoCrudo.slice(match.index, match.index + 80);
|
| 270 |
-
const val = parsearSemiCuantitativo(contexto);
|
| 271 |
-
if (val) resultados[def.campo] = val;
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
return resultados;
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
// Detección de información del paciente
|
| 278 |
-
|
| 279 |
-
const RAZAS_CANINO = [
|
| 280 |
-
'labrador', 'golden retriever', 'golden', 'pastor alemán', 'pastor aleman', 'pastor',
|
| 281 |
-
'poodle', 'caniche', 'beagle', 'bulldog', 'dachshund', 'salchicha', 'teckel',
|
| 282 |
-
'husky', 'chihuahu', 'maltés', 'maltes', 'yorkshire', 'terrier', 'doberman',
|
| 283 |
-
'rottweiler', 'boxer', 'bóxer', 'schnauzer', 'cocker', 'spaniel',
|
| 284 |
-
'border collie', 'border', 'dálmata', 'dalmatian', 'pitbull', 'pit bull',
|
| 285 |
-
'american staffordshire', 'samoyedo', 'akita', 'shiba', 'galgo', 'greyhound',
|
| 286 |
-
'whippet', 'bichón', 'bichon', 'weimaraner', 'setter', 'pointer', 'vizsla',
|
| 287 |
-
'basset', 'mastín', 'mastin', 'mastiff', 'bullmastiff', 'dogo', 'cane corso',
|
| 288 |
-
'pomerania', 'pomeran', 'pequinés', 'pekinese', 'chow chow', 'shar pei',
|
| 289 |
-
'gran danés', 'great dane', 'san bernardo', 'saint bernard', 'bernese',
|
| 290 |
-
'spitz', 'pinscher', 'shih tzu', 'lhasa', 'basenji', 'rhodesian',
|
| 291 |
-
];
|
| 292 |
-
|
| 293 |
-
const RAZAS_FELINO = [
|
| 294 |
-
'persa', 'persian', 'siamés', 'siames', 'siamese', 'bengala', 'bengal',
|
| 295 |
-
'maine coon', 'ragdoll', 'abisinio', 'abyssinian', 'birmano', 'burmese',
|
| 296 |
-
'angora', 'sphynx', 'esfinge', 'scottish fold', 'scottish', 'munchkin',
|
| 297 |
-
'tonkinés', 'cornish rex', 'devon rex', 'noruego', 'norwegian',
|
| 298 |
-
'british shorthair', 'british', 'russian blue', 'azul ruso', 'ocicat',
|
| 299 |
-
'exótico', 'exotic shorthair', 'ragamuffin', 'balinés', 'balinese',
|
| 300 |
-
];
|
| 301 |
-
|
| 302 |
-
// Palabras clave de etiqueta que marcan el inicio de un nuevo campo (para detener la captura de raza)
|
| 303 |
-
const SIGUIENTE_ETIQUETA = /\b(?:edad|age|sexo|sex|g[eé]nero|gender|especie|species|dueño|owner|propietario|doctor|vet|fecha|date|n[uú]m|caso|case|id|muestra|sample|peso|weight)\b/i;
|
| 304 |
-
|
| 305 |
-
function inferEspecie(raza) {
|
| 306 |
-
const r = raza.toLowerCase();
|
| 307 |
-
if (RAZAS_CANINO.some(b => r.includes(b))) return 'Canino';
|
| 308 |
-
if (RAZAS_FELINO.some(b => r.includes(b))) return 'Felino';
|
| 309 |
-
return null;
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
function parsearTextoPaciente(textoCrudo) {
|
| 313 |
-
const p = {};
|
| 314 |
-
|
| 315 |
-
// Especie: tolera variaciones como "Canino", "Dog", "Felino", "Cat"
|
| 316 |
-
const coincEsp = textoCrudo.match(/\b(?:especies?|species|tipo(?:\s+de)?\s+animal)\s*:?\s{0,4}([A-Za-záéíóúÁÉÍÓÚñÑ]{3,20})/i);
|
| 317 |
-
if (coincEsp) {
|
| 318 |
-
const v = coincEsp[1].toLowerCase();
|
| 319 |
-
if (/can[io]|perro|dog/.test(v)) p.especie = 'Canino';
|
| 320 |
-
else if (/fel[io]|gat[ao]|cat/.test(v)) p.especie = 'Felino';
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
// Raza: corta en la siguiente etiqueta o doble espacio para evitar absorber campos adyacentes en tablas
|
| 324 |
-
const coincRaza = textoCrudo.match(/\b(?:raza|breed|race|cruce)\s*:?\s{0,4}([^\n\r;:]{2,60})/i);
|
| 325 |
-
if (coincRaza) {
|
| 326 |
-
const crudo = coincRaza[1];
|
| 327 |
-
const indiceParo = crudo.search(SIGUIENTE_ETIQUETA);
|
| 328 |
-
const limpiado = (indiceParo > 0 ? crudo.slice(0, indiceParo) : crudo)
|
| 329 |
-
.split(/\s{2,}/)[0]
|
| 330 |
-
.trim();
|
| 331 |
-
if (limpiado.length >= 2) p.raza = limpiado.length > 40 ? limpiado.slice(0, 40).trim() : limpiado;
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
// Si no encontro especie pero si raza, infiere la especie a partir de listas de razas conocidas
|
| 335 |
-
if (!p.especie && p.raza) p.especie = inferEspecie(p.raza);
|
| 336 |
-
|
| 337 |
-
// Sexo: soporta abreviaturas (M, F, H) y variantes como "Esterilizada"
|
| 338 |
-
const coincSex = textoCrudo.match(/\b(?:sexo|sex[ou]?|g[eé]nero|gender)\s*:?\s{0,4}([^\n\r;:]{1,30})/i);
|
| 339 |
-
if (coincSex) {
|
| 340 |
-
const v = coincSex[1].trim();
|
| 341 |
-
if (/\b(?:macho|male|castrado|neutered)\b/i.test(v) || /^m\.?\s*$/i.test(v)) p.sexo = 'Macho';
|
| 342 |
-
else if (/\b(?:hembra|female|esterilizada?|spayed)\b/i.test(v) || /^[fh]\.?\s*$/i.test(v)) p.sexo = 'Hembra';
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
// Edad: extrae numero y unidad, normalizando comas decimales
|
| 346 |
-
const coincEdad = textoCrudo.match(/\b(?:edad|age)\s*:?\s{0,4}(\d+(?:[.,]\d+)?)\s*(a[ñn]os?|years?|yr?s?|meses?|months?)\b/i);
|
| 347 |
-
if (coincEdad) {
|
| 348 |
-
p.edad = parseFloat(coincEdad[1].replace(',', '.'));
|
| 349 |
-
p.edadUnidad = /^m/i.test(coincEdad[2]) ? 'meses' : 'anyos';
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
return p;
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
function aplicarPacienteAFormulario(patient) {
|
| 356 |
-
const MAPA = [
|
| 357 |
-
{ id: 'pt-especie', mobId: 'mob-pt-especie', key: 'especie', evt: 'change' },
|
| 358 |
-
{ id: 'pt-raza', mobId: 'mob-pt-raza', key: 'raza', evt: 'input' },
|
| 359 |
-
{ id: 'pt-edad', mobId: 'mob-pt-edad', key: 'edad', evt: 'input' },
|
| 360 |
-
{ id: 'pt-edad-unidad', mobId: 'mob-pt-edad-unidad', key: 'edadUnidad', evt: 'change' },
|
| 361 |
-
{ id: 'pt-sexo', mobId: 'mob-pt-sexo', key: 'sexo', evt: 'change' },
|
| 362 |
-
];
|
| 363 |
-
let contador = 0;
|
| 364 |
-
for (const { id, mobId, key, evt } of MAPA) {
|
| 365 |
-
const val = patient[key];
|
| 366 |
-
if (val === undefined) continue;
|
| 367 |
-
const el = document.getElementById(id);
|
| 368 |
-
const mob = document.getElementById(mobId);
|
| 369 |
-
if (!el) continue;
|
| 370 |
-
const valorCadena = String(val);
|
| 371 |
-
if (el.tagName === 'SELECT') {
|
| 372 |
-
const opcion = [...el.options].find(o => o.value === valorCadena || o.text === valorCadena);
|
| 373 |
-
if (!opcion) continue;
|
| 374 |
-
el.value = opcion.value;
|
| 375 |
-
if (mob) mob.value = opcion.value;
|
| 376 |
-
} else {
|
| 377 |
-
el.value = valorCadena;
|
| 378 |
-
if (mob) mob.value = valorCadena;
|
| 379 |
-
}
|
| 380 |
-
el.dispatchEvent(new Event(evt, { bubbles: true }));
|
| 381 |
-
contador++;
|
| 382 |
-
}
|
| 383 |
-
return contador;
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
async function cargarPdfJs() {
|
| 387 |
-
if (window.pdfjsLib) return window.pdfjsLib;
|
| 388 |
-
await new Promise((resolve, reject) => {
|
| 389 |
-
const script = document.createElement('script');
|
| 390 |
-
script.src = 'assets/lib/pdfjs/pdf.min.js';
|
| 391 |
-
script.onload = resolve;
|
| 392 |
-
script.onerror = reject;
|
| 393 |
-
document.head.appendChild(script);
|
| 394 |
-
});
|
| 395 |
-
return window.pdfjsLib;
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
async function extraerTextoPdf(file) {
|
| 399 |
-
const pdfjs = await cargarPdfJs();
|
| 400 |
-
if (!pdfjs) throw new Error('PDF.js no cargado');
|
| 401 |
-
pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER;
|
| 402 |
-
|
| 403 |
-
const buf = await file.arrayBuffer();
|
| 404 |
-
const pdf = await pdfjs.getDocument({ data: buf }).promise;
|
| 405 |
-
const paginas = [];
|
| 406 |
-
for (let p = 1; p <= pdf.numPages; p++) {
|
| 407 |
-
const page = await pdf.getPage(p);
|
| 408 |
-
const content = await page.getTextContent();
|
| 409 |
-
paginas.push(content.items.map(i => i.str + (i.hasEOL ? '\n' : ' ')).join(''));
|
| 410 |
-
}
|
| 411 |
-
return paginas.join('\n');
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
function aplicarAFormulario(resultados, evaluar) {
|
| 415 |
-
let contador = 0;
|
| 416 |
-
for (const [campo, value] of Object.entries(resultados)) {
|
| 417 |
-
const el = document.querySelector(`[name="${campo}"]`);
|
| 418 |
-
if (!el) continue;
|
| 419 |
-
if (el.tagName === 'SELECT') {
|
| 420 |
-
if ([...el.options].some(o => o.value === value)) {
|
| 421 |
-
el.value = value;
|
| 422 |
-
contador++;
|
| 423 |
-
}
|
| 424 |
-
} else {
|
| 425 |
-
el.value = value;
|
| 426 |
-
contador++;
|
| 427 |
-
}
|
| 428 |
-
}
|
| 429 |
-
if (contador > 0) evaluar();
|
| 430 |
-
return contador;
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
function mostrarToast(mensaje, error = false) {
|
| 434 |
-
let el = document.getElementById('pdf-toast');
|
| 435 |
-
if (!el) {
|
| 436 |
-
el = document.createElement('div');
|
| 437 |
-
el.id = 'pdf-toast';
|
| 438 |
-
document.body.appendChild(el);
|
| 439 |
-
}
|
| 440 |
-
el.textContent = mensaje;
|
| 441 |
-
el.className = 'pdf-toast' + (error ? ' pdf-toast--error' : '');
|
| 442 |
-
el.classList.add('pdf-toast--show');
|
| 443 |
-
clearTimeout(el._t);
|
| 444 |
-
el._t = setTimeout(() => el.classList.remove('pdf-toast--show'), 3500);
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
export function inicializarParserPdf(evaluar) {
|
| 448 |
-
document.querySelectorAll('.btn-importar-pdf').forEach(btn => {
|
| 449 |
-
btn.addEventListener('click', e => {
|
| 450 |
-
e.stopPropagation();
|
| 451 |
-
document.getElementById(`pdf-input-${btn.dataset.panel}`)?.click();
|
| 452 |
-
});
|
| 453 |
-
});
|
| 454 |
-
|
| 455 |
-
document.querySelectorAll('.pdf-input').forEach(input => {
|
| 456 |
-
input.addEventListener('change', async () => {
|
| 457 |
-
const file = input.files[0];
|
| 458 |
-
if (!file) return;
|
| 459 |
-
input.value = '';
|
| 460 |
-
try {
|
| 461 |
-
const textoCrudo = await extraerTextoPdf(file);
|
| 462 |
-
const resultados = parsearTextoLab(textoCrudo);
|
| 463 |
-
const contadorLab = aplicarAFormulario(resultados, evaluar);
|
| 464 |
-
const patient = parsearTextoPaciente(textoCrudo);
|
| 465 |
-
const contadorPac = aplicarPacienteAFormulario(patient);
|
| 466 |
-
const partes = [];
|
| 467 |
-
if (contadorLab > 0) partes.push(`${contadorLab} valor${contadorLab !== 1 ? 'es' : ''}`);
|
| 468 |
-
if (contadorPac > 0) partes.push('datos del paciente');
|
| 469 |
-
mostrarToast(partes.length > 0
|
| 470 |
-
? `${partes.join(' y ')} importados del PDF.`
|
| 471 |
-
: 'No se encontraron datos reconocibles en el PDF.', partes.length === 0);
|
| 472 |
-
} catch {
|
| 473 |
-
mostrarToast('Error al leer el PDF. ¿Es un PDF con texto (no escaneado)?', true);
|
| 474 |
-
}
|
| 475 |
-
});
|
| 476 |
-
});
|
| 477 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/tooltip.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
const burbuja = document.createElement('div');
|
| 2 |
-
burbuja.id = 'tooltip-global';
|
| 3 |
-
document.body.appendChild(burbuja);
|
| 4 |
-
|
| 5 |
-
const MARGEN = 8;
|
| 6 |
-
|
| 7 |
-
const posicionar = (el) => {
|
| 8 |
-
const rect = el.getBoundingClientRect();
|
| 9 |
-
const enFooter = el.closest('.nav-inferior') !== null;
|
| 10 |
-
|
| 11 |
-
burbuja.style.left = '';
|
| 12 |
-
burbuja.style.right = '';
|
| 13 |
-
|
| 14 |
-
let top, left;
|
| 15 |
-
|
| 16 |
-
if (enFooter) {
|
| 17 |
-
top = rect.top - burbuja.offsetHeight - MARGEN;
|
| 18 |
-
} else {
|
| 19 |
-
top = rect.bottom + MARGEN;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
left = rect.left + rect.width / 2 - burbuja.offsetWidth / 2;
|
| 23 |
-
|
| 24 |
-
const margenLateral = 6;
|
| 25 |
-
if (left < margenLateral) left = margenLateral;
|
| 26 |
-
if (left + burbuja.offsetWidth > window.innerWidth - margenLateral) {
|
| 27 |
-
left = window.innerWidth - burbuja.offsetWidth - margenLateral;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
burbuja.style.top = `${top + window.scrollY}px`;
|
| 31 |
-
burbuja.style.left = `${left}px`;
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
let temporizador;
|
| 35 |
-
|
| 36 |
-
document.addEventListener('mouseover', (e) => {
|
| 37 |
-
const el = e.target.closest('[data-tooltip]');
|
| 38 |
-
if (!el) return;
|
| 39 |
-
|
| 40 |
-
clearTimeout(temporizador);
|
| 41 |
-
temporizador = setTimeout(() => {
|
| 42 |
-
burbuja.textContent = el.dataset.tooltip;
|
| 43 |
-
burbuja.classList.add('visible');
|
| 44 |
-
posicionar(el);
|
| 45 |
-
}, 400);
|
| 46 |
-
});
|
| 47 |
-
|
| 48 |
-
document.addEventListener('mouseout', (e) => {
|
| 49 |
-
const el = e.target.closest('[data-tooltip]');
|
| 50 |
-
if (!el) return;
|
| 51 |
-
|
| 52 |
-
clearTimeout(temporizador);
|
| 53 |
-
burbuja.classList.remove('visible');
|
| 54 |
-
});
|
| 55 |
-
|
| 56 |
-
document.addEventListener('click', () => {
|
| 57 |
-
clearTimeout(temporizador);
|
| 58 |
-
burbuja.classList.remove('visible');
|
| 59 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/ui.js
DELETED
|
@@ -1,479 +0,0 @@
|
|
| 1 |
-
// Navegación-Pestañas
|
| 2 |
-
|
| 3 |
-
const tabs = document.querySelectorAll('.tab-nav');
|
| 4 |
-
const paneles = document.querySelectorAll('main > .panel, .col3-wrapper > .panel');
|
| 5 |
-
const examenesSubtabsBar = document.getElementById('examenes-subtabs-bar');
|
| 6 |
-
|
| 7 |
-
const EXAMENES_SUBTAB_PANELS = new Set(['panel-hema', 'panel-bioquim', 'panel-uri', 'panel-endo', 'panel-coag', 'panel-gas']);
|
| 8 |
-
let panelExamenActivo = 'panel-hema';
|
| 9 |
-
let panelActivo = 'panel-flujo';
|
| 10 |
-
|
| 11 |
-
const SWIPE_ORDER = ['panel-flujo', 'panel-paciente', 'panel-hema', 'panel-bioquim', 'panel-uri', 'panel-endo', 'panel-coag', 'panel-gas', 'panel-imagenes', 'panel-resultados'];
|
| 12 |
-
|
| 13 |
-
export function activarTab(targetId) {
|
| 14 |
-
const esSubpanelExamenes = EXAMENES_SUBTAB_PANELS.has(targetId);
|
| 15 |
-
const esTabExamenes = targetId === 'examenes';
|
| 16 |
-
const mostrarExamenes = esTabExamenes || esSubpanelExamenes;
|
| 17 |
-
|
| 18 |
-
// Si se clickea la tab generica "Examenes", muestra el ultimo subtab activo; si es un subtab, lo guarda
|
| 19 |
-
let idPanelActual;
|
| 20 |
-
if (esTabExamenes) {
|
| 21 |
-
idPanelActual = panelExamenActivo;
|
| 22 |
-
} else if (esSubpanelExamenes) {
|
| 23 |
-
panelExamenActivo = targetId;
|
| 24 |
-
idPanelActual = targetId;
|
| 25 |
-
} else {
|
| 26 |
-
idPanelActual = targetId;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
tabs.forEach(tab => {
|
| 30 |
-
const estaActivo = mostrarExamenes
|
| 31 |
-
? tab.dataset.target === 'examenes'
|
| 32 |
-
: tab.dataset.target === targetId;
|
| 33 |
-
tab.classList.toggle('activo', estaActivo);
|
| 34 |
-
tab.setAttribute('aria-current', estaActivo ? 'true' : 'false');
|
| 35 |
-
});
|
| 36 |
-
|
| 37 |
-
if (examenesSubtabsBar) examenesSubtabsBar.hidden = !mostrarExamenes;
|
| 38 |
-
|
| 39 |
-
if (mostrarExamenes) {
|
| 40 |
-
document.querySelectorAll('.tab-examenes').forEach(btn => {
|
| 41 |
-
btn.classList.toggle('activo', btn.dataset.subtabTarget === panelExamenActivo);
|
| 42 |
-
});
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
paneles.forEach(panel => {
|
| 46 |
-
panel.classList.toggle('activo', panel.id === idPanelActual);
|
| 47 |
-
});
|
| 48 |
-
|
| 49 |
-
panelActivo = idPanelActual;
|
| 50 |
-
if (targetId === 'panel-paciente') sincronizarPacienteMob();
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
tabs.forEach(tab => {
|
| 54 |
-
tab.addEventListener('click', () => activarTab(tab.dataset.target));
|
| 55 |
-
});
|
| 56 |
-
|
| 57 |
-
document.querySelectorAll('.tab-examenes').forEach(btn => {
|
| 58 |
-
btn.addEventListener('click', () => activarTab(btn.dataset.subtabTarget));
|
| 59 |
-
});
|
| 60 |
-
|
| 61 |
-
// Swipe para navegar entre secciones
|
| 62 |
-
|
| 63 |
-
let inicioSwipeX = 0;
|
| 64 |
-
let inicioSwipeY = 0;
|
| 65 |
-
|
| 66 |
-
document.querySelector('main').addEventListener('touchstart', e => {
|
| 67 |
-
inicioSwipeX = e.touches[0].clientX;
|
| 68 |
-
inicioSwipeY = e.touches[0].clientY;
|
| 69 |
-
}, { passive: true });
|
| 70 |
-
|
| 71 |
-
document.querySelector('main').addEventListener('touchend', e => {
|
| 72 |
-
const dx = e.changedTouches[0].clientX - inicioSwipeX;
|
| 73 |
-
const dy = e.changedTouches[0].clientY - inicioSwipeY;
|
| 74 |
-
// Ignora gestos cortos o verticales para no interferir con scroll
|
| 75 |
-
if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy)) return;
|
| 76 |
-
const indice = SWIPE_ORDER.indexOf(panelActivo);
|
| 77 |
-
const siguiente = dx < 0 ? SWIPE_ORDER[indice + 1] : SWIPE_ORDER[indice - 1];
|
| 78 |
-
if (siguiente) activarTab(siguiente);
|
| 79 |
-
}, { passive: true });
|
| 80 |
-
|
| 81 |
-
// Sincronizacón de datos de pacientes en mobile
|
| 82 |
-
|
| 83 |
-
const MAPA_MOB_CANON = {
|
| 84 |
-
'mob-pt-especie': 'pt-especie',
|
| 85 |
-
'mob-pt-raza': 'pt-raza',
|
| 86 |
-
'mob-pt-edad': 'pt-edad',
|
| 87 |
-
'mob-pt-edad-unidad': 'pt-edad-unidad',
|
| 88 |
-
'mob-pt-sexo': 'pt-sexo'
|
| 89 |
-
};
|
| 90 |
-
|
| 91 |
-
function sincronizarPacienteMob() {
|
| 92 |
-
Object.entries(MAPA_MOB_CANON).forEach(([mobId, canonId]) => {
|
| 93 |
-
const mobEl = document.getElementById(mobId);
|
| 94 |
-
const canonEl = document.getElementById(canonId);
|
| 95 |
-
if (mobEl && canonEl) mobEl.value = canonEl.value;
|
| 96 |
-
});
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
export function inicializarSincMob(evaluar) {
|
| 100 |
-
Object.entries(MAPA_MOB_CANON).forEach(([mobId, canonId]) => {
|
| 101 |
-
const mobEl = document.getElementById(mobId);
|
| 102 |
-
if (!mobEl) return;
|
| 103 |
-
const tipoEvento = mobEl.tagName === 'SELECT' ? 'change' : 'input';
|
| 104 |
-
mobEl.addEventListener(tipoEvento, () => {
|
| 105 |
-
const canonEl = document.getElementById(canonId);
|
| 106 |
-
if (canonEl) canonEl.value = mobEl.value;
|
| 107 |
-
evaluar();
|
| 108 |
-
});
|
| 109 |
-
});
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
// Filas de grid
|
| 113 |
-
|
| 114 |
-
const panelFlujo = document.getElementById('panel-flujo');
|
| 115 |
-
const btnColapsar = document.getElementById('btn-colapsar-flujo');
|
| 116 |
-
const mainEl = document.querySelector('main');
|
| 117 |
-
|
| 118 |
-
let filaColapsada = '';
|
| 119 |
-
let filaExpandida = '';
|
| 120 |
-
|
| 121 |
-
const esGridEscritorio = () => window.innerWidth > 1100;
|
| 122 |
-
|
| 123 |
-
function inicializarFilasGrid() {
|
| 124 |
-
if (!esGridEscritorio()) return;
|
| 125 |
-
mainEl.style.gridTemplateRows = '1fr auto auto auto';
|
| 126 |
-
|
| 127 |
-
// Mide el panel de flujo expandido y colapsado para animar grid-template-rows con precision
|
| 128 |
-
const alturaPanel = panelFlujo.getBoundingClientRect().height;
|
| 129 |
-
const alturaEncabezado = panelFlujo.querySelector('.panel-cabecera').getBoundingClientRect().height;
|
| 130 |
-
if (alturaPanel > 0) filaExpandida = `${alturaPanel}px`;
|
| 131 |
-
if (alturaEncabezado > 0) filaColapsada = `${alturaEncabezado}px`;
|
| 132 |
-
|
| 133 |
-
mainEl.style.gridTemplateRows = `1fr auto auto ${filaExpandida || 'auto'}`;
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
function establecerFilasGrid(colapsado, animar) {
|
| 137 |
-
if (!esGridEscritorio()) return;
|
| 138 |
-
if (!animar) mainEl.style.transition = 'none';
|
| 139 |
-
mainEl.style.gridTemplateRows = colapsado
|
| 140 |
-
? `1fr auto auto ${filaColapsada}`
|
| 141 |
-
: `1fr auto auto ${filaExpandida}`;
|
| 142 |
-
if (!animar) {
|
| 143 |
-
mainEl.offsetHeight;
|
| 144 |
-
mainEl.style.transition = '';
|
| 145 |
-
}
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
inicializarFilasGrid();
|
| 149 |
-
|
| 150 |
-
const inicioColapsado = localStorage.getItem('mx-flujo-collapsed') === '1';
|
| 151 |
-
if (inicioColapsado) {
|
| 152 |
-
panelFlujo.classList.add('collapsed');
|
| 153 |
-
btnColapsar.setAttribute('aria-expanded', 'false');
|
| 154 |
-
establecerFilasGrid(true, false);
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
btnColapsar.addEventListener('click', () => {
|
| 158 |
-
const colapsado = panelFlujo.classList.toggle('collapsed');
|
| 159 |
-
btnColapsar.setAttribute('aria-expanded', String(!colapsado));
|
| 160 |
-
establecerFilasGrid(colapsado, true);
|
| 161 |
-
localStorage.setItem('mx-flujo-collapsed', colapsado ? '1' : '0');
|
| 162 |
-
if (!colapsado) {
|
| 163 |
-
['panel-endo', 'panel-uri', 'panel-coag', 'panel-gas'].forEach(id => {
|
| 164 |
-
const sp = document.getElementById(id);
|
| 165 |
-
if (sp) establecerSubpanelColapsado(sp, true);
|
| 166 |
-
});
|
| 167 |
-
}
|
| 168 |
-
});
|
| 169 |
-
|
| 170 |
-
window.addEventListener('resize', () => {
|
| 171 |
-
if (esGridEscritorio()) {
|
| 172 |
-
if (!panelFlujo.classList.contains('collapsed')) inicializarFilasGrid();
|
| 173 |
-
} else {
|
| 174 |
-
document.querySelectorAll('.subpanel-anim').forEach(animEl => {
|
| 175 |
-
animEl.style.height = '';
|
| 176 |
-
animEl.style.transition = '';
|
| 177 |
-
});
|
| 178 |
-
document.getElementById('subpanel-citologia')
|
| 179 |
-
?.querySelector('.subpanel-anim')
|
| 180 |
-
?.style.setProperty('height', '');
|
| 181 |
-
}
|
| 182 |
-
});
|
| 183 |
-
|
| 184 |
-
// Paneles colapsables
|
| 185 |
-
|
| 186 |
-
function establecerSubpanelColapsado(subpanel, debeColapsar) {
|
| 187 |
-
if (!esGridEscritorio()) return;
|
| 188 |
-
const animEl = subpanel.querySelector('.subpanel-anim');
|
| 189 |
-
const btn = subpanel.querySelector('.btn-colapsar-subpanel');
|
| 190 |
-
const esRelleno = subpanel.id === 'subpanel-citologia';
|
| 191 |
-
if (debeColapsar === subpanel.classList.contains('collapsed')) return;
|
| 192 |
-
subpanel.classList.toggle('collapsed', debeColapsar);
|
| 193 |
-
if (btn) btn.setAttribute('aria-expanded', String(!debeColapsar));
|
| 194 |
-
// Forzar reflujo antes de cambiar height permite que CSS transition anime correctamente
|
| 195 |
-
if (debeColapsar) {
|
| 196 |
-
animEl.style.height = `${animEl.offsetHeight}px`;
|
| 197 |
-
animEl.offsetHeight;
|
| 198 |
-
animEl.style.height = '0px';
|
| 199 |
-
} else {
|
| 200 |
-
animEl.style.height = `${animEl.scrollHeight}px`;
|
| 201 |
-
if (esRelleno) {
|
| 202 |
-
animEl.addEventListener('transitionend', () => {
|
| 203 |
-
animEl.style.height = '';
|
| 204 |
-
}, { once: true });
|
| 205 |
-
}
|
| 206 |
-
}
|
| 207 |
-
localStorage.setItem(`mx-${subpanel.id}-collapsed`, debeColapsar ? '1' : '0');
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
const GRUPOS_VINCULADOS = [
|
| 211 |
-
['panel-endo', 'panel-uri'],
|
| 212 |
-
['panel-coag', 'panel-gas'],
|
| 213 |
-
];
|
| 214 |
-
|
| 215 |
-
const PANELES_COLAPSADOS_POR_DEFECTO = new Set(['panel-uri', 'panel-endo', 'panel-coag', 'panel-gas']);
|
| 216 |
-
|
| 217 |
-
document.querySelectorAll('.btn-colapsar-subpanel').forEach(btn => {
|
| 218 |
-
const subpanel = btn.closest('.subpanel');
|
| 219 |
-
const animEl = subpanel.querySelector('.subpanel-anim');
|
| 220 |
-
const claveAlmacenamiento = `mx-${subpanel.id}-collapsed`;
|
| 221 |
-
const esRelleno = subpanel.id === 'subpanel-citologia';
|
| 222 |
-
|
| 223 |
-
if (esGridEscritorio()) {
|
| 224 |
-
if (!esRelleno) {
|
| 225 |
-
animEl.style.transition = 'none';
|
| 226 |
-
animEl.style.height = `${animEl.scrollHeight}px`;
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
const valorGuardado = localStorage.getItem(claveAlmacenamiento);
|
| 230 |
-
const debeColapsar = valorGuardado !== null
|
| 231 |
-
? valorGuardado === '1'
|
| 232 |
-
: PANELES_COLAPSADOS_POR_DEFECTO.has(subpanel.id);
|
| 233 |
-
|
| 234 |
-
if (debeColapsar) {
|
| 235 |
-
subpanel.classList.add('collapsed');
|
| 236 |
-
btn.setAttribute('aria-expanded', 'false');
|
| 237 |
-
if (esRelleno) animEl.style.transition = 'none';
|
| 238 |
-
animEl.style.height = '0px';
|
| 239 |
-
if (esRelleno) { animEl.offsetHeight; animEl.style.transition = ''; }
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
if (!esRelleno) { animEl.offsetHeight; animEl.style.transition = ''; }
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
btn.addEventListener('click', () => {
|
| 246 |
-
if (!esGridEscritorio()) return;
|
| 247 |
-
const colapsado = subpanel.classList.toggle('collapsed');
|
| 248 |
-
btn.setAttribute('aria-expanded', String(!colapsado));
|
| 249 |
-
if (colapsado) {
|
| 250 |
-
animEl.style.height = `${animEl.offsetHeight}px`;
|
| 251 |
-
animEl.offsetHeight;
|
| 252 |
-
animEl.style.height = '0px';
|
| 253 |
-
} else {
|
| 254 |
-
animEl.style.height = `${animEl.scrollHeight}px`;
|
| 255 |
-
if (esRelleno) {
|
| 256 |
-
animEl.addEventListener('transitionend', () => {
|
| 257 |
-
animEl.style.height = '';
|
| 258 |
-
}, { once: true });
|
| 259 |
-
}
|
| 260 |
-
}
|
| 261 |
-
localStorage.setItem(claveAlmacenamiento, colapsado ? '1' : '0');
|
| 262 |
-
|
| 263 |
-
const grupo = GRUPOS_VINCULADOS.find(g => g.includes(subpanel.id));
|
| 264 |
-
if (grupo) {
|
| 265 |
-
grupo.forEach(id => {
|
| 266 |
-
if (id !== subpanel.id) {
|
| 267 |
-
const asociado = document.getElementById(id);
|
| 268 |
-
if (asociado) establecerSubpanelColapsado(asociado, colapsado);
|
| 269 |
-
}
|
| 270 |
-
});
|
| 271 |
-
}
|
| 272 |
-
});
|
| 273 |
-
});
|
| 274 |
-
|
| 275 |
-
// Patrones de paneles colapsables
|
| 276 |
-
|
| 277 |
-
const btnColapsarPatrones = document.getElementById('btn-colapsar-patrones');
|
| 278 |
-
const patronesAnim = document.getElementById('patrones-anim');
|
| 279 |
-
|
| 280 |
-
export function colapsarPatrones(debeColapsar) {
|
| 281 |
-
const estaExpandido = btnColapsarPatrones.getAttribute('aria-expanded') === 'true';
|
| 282 |
-
const colapsado = debeColapsar ?? estaExpandido;
|
| 283 |
-
|
| 284 |
-
if (colapsado && estaExpandido) {
|
| 285 |
-
patronesAnim.style.height = `${patronesAnim.scrollHeight}px`;
|
| 286 |
-
patronesAnim.offsetHeight;
|
| 287 |
-
patronesAnim.style.height = '0px';
|
| 288 |
-
btnColapsarPatrones.setAttribute('aria-expanded', 'false');
|
| 289 |
-
} else if (!colapsado && !estaExpandido) {
|
| 290 |
-
patronesAnim.style.height = `${patronesAnim.scrollHeight}px`;
|
| 291 |
-
patronesAnim.addEventListener('transitionend', () => {
|
| 292 |
-
if (btnColapsarPatrones.getAttribute('aria-expanded') === 'true') {
|
| 293 |
-
patronesAnim.style.height = '';
|
| 294 |
-
}
|
| 295 |
-
}, { once: true });
|
| 296 |
-
btnColapsarPatrones.setAttribute('aria-expanded', 'true');
|
| 297 |
-
}
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
btnColapsarPatrones.addEventListener('click', () => colapsarPatrones());
|
| 301 |
-
|
| 302 |
-
// Imágenes
|
| 303 |
-
|
| 304 |
-
export const imagenesDataUrl = [null, null];
|
| 305 |
-
export const capturasMicroscopio = [];
|
| 306 |
-
|
| 307 |
-
const MAX_CAPTURAS_MICRO = 4;
|
| 308 |
-
|
| 309 |
-
document.querySelectorAll('.zona-imagen').forEach(zona => {
|
| 310 |
-
const indice = parseInt(zona.dataset.zona);
|
| 311 |
-
const input = zona.querySelector('.input-zona');
|
| 312 |
-
const vacia = zona.querySelector('.zona-vacia');
|
| 313 |
-
const btnQuitar = zona.querySelector('.btn-quitar-zona');
|
| 314 |
-
const vistaPrevia = document.createElement('img');
|
| 315 |
-
vistaPrevia.className = 'zona-img-preview';
|
| 316 |
-
vistaPrevia.alt = `Citología ${indice + 1}`;
|
| 317 |
-
vistaPrevia.hidden = true;
|
| 318 |
-
btnQuitar.before(vistaPrevia);
|
| 319 |
-
|
| 320 |
-
zona.addEventListener('click', e => {
|
| 321 |
-
if (btnQuitar.contains(e.target)) return;
|
| 322 |
-
input.click();
|
| 323 |
-
});
|
| 324 |
-
|
| 325 |
-
input.addEventListener('change', () => {
|
| 326 |
-
const file = input.files[0];
|
| 327 |
-
if (!file) return;
|
| 328 |
-
const reader = new FileReader();
|
| 329 |
-
reader.onload = ev => {
|
| 330 |
-
const img = new Image();
|
| 331 |
-
img.onload = () => {
|
| 332 |
-
// Reduce la imagen a max 1024px en su lado mayor para no saturar la memoria ni la API
|
| 333 |
-
const MAX_PIXELES = 1024;
|
| 334 |
-
const scale = Math.min(MAX_PIXELES / img.width, MAX_PIXELES / img.height, 1);
|
| 335 |
-
const canvas = document.createElement('canvas');
|
| 336 |
-
canvas.width = Math.round(img.width * scale);
|
| 337 |
-
canvas.height = Math.round(img.height * scale);
|
| 338 |
-
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
|
| 339 |
-
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
| 340 |
-
imagenesDataUrl[indice] = dataUrl;
|
| 341 |
-
vistaPrevia.src = dataUrl;
|
| 342 |
-
vistaPrevia.hidden = false;
|
| 343 |
-
btnQuitar.hidden = false;
|
| 344 |
-
vacia.hidden = true;
|
| 345 |
-
zona.classList.add('con-imagen');
|
| 346 |
-
};
|
| 347 |
-
img.src = ev.target.result;
|
| 348 |
-
};
|
| 349 |
-
reader.readAsDataURL(file);
|
| 350 |
-
});
|
| 351 |
-
|
| 352 |
-
btnQuitar.addEventListener('click', e => {
|
| 353 |
-
e.stopPropagation();
|
| 354 |
-
imagenesDataUrl[indice] = null;
|
| 355 |
-
vistaPrevia.src = '';
|
| 356 |
-
vistaPrevia.hidden = true;
|
| 357 |
-
btnQuitar.hidden = true;
|
| 358 |
-
vacia.hidden = false;
|
| 359 |
-
zona.classList.remove('con-imagen');
|
| 360 |
-
input.value = '';
|
| 361 |
-
});
|
| 362 |
-
});
|
| 363 |
-
|
| 364 |
-
// Captura de microscopio
|
| 365 |
-
|
| 366 |
-
(function () {
|
| 367 |
-
const zona = document.querySelector('.zona-microscopio');
|
| 368 |
-
if (!zona) return;
|
| 369 |
-
|
| 370 |
-
const micVacia = zona.querySelector('.micro-vacia');
|
| 371 |
-
const video = zona.querySelector('.micro-video');
|
| 372 |
-
const controles = zona.querySelector('.micro-controles');
|
| 373 |
-
const btnGaleria = zona.querySelector('.micro-btn-galeria');
|
| 374 |
-
const badge = zona.querySelector('.micro-badge');
|
| 375 |
-
const btnCapturar = zona.querySelector('.micro-btn-capturar');
|
| 376 |
-
const btnCerrar = zona.querySelector('.micro-btn-cerrar');
|
| 377 |
-
const galeriaEl = zona.querySelector('.micro-galeria');
|
| 378 |
-
|
| 379 |
-
let stream = null;
|
| 380 |
-
let galeriaEsVisible = false;
|
| 381 |
-
|
| 382 |
-
function detenerStream() {
|
| 383 |
-
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
function actualizarInsignia() {
|
| 387 |
-
const n = capturasMicroscopio.length;
|
| 388 |
-
badge.textContent = n;
|
| 389 |
-
badge.hidden = n === 0;
|
| 390 |
-
btnCapturar.disabled = n >= MAX_CAPTURAS_MICRO;
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
function renderizarGaleria() {
|
| 394 |
-
if (capturasMicroscopio.length === 0) {
|
| 395 |
-
galeriaEl.innerHTML = '<span class="micro-galeria-vacia">Sin capturas</span>';
|
| 396 |
-
return;
|
| 397 |
-
}
|
| 398 |
-
galeriaEl.innerHTML = capturasMicroscopio.map((src, i) => `
|
| 399 |
-
<div class="micro-thumb">
|
| 400 |
-
<img src="${src}" alt="Captura ${i + 1}">
|
| 401 |
-
<button class="micro-thumb-quitar" type="button" data-capture-idx="${i}" aria-label="Eliminar captura ${i + 1}">
|
| 402 |
-
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true">
|
| 403 |
-
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
| 404 |
-
</svg>
|
| 405 |
-
</button>
|
| 406 |
-
</div>`).join('');
|
| 407 |
-
|
| 408 |
-
galeriaEl.querySelectorAll('.micro-thumb-quitar').forEach(btn => {
|
| 409 |
-
btn.addEventListener('click', e => {
|
| 410 |
-
e.stopPropagation();
|
| 411 |
-
const i = parseInt(btn.dataset.captureIdx);
|
| 412 |
-
capturasMicroscopio.splice(i, 1);
|
| 413 |
-
actualizarInsignia();
|
| 414 |
-
renderizarGaleria();
|
| 415 |
-
if (capturasMicroscopio.length === 0 && galeriaEsVisible) alternarGaleria();
|
| 416 |
-
});
|
| 417 |
-
});
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
function alternarGaleria() {
|
| 421 |
-
galeriaEsVisible = !galeriaEsVisible;
|
| 422 |
-
galeriaEl.hidden = !galeriaEsVisible;
|
| 423 |
-
if (galeriaEsVisible) renderizarGaleria();
|
| 424 |
-
btnGaleria.style.color = galeriaEsVisible ? 'var(--accent)' : '';
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
async function abrirCamara() {
|
| 428 |
-
try {
|
| 429 |
-
// Preferencia por camara trasera (microscopio o movil apuntando a la muestra)
|
| 430 |
-
stream = await navigator.mediaDevices.getUserMedia({
|
| 431 |
-
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 } }
|
| 432 |
-
});
|
| 433 |
-
video.srcObject = stream;
|
| 434 |
-
video.hidden = false;
|
| 435 |
-
micVacia.hidden = true;
|
| 436 |
-
controles.hidden = false;
|
| 437 |
-
actualizarInsignia();
|
| 438 |
-
} catch {
|
| 439 |
-
// Permiso denegado o camara no disponible; no se requiere fallback
|
| 440 |
-
}
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
zona.addEventListener('click', e => {
|
| 444 |
-
if (controles.contains(e.target) || galeriaEl.contains(e.target)) return;
|
| 445 |
-
if (!stream) abrirCamara();
|
| 446 |
-
});
|
| 447 |
-
|
| 448 |
-
btnGaleria.addEventListener('click', e => {
|
| 449 |
-
e.stopPropagation();
|
| 450 |
-
alternarGaleria();
|
| 451 |
-
});
|
| 452 |
-
|
| 453 |
-
btnCapturar.addEventListener('click', e => {
|
| 454 |
-
e.stopPropagation();
|
| 455 |
-
if (capturasMicroscopio.length >= MAX_CAPTURAS_MICRO) return;
|
| 456 |
-
const canvas = document.createElement('canvas');
|
| 457 |
-
// Escala el fotograma de video para mantener un tamano razonable antes de enviarlo al modelo
|
| 458 |
-
const MAX_PIXELES = 1024;
|
| 459 |
-
const scale = Math.min(MAX_PIXELES / video.videoWidth, MAX_PIXELES / video.videoHeight, 1);
|
| 460 |
-
canvas.width = Math.round(video.videoWidth * scale);
|
| 461 |
-
canvas.height = Math.round(video.videoHeight * scale);
|
| 462 |
-
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
|
| 463 |
-
capturasMicroscopio.push(canvas.toDataURL('image/jpeg', 0.85));
|
| 464 |
-
actualizarInsignia();
|
| 465 |
-
if (galeriaEsVisible) renderizarGaleria();
|
| 466 |
-
});
|
| 467 |
-
|
| 468 |
-
btnCerrar.addEventListener('click', e => {
|
| 469 |
-
e.stopPropagation();
|
| 470 |
-
detenerStream();
|
| 471 |
-
video.hidden = true;
|
| 472 |
-
video.srcObject = null;
|
| 473 |
-
controles.hidden = true;
|
| 474 |
-
galeriaEl.hidden = true;
|
| 475 |
-
galeriaEsVisible = false;
|
| 476 |
-
micVacia.hidden = false;
|
| 477 |
-
});
|
| 478 |
-
})();
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|