diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a5a3c28..0000000 --- a/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# Python cache files -__pycache__/ -*.py[cod] - -# Environment directories -venv/ -env/ -.env/ - -# IDE configurations -.vscode/ -.idea/ - -# Logs and data files -data/log.txt -data/data.json - -# Allow script groups and their configurations -!backend/script_groups/ -!backend/script_groups/*/ -!backend/script_groups/*/*.py -!backend/script_groups/*/schema.json -!backend/script_groups/*/esquema.json -!backend/script_groups/*/description.json - -# But ignore working directory configurations -backend/script_groups/*/work_dir.json diff --git a/__pycache__/config_manager.cpython-310.pyc b/__pycache__/config_manager.cpython-310.pyc index 88e9b57..a7dc8c1 100644 Binary files a/__pycache__/config_manager.cpython-310.pyc and b/__pycache__/config_manager.cpython-310.pyc differ diff --git a/app.py b/app.py index 2ddc32a..f4ea53b 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,12 @@ -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, url_for from flask_sock import Sock from config_manager import ConfigurationManager import os import json # Added import -app = Flask(__name__) +app = Flask( + __name__, static_url_path="", static_folder="static", template_folder="templates" +) sock = Sock(app) config_manager = ConfigurationManager() diff --git a/backend/script_groups/EmailCrono/config.json b/backend/script_groups/EmailCrono/config.json new file mode 100644 index 0000000..f9b1ad9 --- /dev/null +++ b/backend/script_groups/EmailCrono/config.json @@ -0,0 +1,6 @@ +{ + "input_dir": "C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails", + "output_dir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-InLavoro\\HENKEL\\93040 - HENKEL - BowlingGreen\\Description\\HENKEL - ALPLA - AUTEFA - Batch Data", + "cronologia_file": "cronologia.md", + "attachments_dir": "adjuntos" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/config/__pycache__/config.cpython-310.pyc b/backend/script_groups/EmailCrono/config/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..b771230 Binary files /dev/null and b/backend/script_groups/EmailCrono/config/__pycache__/config.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/config/beautify_rules.json b/backend/script_groups/EmailCrono/config/beautify_rules.json new file mode 100644 index 0000000..81c9fcf --- /dev/null +++ b/backend/script_groups/EmailCrono/config/beautify_rules.json @@ -0,0 +1,171 @@ +{ + "__documentation": { + "__format": "Las reglas siguen el siguiente formato:", + "pattern": "Patrón a buscar - puede ser texto o regex", + "replacement": "Texto que reemplazará al patrón (puede estar vacío)", + "action": "Tipo de acción: replace, remove_line, remove_block, add_before, add_after", + "type": "Cómo interpretar el patrón: string, regex, left, right, substring", + "priority": "Orden de ejecución (menor número = mayor prioridad)" + }, + "__examples": { + "replace": "Reemplaza texto: reemplaza cada coincidencia por el replacement", + "remove_line": "Elimina línea: elimina la línea completa si encuentra el patrón", + "remove_block": "Elimina bloque: elimina desde el inicio hasta el fin del patrón con .....", + "add_before": "Agrega antes: inserta el replacement antes de la línea con el patrón", + "add_after": "Agrega después: inserta el replacement después de la línea con el patrón" + }, + "rules": [ + { + "__comment": "Reemplaza non-breaking space por espacio normal", + "pattern": "\u00a0", + "replacement": " ", + "action": "replace", + "type": "string", + "priority": 1 + }, + { + "__comment": "Elimina marcador de mensaje original", + "pattern": "--- Messaggio originale ---", + "replacement": "***", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina firma de dispositivo móvil", + "pattern": "(?m)^Sent from my.*$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina aviso medioambiental", + "pattern": "(?m)^Please take care of the environment.*$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina aviso de mensaje automático", + "pattern": "(?m)^This message is from an.*$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina aviso de confidencialidad en italiano", + "pattern": "eventuali allegati sono confidenziali", + "replacement": "", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina aviso de confidencialidad en inglés", + "pattern": "any attachments are confidential", + "replacement": "", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina solicitud de LinkedIn", + "pattern": "Please sign up on our Linkedin", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina aviso de no compartir contenido", + "pattern": "di non copiare o condividere i contenuti con nessuno", + "replacement": "", + "action": "remove_line", + "type": "substring", + "priority": 2 + }, + { + "__comment": "Elimina líneas de email individual", + "pattern": "(?m)^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina líneas con múltiples emails", + "pattern": "(?m)(?:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\s*(?:;\\s*)?$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\s*;\\s*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}.*$)", + "replacement": "", + "action": "remove_line", + "type": "regex", + "priority": 2 + }, + { + "__comment": "Elimina línea de teléfono", + "pattern": "Phone:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de móvil", + "pattern": "Mobile:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de CC", + "pattern": "Cc:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de destinatario (italiano)", + "pattern": "A:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Elimina línea de destinatario", + "pattern": "To:", + "replacement": "", + "action": "remove_line", + "type": "left", + "priority": 2 + }, + { + "__comment": "Agrega separador antes del asunto", + "pattern": "Subject: ", + "replacement": "***", + "action": "add_before", + "type": "left", + "priority": 3 + }, + { + "__comment": "Elimina firma corporativa", + "pattern": "Strada Isolanda.....Website:www.vetromeccanica.it", + "replacement": "", + "action": "remove_block", + "type": "string", + "priority": 4 + }, + { + "__comment": "Elimina aviso legal largo", + "pattern": "IMPORTANT NOTICE: This message may.....without retaining any copy", + "replacement": "", + "action": "remove_block", + "type": "string", + "priority": 4 + } + ] +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/config/config.py b/backend/script_groups/EmailCrono/config/config.py new file mode 100644 index 0000000..162a8b1 --- /dev/null +++ b/backend/script_groups/EmailCrono/config/config.py @@ -0,0 +1,44 @@ +# config/config.py +import json +import os + +class Config: + def __init__(self, config_file='config.json'): + self.config_file = config_file + self.config = self._load_config() + + def _load_config(self): + if not os.path.exists(self.config_file): + default_config = { + 'input_dir': '.', + 'output_dir': '.', + 'cronologia_file': 'cronologia.md', + 'attachments_dir': 'adjuntos' + } + self._save_config(default_config) + return default_config + + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def _save_config(self, config): + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4) + + def get_input_dir(self): + return self.config.get('input_dir', '.') + + def get_output_dir(self): + return self.config.get('output_dir', '.') + + def get_cronologia_file(self): + return os.path.join( + self.get_output_dir(), + self.config.get('cronologia_file', 'cronologia.md') + ) + + def get_attachments_dir(self): + return os.path.join( + self.get_output_dir(), + self.config.get('attachments_dir', 'adjuntos') + ) \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/data.json b/backend/script_groups/EmailCrono/data.json new file mode 100644 index 0000000..7cac4f7 --- /dev/null +++ b/backend/script_groups/EmailCrono/data.json @@ -0,0 +1,5 @@ +{ + "attachments_dir": "adjuntos", + "cronologia_file": "cronologia.md", + "output_dir": "C:\\\\Users\\\\migue\\\\OneDrive\\\\Miguel\\\\Obsidean\\\\Trabajo\\\\VM\\\\04-InLavoro\\\\HENKEL\\\\93040 - HENKEL - BowlingGreen\\\\Description\\\\HENKEL - ALPLA - AUTEFA - Batch Data" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/description.json b/backend/script_groups/EmailCrono/description.json new file mode 100644 index 0000000..1160d57 --- /dev/null +++ b/backend/script_groups/EmailCrono/description.json @@ -0,0 +1,6 @@ +{ + "name": "Desempaquetado de Emails EML", + "description": "This script processes email files (.eml) into a chronological narrative in Markdown format, optimized for processing with Large Language Models (LLMs). It extracts essential information from emails while removing unnecessary metadata, creating a clean, temporal narrative that can be easily analyzed. ", + "version": "1.0", + "author": "Unknown" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/esquema_group.json b/backend/script_groups/EmailCrono/esquema_group.json new file mode 100644 index 0000000..c6a5779 --- /dev/null +++ b/backend/script_groups/EmailCrono/esquema_group.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "attachments_dir": { + "type": "string", + "title": "Directorio de adjuntos", + "description": "adjuntos" + }, + "cronologia_file": { + "type": "string", + "title": "Nombre del archivo de cronologia.md", + "description": "" + } + } +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/esquema_work.json b/backend/script_groups/EmailCrono/esquema_work.json new file mode 100644 index 0000000..76dd02a --- /dev/null +++ b/backend/script_groups/EmailCrono/esquema_work.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "output_dir": { + "type": "string", + "title": "Directorio de destino de la cronologia", + "description": "" + } + } +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/models/__pycache__/mensaje_email.cpython-310.pyc b/backend/script_groups/EmailCrono/models/__pycache__/mensaje_email.cpython-310.pyc new file mode 100644 index 0000000..ddc2ddf Binary files /dev/null and b/backend/script_groups/EmailCrono/models/__pycache__/mensaje_email.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/models/mensaje_email.py b/backend/script_groups/EmailCrono/models/mensaje_email.py new file mode 100644 index 0000000..b170920 --- /dev/null +++ b/backend/script_groups/EmailCrono/models/mensaje_email.py @@ -0,0 +1,145 @@ +# models/mensaje_email.py +import re +import hashlib +from datetime import datetime +from email.utils import parseaddr, parsedate_to_datetime + +class MensajeEmail: + def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None): + self.remitente = self._estandarizar_remitente(remitente) + self.fecha = self._estandarizar_fecha(fecha) + self.subject = subject if subject else 'Sin Asunto' + self.contenido = self._limpiar_contenido(contenido) + self.adjuntos = adjuntos if adjuntos else [] + self.hash = self._generar_hash() + + def _formatear_subject_para_link(self, subject): + """ + Formatea el subject para usarlo como ancla en links de Obsidian + Remueve caracteres especiales y espacios múltiples + """ + if not subject: + return "Sin-Asunto" + # Eliminar caracteres especiales y reemplazar espacios con guiones + formatted = re.sub(r'[^\w\s-]', '', subject) + formatted = re.sub(r'\s+', '-', formatted.strip()) + return formatted + + def _limpiar_contenido(self, contenido): + if not contenido: + return "" + + # Eliminar líneas de metadatos + lines = contenido.split('\n') + cleaned_lines = [] + + for line in lines: + # Skip metadata lines + if line.strip().startswith(('Da: ', 'Inviato: ', 'A: ', 'From: ', 'Sent: ', 'To: ')) or line.strip().startswith('Oggetto: '): + continue + # Limpiar espacios múltiples dentro de cada línea, pero mantener la línea completa + cleaned_line = re.sub(r' +', ' ', line) + cleaned_lines.append(cleaned_line) + + # Unir las líneas preservando los saltos de línea + text = '\n'.join(cleaned_lines) + + # Limpiar la combinación específica de CRLF+NBSP+CRLF + text = re.sub(r'\r?\n\xa0\r?\n', '\n', text) + + # Reemplazar CRLF por LF + text = text.replace('\r\n', '\n') + + # Reemplazar CR por LF + text = text.replace('\r', '\n') + + # Reemplazar 3 o más saltos de línea por dos + text = re.sub(r'\n{3,}', '\n\n', text) + + # Eliminar espacios al inicio y final del texto completo + return text.strip() + + def to_markdown(self): + # Hash con caracteres no título + hash_line = f"+ {self.hash}\n\n" + + # Subject como título + subject_line = f"### {self.subject if self.subject else 'Sin Asunto'}\n\n" + + # Fecha en formato legible + fecha_formato = self.fecha.strftime('%d-%m-%Y') + fecha_line = f"- {fecha_formato}\n\n" + + # Contenido del mensaje + md = f"{hash_line}{subject_line}{fecha_line}" + md += self.contenido + "\n\n" + + # Adjuntos si existen + if self.adjuntos: + md += "### Adjuntos\n" + for adj in self.adjuntos: + md += f"- [[{adj}]]\n" + md += "---\n\n" + return md + + def get_index_entry(self): + """ + Genera una entrada de lista para el índice + """ + fecha_formato = self.fecha.strftime('%d-%m-%Y') + subject_link = self._formatear_subject_para_link(self.subject) + return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]" + + def _estandarizar_remitente(self, remitente): + if 'Da:' in remitente: + remitente = remitente.split('Da:')[1].split('Inviato:')[0] + elif 'From:' in remitente: + remitente = remitente.split('From:')[1].split('Sent:')[0] + + nombre, email = parseaddr(remitente) + if not nombre and email: + nombre = email.split('@')[0] + elif not nombre and not email: + nombre_match = re.search(r'([A-Za-z\s]+)\s*<', remitente) + if nombre_match: + nombre = nombre_match.group(1) + else: + return "Remitente Desconocido" + + nombre = re.sub(r'[<>:"/\\|?*]', '', nombre.strip()) + nombre = nombre.encode('ascii', 'ignore').decode('ascii') + return nombre + + def _estandarizar_fecha(self, fecha): + if isinstance(fecha, str): + try: + return parsedate_to_datetime(fecha) + except: + return datetime.now() + return fecha + + def _generar_hash(self): + """ + Genera un hash único para el mensaje basado en una combinación de campos + que identifican únicamente el mensaje + """ + # Limpiar y normalizar el contenido para el hash + # Para el hash, sí normalizamos completamente los espacios + contenido_hash = re.sub(r'\s+', ' ', self.contenido).strip() + + # Normalizar el subject + subject_normalizado = re.sub(r'\s+', ' ', self.subject if self.subject else '').strip() + + # Crear una cadena con los elementos clave del mensaje + elementos_hash = [ + self.remitente.strip(), + self.fecha.strftime('%Y%m%d%H%M'), # Solo hasta minutos para permitir pequeñas variaciones + subject_normalizado, + contenido_hash[:500] # Usar solo los primeros 500 caracteres del contenido normalizado + ] + + # Unir todos los elementos con un separador único + texto_hash = '|'.join(elementos_hash) + + # Generar el hash + return hashlib.md5(texto_hash.encode()).hexdigest() \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/attachment_handler.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/attachment_handler.cpython-310.pyc new file mode 100644 index 0000000..a926e51 Binary files /dev/null and b/backend/script_groups/EmailCrono/utils/__pycache__/attachment_handler.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/beautify.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/beautify.cpython-310.pyc new file mode 100644 index 0000000..c12d078 Binary files /dev/null and b/backend/script_groups/EmailCrono/utils/__pycache__/beautify.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/email_parser.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/email_parser.cpython-310.pyc new file mode 100644 index 0000000..b36b5f2 Binary files /dev/null and b/backend/script_groups/EmailCrono/utils/__pycache__/email_parser.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/forward_handler.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/forward_handler.cpython-310.pyc new file mode 100644 index 0000000..3892e80 Binary files /dev/null and b/backend/script_groups/EmailCrono/utils/__pycache__/forward_handler.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/utils/__pycache__/markdown_handler.cpython-310.pyc b/backend/script_groups/EmailCrono/utils/__pycache__/markdown_handler.cpython-310.pyc new file mode 100644 index 0000000..3f80d80 Binary files /dev/null and b/backend/script_groups/EmailCrono/utils/__pycache__/markdown_handler.cpython-310.pyc differ diff --git a/backend/script_groups/EmailCrono/utils/attachment_handler.py b/backend/script_groups/EmailCrono/utils/attachment_handler.py new file mode 100644 index 0000000..80679cb --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/attachment_handler.py @@ -0,0 +1,33 @@ +# utils/attachment_handler.py +import os +import hashlib +import re + +def guardar_adjunto(parte, dir_adjuntos): + nombre = parte.get_filename() + if not nombre: + return None + + nombre = re.sub(r'[<>:"/\\|?*]', '_', nombre) + ruta = os.path.join(dir_adjuntos, nombre) + + if os.path.exists(ruta): + contenido_nuevo = parte.get_payload(decode=True) + hash_nuevo = hashlib.md5(contenido_nuevo).hexdigest() + + with open(ruta, 'rb') as f: + hash_existente = hashlib.md5(f.read()).hexdigest() + + if hash_nuevo == hash_existente: + return ruta + + base, ext = os.path.splitext(nombre) + i = 1 + while os.path.exists(ruta): + ruta = os.path.join(dir_adjuntos, f"{base}_{i}{ext}") + i += 1 + + with open(ruta, 'wb') as f: + f.write(parte.get_payload(decode=True)) + + return ruta diff --git a/backend/script_groups/EmailCrono/utils/beautify.py b/backend/script_groups/EmailCrono/utils/beautify.py new file mode 100644 index 0000000..fb08ec3 --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/beautify.py @@ -0,0 +1,225 @@ +import json +import re +from pathlib import Path +from collections import defaultdict +from enum import Enum + +class PatternType(Enum): + REGEX = "regex" + STRING = "string" + LEFT = "left" + RIGHT = "right" + SUBSTRING = "substring" + +class BeautifyProcessor: + def __init__(self, rules_file): + self.rules_by_priority = self._load_rules(rules_file) + + def _load_rules(self, rules_file): + rules_by_priority = defaultdict(list) + + try: + with open(rules_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + if not isinstance(data, dict) or 'rules' not in data: + raise ValueError("El archivo JSON debe contener un objeto con una clave 'rules'") + + for rule in data['rules']: + try: + pattern = rule['pattern'] + replacement = rule['replacement'] + action = rule['action'] + pattern_type = PatternType(rule.get('type', 'string')) + priority = int(rule.get('priority', 999)) + + # Para remove_block, convertir el patrón con ..... a una regex + if action == "remove_block": + pattern = self._convert_block_pattern_to_regex(pattern) + pattern_type = PatternType.REGEX + elif pattern_type == PatternType.REGEX: + pattern = re.compile(pattern) + + rules_by_priority[priority].append((pattern, replacement, action, pattern_type)) + + except KeyError as e: + print(f"Error en regla: falta campo requerido {e}") + continue + except ValueError as e: + print(f"Error en regla: tipo de patrón inválido {rule.get('type')}") + continue + except Exception as e: + print(f"Error procesando regla: {e}") + continue + + except json.JSONDecodeError as e: + print(f"Error decodificando JSON: {e}") + except Exception as e: + print(f"Error cargando reglas: {e}") + + return rules_by_priority + + def _convert_block_pattern_to_regex(self, pattern): + """ + Convierte un patrón de bloque con ..... en una expresión regular. + Primero maneja el comodín ..... y luego escapa el resto de caracteres especiales. + """ + # Reemplazar temporalmente los ..... con un marcador único + marker = "__BLOCK_MARKER__" + pattern = pattern.replace(".....", marker) + + # Escapar caracteres especiales + pattern = re.escape(pattern) + + # Restaurar el marcador con el patrón .*? + pattern = pattern.replace(marker, ".*?") + + return re.compile(f'(?s){pattern}') + + def _process_remove_block(self, text, pattern): + result = text + matches = list(pattern.finditer(result)) + + for match in reversed(matches): + start, end = match.span() + + line_start = result.rfind('\n', 0, start) + 1 + if line_start == 0: + line_start = 0 + + line_end = result.find('\n', end) + if line_end == -1: + line_end = len(result) + else: + line_end += 1 + + while line_start > 0 and result[line_start-1:line_start] == '\n' and \ + (line_start == 1 or result[line_start-2:line_start-1] == '\n'): + line_start -= 1 + + while line_end < len(result) and result[line_end-1:line_end] == '\n' and \ + (line_end == len(result)-1 or result[line_end:line_end+1] == '\n'): + line_end += 1 + + result = result[:line_start] + result[line_end:] + + return result + + def _line_matches(self, line, pattern, pattern_type): + line = line.strip() + if pattern_type == PatternType.REGEX: + return bool(pattern.search(line)) + elif pattern_type == PatternType.LEFT: + return line.startswith(pattern) + elif pattern_type == PatternType.RIGHT: + return line.endswith(pattern) + elif pattern_type == PatternType.SUBSTRING: + return pattern in line + elif pattern_type == PatternType.STRING: + return line == pattern + return False + + def _apply_replace(self, text, pattern, replacement, pattern_type): + if pattern_type == PatternType.REGEX: + return pattern.sub(replacement, text) + elif pattern_type == PatternType.STRING: + return text.replace(pattern, replacement) + elif pattern_type == PatternType.SUBSTRING: + return text.replace(pattern, replacement) + elif pattern_type == PatternType.LEFT: + lines = text.splitlines() + result_lines = [] + for line in lines: + if line.strip().startswith(pattern): + result_lines.append(line.replace(pattern, replacement, 1)) + else: + result_lines.append(line) + return '\n'.join(result_lines) + elif pattern_type == PatternType.RIGHT: + lines = text.splitlines() + result_lines = [] + for line in lines: + if line.strip().endswith(pattern): + result_lines.append(line[:line.rindex(pattern)] + replacement + line[line.rindex(pattern) + len(pattern):]) + else: + result_lines.append(line) + return '\n'.join(result_lines) + return text + + def process_text(self, text): + if not text: + return text + + result = text + for priority in sorted(self.rules_by_priority.keys()): + rules = self.rules_by_priority[priority] + print(f"Aplicando reglas de prioridad {priority}") + + for pattern, replacement, action, pattern_type in rules: + try: + if action == "remove_block": + result = self._process_remove_block(result, pattern) + elif action == "replace": + result = self._apply_replace(result, pattern, replacement, pattern_type) + elif action == "remove_line": + result = self._process_remove_line(result, pattern, pattern_type) + elif action in ["add_before", "add_after"]: + result = self._process_line_additions(result, pattern, replacement, action, pattern_type) + except Exception as e: + print(f"Error aplicando regla {pattern}: {e}") + continue + + return result + + def process_file(self, input_file, output_file=None): + try: + with open(input_file, 'r', encoding='utf-8') as f: + content = f.read() + + processed_content = self.process_text(content) + + output = output_file or input_file + with open(output, 'w', encoding='utf-8') as f: + f.write(processed_content) + + except Exception as e: + print(f"Error procesando archivo {input_file}: {e}") + + def _process_remove_line(self, text, pattern, pattern_type): + lines = text.splitlines() + result_lines = [] + skip_next_empty = False + + for i, line in enumerate(lines): + should_remove = self._line_matches(line, pattern, pattern_type) + + if should_remove: + if i < len(lines) - 1 and not lines[i + 1].strip(): + skip_next_empty = True + continue + + if skip_next_empty and not line.strip(): + skip_next_empty = False + continue + + result_lines.append(line) + skip_next_empty = False + + return '\n'.join(result_lines) + + def _process_line_additions(self, text, pattern, replacement, action, pattern_type): + lines = text.splitlines() + result_lines = [] + + for line in lines: + if self._line_matches(line, pattern, pattern_type): + if action == "add_before": + result_lines.append(replacement) + result_lines.append(line) + else: # add_after + result_lines.append(line) + result_lines.append(replacement) + else: + result_lines.append(line) + + return '\n'.join(result_lines) \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/utils/email_parser.py b/backend/script_groups/EmailCrono/utils/email_parser.py new file mode 100644 index 0000000..6ba70e9 --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/email_parser.py @@ -0,0 +1,295 @@ +# utils/email_parser.py +import email +from email import policy +from email.parser import BytesParser +from datetime import datetime +import re +from pathlib import Path +from bs4 import BeautifulSoup +from email.utils import parsedate_to_datetime +from models.mensaje_email import MensajeEmail +from utils.attachment_handler import guardar_adjunto +import tempfile +import os + +def _get_payload_safely(parte): + """ + Obtiene el payload de una parte del email de forma segura + """ + try: + if parte.is_multipart(): + return None + payload = parte.get_payload(decode=True) + if payload is None: + return None + charset = parte.get_content_charset() or 'utf-8' + return payload.decode(charset, errors='ignore') + except Exception as e: + print(f"Error getting payload: {str(e)}") + return None + +def _extract_subject_from_text(text): + """ + Extrae el asunto de un texto dados diferentes formatos de cabecera + """ + subject_headers = { + 'Oggetto: ': 9, # Italian + 'Subject: ': 9, # English + 'Asunto: ': 8, # Spanish + 'Sujet: ': 7, # French + 'Betreff: ': 9 # German + } + + for line in text.split('\n'): + line = line.strip() + for header, offset in subject_headers.items(): + if line.startswith(header): + return line[offset:].strip() + return None + +def _should_skip_line(line): + """ + Determina si una línea debe ser omitida por ser una cabecera de email + """ + headers_to_skip = [ + 'Da: ', 'Inviato: ', 'A: ', # Italian + 'From: ', 'Sent: ', 'To: ', # English + 'De: ', 'Enviado: ', 'Para: ', # Spanish + 'Von: ', 'Gesendet: ', 'An: ', # German + 'De : ', 'Envoyé : ', 'À : ' # French + ] + return any(line.strip().startswith(header) for header in headers_to_skip) + +def _html_a_markdown(html): + """ + Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente + """ + if html is None: + return (None, "") + + try: + # Limpieza básica + html = html.replace('\xa0', ' ') # NBSP a espacio normal + html = html.replace('\r\n', '\n') # CRLF a LF + html = html.replace('\r', '\n') # CR a LF + + soup = BeautifulSoup(html, 'html.parser') + + # Procesar tablas + for table in soup.find_all('table'): + try: + rows = table.find_all('tr') + if not rows: + continue + + markdown_table = [] + max_widths = [] + + # Calcular anchos máximos + for row in rows: + cells = row.find_all(['th', 'td']) + while len(max_widths) < len(cells): + max_widths.append(0) + for i, cell in enumerate(cells): + cell_text = cell.get_text().strip() + max_widths[i] = max(max_widths[i], len(cell_text)) + + # Construir tabla markdown + if max_widths: # Solo si tenemos celdas válidas + header_row = rows[0].find_all(['th', 'td']) + header = '| ' + ' | '.join(cell.get_text().strip().ljust(max_widths[i]) + for i, cell in enumerate(header_row)) + ' |' + separator = '|' + '|'.join('-' * (width + 2) for width in max_widths) + '|' + + markdown_table.append(header) + markdown_table.append(separator) + + for row in rows[1:]: + cells = row.find_all(['td', 'th']) + row_text = '| ' + ' | '.join(cell.get_text().strip().ljust(max_widths[i]) + for i, cell in enumerate(cells)) + ' |' + markdown_table.append(row_text) + + table.replace_with(soup.new_string('\n' + '\n'.join(markdown_table))) + except Exception as e: + print(f"Error procesando tabla: {str(e)}") + continue + + # Procesar saltos de línea + for br in soup.find_all('br'): + br.replace_with('\n') + + # Obtener texto limpio + text = soup.get_text() + + # Procesar líneas + cleaned_lines = [] + subject = None + + for line in text.split('\n'): + if not subject: + subject = _extract_subject_from_text(line) + + if not _should_skip_line(line): + cleaned_lines.append(line) + + final_text = '\n'.join(cleaned_lines).strip() + return (subject, final_text) + + except Exception as e: + print(f"Error en html_a_markdown: {str(e)}") + return (None, html if html else "") + +def _procesar_email_adjunto(parte, dir_adjuntos): + """ + Procesa un email que viene como adjunto dentro de otro email. + """ + try: + mensajes = [] + if parte.is_multipart(): + # Si es multipart, procesar cada subparte + for subparte in parte.walk(): + if subparte.get_content_type() == "message/rfc822": + # Si es un mensaje RFC822, obtener el payload como lista + payload = subparte.get_payload() + if isinstance(payload, list): + for msg in payload: + mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) + elif isinstance(payload, email.message.Message): + mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) + else: + # Si no es multipart, intentar procesar como mensaje único + payload = parte.get_payload() + if isinstance(payload, list): + for msg in payload: + mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) + elif isinstance(payload, email.message.Message): + mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) + + return mensajes + except Exception as e: + print(f"Error procesando email adjunto: {str(e)}") + return [] + +def procesar_eml(ruta_archivo, dir_adjuntos): + """ + Punto de entrada principal para procesar archivos .eml + """ + try: + with open(ruta_archivo, 'rb') as eml: + mensaje = BytesParser(policy=policy.default).parse(eml) + return procesar_eml_interno(mensaje, dir_adjuntos) + except Exception as e: + print(f"Error al abrir el archivo {ruta_archivo}: {str(e)}") + return [] + +def procesar_eml_interno(mensaje, dir_adjuntos): + """ + Procesa un mensaje de email, ya sea desde archivo o adjunto + """ + mensajes = [] + + try: + remitente = mensaje.get('from', '') + fecha_str = mensaje.get('date', '') + fecha = _parsear_fecha(fecha_str) + + # Get subject from email headers first + subject = mensaje.get('subject', '') + if subject: + # Try to decode if it's encoded + subject = str(email.header.make_header(email.header.decode_header(subject))) + + contenido = "" + adjuntos = [] + tiene_html = False + + # First pass: check for HTML content + if mensaje.is_multipart(): + for parte in mensaje.walk(): + if parte.get_content_type() == "text/html": + tiene_html = True + break + else: + tiene_html = mensaje.get_content_type() == "text/html" + + # Second pass: process content and attachments + if mensaje.is_multipart(): + for parte in mensaje.walk(): + content_type = parte.get_content_type() + + try: + if content_type == "text/html": + html_content = _get_payload_safely(parte) + if html_content: + part_subject, text = _html_a_markdown(html_content) + if not subject and part_subject: + subject = part_subject + if text: + contenido = text + elif content_type == "text/plain" and not tiene_html: + text = _get_payload_safely(parte) + if text: + contenido = text + elif content_type == "message/rfc822": + # Procesar email adjunto + mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes.extend(mensajes_adjuntos) + elif parte.get_content_disposition() == 'attachment': + nombre = parte.get_filename() + if nombre and nombre.lower().endswith('.eml'): + # Si es un archivo .eml adjunto + mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos) + mensajes.extend(mensajes_adjuntos) + else: + # Otros tipos de adjuntos + ruta_adjunto = guardar_adjunto(parte, dir_adjuntos) + if ruta_adjunto: + adjuntos.append(Path(ruta_adjunto).name) + except Exception as e: + print(f"Error procesando parte del mensaje: {str(e)}") + continue + else: + if mensaje.get_content_type() == "text/html": + html_content = _get_payload_safely(mensaje) + if html_content: + part_subject, contenido = _html_a_markdown(html_content) + if not subject and part_subject: + subject = part_subject + else: + contenido = _get_payload_safely(mensaje) or "" + + # Solo agregar el mensaje si tiene contenido útil + if contenido or subject or adjuntos: + mensajes.append(MensajeEmail( + remitente=remitente, + fecha=fecha, + contenido=contenido, + subject=subject, + adjuntos=adjuntos + )) + + except Exception as e: + print(f"Error procesando mensaje: {str(e)}") + + return mensajes + +def _parsear_fecha(fecha_str): + try: + fecha = parsedate_to_datetime(fecha_str) + return fecha.replace(tzinfo=None) # Remove timezone info + except: + try: + fecha_match = re.search(r'venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})', fecha_str) + if fecha_match: + dia, mes, año, hora, minuto = fecha_match.groups() + meses_it = { + 'gennaio': 1, 'febbraio': 2, 'marzo': 3, 'aprile': 4, + 'maggio': 5, 'giugno': 6, 'luglio': 7, 'agosto': 8, + 'settembre': 9, 'ottobre': 10, 'novembre': 11, 'dicembre': 12 + } + mes_num = meses_it.get(mes.lower(), 1) + return datetime(int(año), mes_num, int(dia), int(hora), int(minuto)) + except: + pass + return datetime.now() \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/utils/markdown_handler.py b/backend/script_groups/EmailCrono/utils/markdown_handler.py new file mode 100644 index 0000000..2991e3d --- /dev/null +++ b/backend/script_groups/EmailCrono/utils/markdown_handler.py @@ -0,0 +1,39 @@ +# utils/markdown_handler.py +import os +import re +from datetime import datetime +from models.mensaje_email import MensajeEmail + +def cargar_cronologia_existente(archivo): + mensajes = [] + if not os.path.exists(archivo): + return mensajes + + with open(archivo, 'r', encoding='utf-8') as f: + contenido = f.read() + + bloques = contenido.split('---\n\n') + for bloque in bloques: + if not bloque.strip(): + continue + + match = re.match(r'## (\d{14})\|(.*?)\n\n(.*)', bloque.strip(), re.DOTALL) + if match: + fecha_str, remitente, contenido = match.groups() + fecha = datetime.strptime(fecha_str, '%Y%m%d%H%M%S') + + adjuntos = [] + if '### Adjuntos' in contenido: + contenido_principal, lista_adjuntos = contenido.split('### Adjuntos') + adjuntos = [adj.strip()[2:-2] for adj in lista_adjuntos.strip().split('\n')] + contenido = contenido_principal.strip() + + mensajes.append(MensajeEmail( + remitente=remitente, + fecha=fecha, + contenido=contenido, + adjuntos=adjuntos + )) + + return mensajes + diff --git a/backend/script_groups/EmailCrono/work_dir.json b/backend/script_groups/EmailCrono/work_dir.json new file mode 100644 index 0000000..544f154 --- /dev/null +++ b/backend/script_groups/EmailCrono/work_dir.json @@ -0,0 +1,3 @@ +{ + "path": "C:/Trabajo/VM/40 - 93040 - HENKEL - NEXT2 Problem/Reporte/Emails" +} \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/x1.py b/backend/script_groups/EmailCrono/x1.py new file mode 100644 index 0000000..73e2900 --- /dev/null +++ b/backend/script_groups/EmailCrono/x1.py @@ -0,0 +1,101 @@ +""" +Script para dessasemblar los emails y generar un archivo de texto con la cronología de los mensajes. +""" + +# main.py +import os +from pathlib import Path +from utils.email_parser import procesar_eml +from utils.markdown_handler import cargar_cronologia_existente +from utils.beautify import BeautifyProcessor +from config.config import Config +import hashlib + +def generar_indice(mensajes): + """ + Genera una lista de mensajes usando el formato de Obsidian + """ + indice = "# Índice de Mensajes\n\n" + + for mensaje in mensajes: + indice += mensaje.get_index_entry() + "\n" + + indice += "\n---\n\n" + return indice + +def main(): + config = Config() + + # Debug prints + print(f"Input directory: {config.get_input_dir()}") + print(f"Output directory: {config.get_output_dir()}") + print(f"Cronologia file: {config.get_cronologia_file()}") + print(f"Attachments directory: {config.get_attachments_dir()}") + + # Obtener el directorio donde está el script actual + script_dir = os.path.dirname(os.path.abspath(__file__)) + # Construir la ruta al archivo de reglas en el subdirectorio config + beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json") + beautifier = BeautifyProcessor(beautify_rules) + print(f"Beautify rules file: {beautify_rules}") + + # Ensure directories exist + os.makedirs(config.get_output_dir(), exist_ok=True) + os.makedirs(config.get_attachments_dir(), exist_ok=True) + + # Check if input directory exists and has files + input_path = Path(config.get_input_dir()) + if not input_path.exists(): + print(f"Error: Input directory {input_path} does not exist") + return + + eml_files = list(input_path.glob('*.eml')) + print(f"Found {len(eml_files)} .eml files") + + mensajes = [] + print(f"Loaded {len(mensajes)} existing messages") + mensajes_hash = {msg.hash for msg in mensajes} + + total_procesados = 0 + total_nuevos = 0 + mensajes_duplicados = 0 + + for archivo in eml_files: + print(f"\nProcessing {archivo}") + nuevos_mensajes = procesar_eml(archivo, config.get_attachments_dir()) + total_procesados += len(nuevos_mensajes) + + # Verificar duplicados y aplicar beautify solo a los mensajes nuevos + for msg in nuevos_mensajes: + if msg.hash not in mensajes_hash: + # Aplicar beautify solo si el mensaje es nuevo + msg.contenido = beautifier.process_text(msg.contenido) + mensajes.append(msg) + mensajes_hash.add(msg.hash) + total_nuevos += 1 + else: + mensajes_duplicados += 1 + + print(f"\nEstadísticas de procesamiento:") + print(f"- Total mensajes encontrados: {total_procesados}") + print(f"- Mensajes únicos añadidos: {total_nuevos}") + print(f"- Mensajes duplicados ignorados: {mensajes_duplicados}") + + # Ordenar mensajes de más reciente a más antiguo + mensajes.sort(key=lambda x: x.fecha, reverse=True) + + # Generar el índice + indice = generar_indice(mensajes) + + # Escribir el archivo con el índice y los mensajes + output_file = config.get_cronologia_file() + print(f"\nWriting {len(mensajes)} messages to {output_file}") + with open(output_file, 'w', encoding='utf-8') as f: + # Primero escribir el índice + f.write(indice) + # Luego escribir todos los mensajes + for msg in mensajes: + f.write(msg.to_markdown()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/commands.sh b/commands.sh deleted file mode 100644 index 7c17e14..0000000 --- a/commands.sh +++ /dev/null @@ -1,3 +0,0 @@ -# Crear .gitkeep en cada directorio de script grupo -mkdir -p backend/script_groups/example_group -touch backend/script_groups/example_group/.gitkeep diff --git a/config_manager.py b/config_manager.py index 8a943de..ab5fcc0 100644 --- a/config_manager.py +++ b/config_manager.py @@ -122,22 +122,13 @@ class ConfigurationManager: # Determine schema path based on level if level == "1": - path = os.path.join(self.data_path, "esquema.json") - # Try esquema.json first, then schema.json if not found - if not os.path.exists(path): - path = os.path.join(self.data_path, "schema.json") + path = os.path.join(self.data_path, "esquema_general.json") elif level == "2": - path = os.path.join(self.script_groups_path, group, "esquema.json") - # Try esquema.json first, then schema.json if not found - if not os.path.exists(path): - path = os.path.join(self.script_groups_path, group, "schema.json") + path = os.path.join(self.script_groups_path, group, "esquema_group.json") elif level == "3": if not group: return {"type": "object", "properties": {}} - path = os.path.join(self.script_groups_path, group, "esquema.json") - # Try esquema.json first, then schema.json if not found - if not os.path.exists(path): - path = os.path.join(self.script_groups_path, group, "schema.json") + path = os.path.join(self.script_groups_path, group, "esquema_work.json") else: return {"type": "object", "properties": {}} @@ -169,11 +160,11 @@ class ConfigurationManager: try: # Determinar rutas de schema y config if level == "1": - schema_path = os.path.join(self.data_path, "esquema.json") + schema_path = os.path.join(self.data_path, "esquema_general.json") config_path = os.path.join(self.data_path, "data.json") elif level == "2": schema_path = os.path.join( - self.script_groups_path, group, "esquema.json" + self.script_groups_path, group, "esquema_group.json" ) config_path = os.path.join(self.script_groups_path, group, "data.json") elif level == "3": @@ -183,7 +174,7 @@ class ConfigurationManager: "message": "Group is required for level 3", } schema_path = os.path.join( - self.script_groups_path, group, "esquema.json" + self.script_groups_path, group, "esquema_work.json" ) config_path = ( os.path.join(self.working_directory, "data.json") diff --git a/data/esquema_general.json b/data/esquema_general.json new file mode 100644 index 0000000..1c9e43a --- /dev/null +++ b/data/esquema_general.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/excel/__init__.py b/services/excel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/language/__init__.py b/services/language/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/llm/__init__.py b/services/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/llm/__pycache__/base.cpython-310.pyc b/services/llm/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..e5aadd3 Binary files /dev/null and b/services/llm/__pycache__/base.cpython-310.pyc differ diff --git a/services/llm/__pycache__/grok_service.cpython-310.pyc b/services/llm/__pycache__/grok_service.cpython-310.pyc new file mode 100644 index 0000000..e12073b Binary files /dev/null and b/services/llm/__pycache__/grok_service.cpython-310.pyc differ diff --git a/services/llm/__pycache__/llm_factory.cpython-310.pyc b/services/llm/__pycache__/llm_factory.cpython-310.pyc new file mode 100644 index 0000000..8f8a6fb Binary files /dev/null and b/services/llm/__pycache__/llm_factory.cpython-310.pyc differ diff --git a/services/llm/__pycache__/ollama_service.cpython-310.pyc b/services/llm/__pycache__/ollama_service.cpython-310.pyc new file mode 100644 index 0000000..e8106f6 Binary files /dev/null and b/services/llm/__pycache__/ollama_service.cpython-310.pyc differ diff --git a/services/llm/__pycache__/openai_service.cpython-310.pyc b/services/llm/__pycache__/openai_service.cpython-310.pyc new file mode 100644 index 0000000..e34a380 Binary files /dev/null and b/services/llm/__pycache__/openai_service.cpython-310.pyc differ diff --git a/services/translation/__init__.py b/services/translation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..0707538 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,104 @@ +.sidebar { + position: fixed; + top: 0; + right: -400px; + width: 400px; + height: 100vh; + background: white; + box-shadow: -2px 0 5px rgba(0,0,0,0.1); + transition: right 0.3s ease; + z-index: 40; + overflow-y: auto; +} + +.sidebar.open { + right: 0; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0,0,0,0.5); + display: none; + z-index: 30; +} + +.overlay.show { + display: block; +} + +#schema-editor { + z-index: 50; +} + +#schema-editor .modal-content { + z-index: 51; + width: 50% !important; + max-width: 800px; +} + +/* Reducción general de padding */ +.container { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; +} + +.p-6 { + padding: 1.1rem !important; +} + +.px-4 { + padding-left: 0.7rem !important; + padding-right: 0.7rem !important; +} + +.py-8 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +/* Ajustes para el log */ +#log-area { + font-size: 0.7rem !important; + line-height: 1.2 !important; + padding: 0.7rem !important; +} + +/* Ajustes para márgenes */ +.mb-4 { + margin-bottom: 0.7rem !important; +} + +.mb-6 { + margin-bottom: 1.1rem !important; +} + +.mb-8 { + margin-bottom: 1.5rem !important; +} + +.mt-4 { + margin-top: 0.7rem !important; +} + +/* Estilos del modal */ +.modal-header { + position: sticky; + top: 0; + background: white; + z-index: 52; + padding: 1rem; + border-bottom: 1px solid #e5e7eb; +} + +.modal-footer { + position: sticky; + bottom: 0; + background: white; + z-index: 52; + padding: 1rem; + border-top: 1px solid #e5e7eb; +} diff --git a/static/js/scripts.js b/static/js/scripts.js new file mode 100644 index 0000000..b73a144 --- /dev/null +++ b/static/js/scripts.js @@ -0,0 +1,847 @@ +let socket; +let currentGroup; + +// Initialize WebSocket connection +function initWebSocket() { + socket = new WebSocket(`ws://${location.host}/ws`); + socket.onmessage = function(event) { + addLogLine(event.data); + }; + socket.onclose = function() { + console.log('WebSocket cerrado, intentando reconexión...'); + setTimeout(initWebSocket, 1000); + }; + socket.onerror = function(error) { + console.error('Error en WebSocket:', error); + }; +} + +// Load configurations for all levels +async function loadConfigs() { + const group = document.getElementById('script-group').value; + currentGroup = group; + console.log('Loading configs for group:', group); // Debug line + + try { + // Cargar niveles 1 y 2 + for (let level of [1, 2]) { + console.log(`Loading level ${level} config...`); // Debug line + const response = await fetch(`/api/config/${level}?group=${group}`); + const data = await response.json(); + console.log(`Level ${level} data:`, data); // Debug line + await renderForm(`level${level}-form`, data); + } + + // Cargar nivel 3 solo si hay directorio de trabajo + const workingDirResponse = await fetch(`/api/working-directory/${group}`); + const workingDirResult = await workingDirResponse.json(); + if (workingDirResult.status === 'success' && workingDirResult.path) { + console.log('Loading level 3 config...'); // Debug line + const response = await fetch(`/api/config/3?group=${group}`); + const data = await response.json(); + console.log('Level 3 data:', data); // Debug line + await renderForm('level3-form', data); + } + + await loadScripts(group); + } catch (error) { + console.error('Error loading configs:', error); + } +} + +// Load and display available scripts +async function loadScripts(group) { + const response = await fetch(`/api/scripts/${group}`); + const scripts = await response.json(); + const container = document.getElementById('scripts-list'); + container.innerHTML = scripts.map(script => ` +
No hay esquema definido para este nivel.
'; + return; + } + + container.innerHTML = ` + +Error cargando el esquema.
'; + } +} + +function generateFormFields(schema, data, prefix, level) { + console.log('Generating fields with data:', { schema, data, prefix, level }); // Debug line + let html = ''; + + if (!schema.properties) { + console.warn('Schema has no properties'); + return html; + } + + for (const [key, def] of Object.entries(schema.properties)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + const value = getValue(data, fullKey); + console.log(`Field ${fullKey}:`, { definition: def, value: value }); // Debug line + + html += `${def.description}
`; + } + html += '