aarnal80 commited on
Commit
f33e4aa
·
verified ·
1 Parent(s): c952be1

Update js/analysisModule.js

Browse files
Files changed (1) hide show
  1. js/analysisModule.js +235 -50
js/analysisModule.js CHANGED
@@ -1,56 +1,241 @@
1
- import { llmProviders } from './iaConfigModule.js';
2
-
3
- // Módulo de análisis médico con LLM configurado (llamadas front-end)
4
- export async function analyzeMedical(text) {
5
- if (!text) return '';
6
- const cfg = JSON.parse(localStorage.getItem('iaConfig')) || {};
7
- const provider = cfg.llm.provider;
8
- const provObj = llmProviders.find(p => p.value === provider) || {};
9
- // Obtener API key adecuada del proveedor
10
- const apiKey = cfg.llm.apiKeys?.[provider] ?? cfg.llm.apiKey;
11
- const model = cfg.llm.model;
12
- // Determinar endpoint según proveedor
13
- let url;
14
- if (provider === 'openai') {
15
- url = 'https://api.openai.com/v1/chat/completions';
16
- } else if (provider === 'deepseek') {
17
- url = 'https://api.deepseek.com/v1/chat/completions';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  } else {
19
- throw new Error('Proveedor no soportado');
20
- }
21
- const systemMessage = 'Eres un médico experto especializado en generar informes clínicos concisos y estructurados.';
22
- const userPrompt = `Te daré la transcripción detallada de mi conversación con la paciente y escribe una descripción detallada de la enfermedad actual y la exploración física de un paciente en contexto clínico, siguiendo estas características:\n
23
- Enfermedad actual:\n- Incluye la edad, el género y el motivo de consulta del paciente. (si no te doy el dato, omite).\n- Detalla evolución de síntomas y su progresión.\n- Describe signos y antecedentes relevantes con lenguaje técnico comprensible.\n
24
- Exploración física:\n- Describe hallazgos objetivos observados en la exploración.\n- Usa términos médicos precisos, sin juicios diagnósticos.\n
25
- Tareas del modelo:\n- Responde en dos párrafos, sin títulos 'Enfermedad actual:' ni 'Exploración física:'.\n- El primero para la enfermedad actual.\n- El segundo para la exploración.\n
26
- Transcripción: ${text}`;
27
  let messages;
28
  if (provider === 'openai') {
29
- // Some OpenAI models don't support 'system' role
30
- messages = [
31
- { role: 'user', content: `${systemMessage}\n\n${userPrompt}` }
32
- ];
33
  } else {
34
- messages = [
35
- { role: 'system', content: systemMessage },
36
- { role: 'user', content: userPrompt }
37
- ];
38
- }
39
- // Enviar solicitud directa al proveedor
40
- const headers = {
41
- 'Content-Type': 'application/json',
42
- 'Authorization': `Bearer ${apiKey}`
43
- };
44
- const payload = { model, messages, temperature: 0.5 };
45
- const res = await fetch(url, {
46
- method: 'POST',
47
- headers,
48
- body: JSON.stringify(payload)
49
- });
50
- if (!res.ok) {
51
- const err = await res.text();
52
- throw new Error(`Error en análisis médico: ${res.status} ${err}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
- const data = await res.json();
55
- return data.choices?.[0]?.message?.content || '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
 
 
1
+ // js/labAnalysisModule.js
2
+
3
+ // Importar funciones/datos necesarios de otros módulos
4
+ import { getIaConfig, llmProviders } from './iaConfigModule.js';
5
+
6
+ /**
7
+ * Construye el prompt para que la IA analice los resultados de laboratorio.
8
+ * (Sin cambios en esta función)
9
+ * @param {string} rawLabText - El texto crudo pegado por el usuario.
10
+ * @returns {string} El prompt formateado como string.
11
+ */
12
+ function buildLabAnalysisPrompt(rawLabText) {
13
+ // --- INICIO PROMPT LABORATORIO ---
14
+ const prompt = `Quiero que me ayudes a ordenar resultados de análisis médicos. Te proporcionaré datos de entrada desordenados y debes extraer los datos relevantes siguiendo el formato XLabs. El formato XLabs se define como seis categorías en este orden exacto, cada una en una línea continua:
15
+
16
+ Hematología
17
+ Coagulación
18
+ Bioquímica
19
+ Gasometría
20
+ Orina
21
+ Otras pruebas
22
+
23
+ La salida debe estar en español, formateada así:
24
+ Categoría: Parametro Resultado unidades | Parametro Resultado unidades | ...
25
+
26
+ Ejemplo de Entrada:
27
+ Glucosa * 171 mg/dL 74 - 109
28
+ Urea * 56 mg/dL 17 - 48
29
+ Creatinina 0.82 mg/dL 0.7
30
+
31
+ Ejemplo de Salida Esperada:
32
+ Bioquímica: Glucosa **171** mg/dL | Urea **56** mg/dL | Creatinina 0.82 mg/dL
33
+
34
+ Ajusta el formato con estas modificaciones específicas:
35
+ Hematología:
36
+ Incluye únicamente: Hematíes, Hemoglobina, Hematocrito, VCM, HCM.
37
+ Para elementos celulares (Leucocitos, Neutrófilos, Linfocitos, Monocitos, etc.): agrupa mostrando primero el porcentaje y, entre paréntesis, el valor absoluto. Ejemplo: Neutrofilos 49.1 % (4.03 10^9/L) | Linfocitos 39.8 % (3.27 10^9/L).
38
+ No incluyas Eosinófilos ni Basófilos si están dentro de los rangos normales.
39
+ Incluye Plaquetas, pero NO incluyas VPM.
40
+
41
+ Coagulación:
42
+ Resume los datos usando abreviaturas: "INR" para Ratio normalizado internacional, "PT" para Tiempo de protrombina (Quick), "PTT" para T. tromboplastina parcial activada.
43
+
44
+ Otras pruebas:
45
+ Si no hay elementos en una categoría (ej., "Otras pruebas: Ninguna"), OMITE completamente esa categoría en la salida. No escribas "Ninguna".
46
+
47
+ **MUY IMPORTANTE: Alerta de valores alterados:**
48
+ Si CUALQUIER parámetro está fuera del rango normal (alterado), **DEBES envolver el valor numérico resultado entre DOBLES ASTERISCOS**. Ejemplo: Glucosa **171** mg/dL. Asegúrate de aplicar esto consistentemente a TODOS y CADA UNO de los valores que identifiques como alterados según los rangos de referencia si están disponibles en la entrada, o si están marcados explícitamente como alterados (ej. con '*'). Si no puedes determinar si está alterado, no lo marques.
49
+
50
+ Reglas generales:
51
+ 1. Usa normas gramaticales y mayúsculas correctas (ej., "Hematíes" no "HEMATIES").
52
+ 2. Si un apartado (Categoría) no tiene ninguna prueba que mostrar después de aplicar las reglas, omite el apartado completo.
53
+ 3. NO uses formato bold (negrita) ni cambios de tamaño de texto en tu respuesta, EXCEPTO los dobles asteriscos (**valor**) para marcar los valores alterados.
54
+ 4. NO incluyas introducciones, conclusiones, ni frases extra (ej., evita decir "Aquí está el formato XLabs...", "Resultados formateados:", etc.).
55
+ 5. Proporciona ÚNICAMENTE la respuesta directa con los resultados formateados como se especifica.
56
+ 6. Sigue estrictamente TODAS estas instrucciones. No inventes ningún resultado.
57
+
58
+ Datos de laboratorio para analizar:
59
+ --- INICIO DATOS ---
60
+ ${rawLabText}
61
+ --- FIN DATOS ---`;
62
+ // --- FIN PROMPT LABORATORIO ---
63
+ return prompt;
64
+ }
65
+
66
+ /**
67
+ * Llama a la API del LLM configurado para analizar el texto de resultados de laboratorio.
68
+ * (Sin cambios en esta función)
69
+ * @param {string} text - El texto crudo de los resultados de laboratorio.
70
+ * @returns {Promise<string>} Una promesa que resuelve con la respuesta formateada de la IA como string.
71
+ */
72
+ export async function analyzeLabResults(text) {
73
+ // --- INICIO VALIDACIÓN ENTRADA Y CONFIGURACIÓN ---
74
+ if (!text || !text.trim()) {
75
+ console.warn("[labAnalysisModule] analyzeLabResults llamada con texto vacío.");
76
+ return Promise.resolve("");
77
+ }
78
+ console.log("[labAnalysisModule] Iniciando análisis de exámenes...");
79
+ const config = getIaConfig();
80
+ if (!config || !config.llm || !config.llm.provider || !config.llm.model) {
81
+ throw new Error("Configuración del LLM incompleta. Por favor, revisa la Configuración IA.");
82
+ }
83
+ const provider = config.llm.provider;
84
+ const model = config.llm.model;
85
+ const apiKey = config.llm.apiKeys?.[provider];
86
+ if (!apiKey) {
87
+ throw new Error(`No se encontró API Key para el proveedor LLM '${provider}'. Revisa la Configuración IA.`);
88
+ }
89
+ const providerDetails = llmProviders.find(p => p.value === provider);
90
+ if (!providerDetails || !providerDetails.url) {
91
+ throw new Error(`Detalles (URL) no encontrados para el proveedor LLM '${provider}'. Revisa la lista de proveedores en iaConfigModule.js.`);
92
+ }
93
+ // --- FIN VALIDACIÓN ENTRADA Y CONFIGURACIÓN ---
94
+
95
+ // --- INICIO PREPARACIÓN LLAMADA API ---
96
+ let apiUrl;
97
+ if (provider === 'openai' || provider === 'deepseek') {
98
+ apiUrl = `${providerDetails.url}/v1/chat/completions`;
99
  } else {
100
+ console.error(`[labAnalysisModule] Proveedor LLM '${provider}' no tiene un endpoint definido.`);
101
+ throw new Error(`Proveedor LLM '${provider}' no soportado actualmente por labAnalysisModule.`);
102
+ }
103
+ const systemMessage = "Eres un asistente experto en formatear resultados de análisis de laboratorio médicos siguiendo el formato XLabs y destacando valores alterados.";
104
+ const userPrompt = buildLabAnalysisPrompt(text);
 
 
 
105
  let messages;
106
  if (provider === 'openai') {
107
+ messages = [{ role: 'user', content: `${systemMessage}\n\n${userPrompt}` }];
 
 
 
108
  } else {
109
+ messages = [{ role: 'system', content: systemMessage }, { role: 'user', content: userPrompt }];
110
+ }
111
+ const payload = { model: model, messages: messages, temperature: 0.2 };
112
+ console.log(`[labAnalysisModule] Enviando petición a ${apiUrl} con modelo ${model}`);
113
+ // --- FIN PREPARACIÓN LLAMADA API ---
114
+
115
+ // --- INICIO EJECUCIÓN LLAMADA API Y MANEJO RESPUESTA ---
116
+ try {
117
+ const response = await fetch(apiUrl, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
120
+ body: JSON.stringify(payload)
121
+ });
122
+ if (!response.ok) {
123
+ const errorBody = await response.text();
124
+ console.error(`[labAnalysisModule] Error API ${response.status}: ${errorBody}`);
125
+ throw new Error(`Error ${response.status} de la API LLM: ${errorBody || response.statusText}`);
126
+ }
127
+ const data = await response.json();
128
+ const content = data.choices?.[0]?.message?.content;
129
+ if (!content) {
130
+ console.error("[labAnalysisModule] Respuesta inesperada de la API (sin contenido):", data);
131
+ throw new Error("La respuesta de la API no contiene el contenido esperado.");
132
+ }
133
+ console.log("[labAnalysisModule] Análisis de exámenes completado exitosamente.");
134
+ return content.trim();
135
+ } catch (error) {
136
+ console.error("[labAnalysisModule] Fallo en la llamada API o procesamiento:", error);
137
+ throw error; // Relanzar para manejo en main.js
138
+ }
139
+ // --- FIN EJECUCIÓN LLAMADA API Y MANEJO RESPUESTA ---
140
+ }
141
+
142
+
143
+ // --- INICIO: Función displayLabResults (MODIFICADA con filtro mejorado) ---
144
+ /**
145
+ * Muestra los resultados de laboratorio formateados en la UI, aplicando el filtro mejorado si se indica.
146
+ * @param {string} resultsText - El texto formateado recibido de la IA.
147
+ * @param {HTMLElement} containerElement - El elemento del DOM donde mostrar los resultados.
148
+ * @param {boolean} filterAltered - `true` si se debe mostrar solo resultados alterados y sus cabeceras.
149
+ */
150
+ export function displayLabResults(resultsText, containerElement, filterAltered) {
151
+ // --- INICIO VALIDACIÓN CONTENEDOR ---
152
+ if (!containerElement) {
153
+ console.error("[labAnalysisModule] El contenedor para mostrar resultados no fue encontrado en el DOM.");
154
+ return;
155
+ }
156
+ containerElement.innerHTML = ''; // Limpiar resultados anteriores
157
+ // --- FIN VALIDACIÓN CONTENEDOR ---
158
+
159
+ // --- INICIO MANEJO RESULTADOS VACÍOS ---
160
+ if (!resultsText || !resultsText.trim()) {
161
+ containerElement.textContent = 'No se generaron resultados o la respuesta estaba vacía.';
162
+ return;
163
  }
164
+ // --- FIN MANEJO RESULTADOS VACÍOS ---
165
+
166
+ // --- INICIO LÓGICA DE FILTRADO Y VISUALIZACIÓN ---
167
+ const lines = resultsText.split('\n').filter(line => line.trim() !== ''); // Dividir y quitar líneas vacías
168
+ let linesToDisplay = lines; // Por defecto, mostrar todas las líneas
169
+
170
+ // Aplicar filtro MEJORADO si está activo
171
+ if (filterAltered) {
172
+ console.log("[labAnalysisModule] Aplicando filtro de alterados (mejorado).");
173
+ const filteredLines = [];
174
+ let currentHeader = null; // Guarda la última cabecera encontrada
175
+ let currentAlteredResults = []; // Guarda resultados alterados bajo la cabecera actual
176
+ let headerAdded = false; // Flag para saber si ya añadimos la cabecera actual
177
+
178
+ for (const line of lines) {
179
+ const trimmedLine = line.trim();
180
+ const isHeader = trimmedLine.endsWith(':'); // Identifica cabeceras (ej: "Hematología:")
181
+ const hasAlteration = line.includes('**'); // Identifica líneas con alteración
182
+
183
+ if (isHeader) {
184
+ // Al encontrar una NUEVA cabecera, procesamos la anterior si tuvo resultados alterados
185
+ if (currentHeader && currentAlteredResults.length > 0) {
186
+ // La sección anterior sí tenía alteraciones, la añadimos completa
187
+ // (la cabecera ya se añadió cuando se encontró la primera alteración)
188
+ filteredLines.push(...currentAlteredResults);
189
+ }
190
+ // Reiniciamos para la nueva categoría
191
+ currentHeader = line; // Guardamos la nueva cabecera
192
+ currentAlteredResults = []; // Limpiamos la lista de resultados alterados
193
+ headerAdded = false; // Reseteamos el flag para la nueva cabecera
194
+ } else if (hasAlteration) {
195
+ // Es una línea de resultado Y está alterada
196
+ if (currentHeader && !headerAdded) {
197
+ // Si es la PRIMERA alteración que encontramos para esta cabecera, añadimos la cabecera PRIMERO
198
+ filteredLines.push(currentHeader);
199
+ headerAdded = true; // Marcamos que la cabecera ya fue añadida
200
+ }
201
+ // Añadimos la línea alterada a la lista temporal (si no hay cabecera, se añade igual por si acaso)
202
+ currentAlteredResults.push(line);
203
+ }
204
+ // Las líneas de resultado NO alteradas simplemente se ignoran cuando el filtro está activo
205
+ }
206
+
207
+ // Procesar la última categoría después de salir del bucle
208
+ if (currentHeader && currentAlteredResults.length > 0) {
209
+ // Si no se añadió la última cabecera (porque la única alteración fue la última línea)
210
+ if (!headerAdded) {
211
+ filteredLines.push(currentHeader);
212
+ }
213
+ filteredLines.push(...currentAlteredResults);
214
+ }
215
+
216
+ // Si después de filtrar no queda NADA, mostrar un mensaje
217
+ if (filteredLines.length === 0) {
218
+ linesToDisplay = ["No se encontraron resultados marcados como alterados."];
219
+ } else {
220
+ linesToDisplay = filteredLines; // Usar las líneas filtradas
221
+ }
222
+ }
223
+
224
+ // --- INICIO FORMATEO Y RENDERIZADO ---
225
+ // (Esta parte no cambia, solo opera sobre `linesToDisplay`)
226
+ const fragment = document.createDocumentFragment();
227
+ linesToDisplay.forEach(line => {
228
+ const p = document.createElement('p');
229
+ p.style.margin = '0 0 0.3em 0'; // Pequeño margen inferior
230
+ // Reemplazar **valor** por <strong> con estilo para resaltado rojo/negrita
231
+ p.innerHTML = line.replace(
232
+ /\*\*(.*?)\*\*/g, // Regex: captura texto entre ** (no greedy)
233
+ '<strong class="text-red-600 font-bold">$1</strong>' // $1 es el texto capturado
234
+ );
235
+ fragment.appendChild(p);
236
+ });
237
+ containerElement.appendChild(fragment); // Añadir al DOM
238
+ // --- FIN FORMATEO Y RENDERIZADO ---
239
+ // --- FIN LÓGICA DE FILTRADO Y VISUALIZACIÓN ---
240
  }
241
+ // --- FIN: Función displayLabResults ---