Primera version funcionando

This commit is contained in:
Miguel 2025-02-08 23:38:04 +01:00
commit d92eeb8c75
13 changed files with 1238 additions and 0 deletions

Binary file not shown.

151
app.py Normal file
View File

@ -0,0 +1,151 @@
from flask import Flask, render_template, request, jsonify
from flask_sock import Sock
from config_manager import ConfigurationManager
import os
app = Flask(__name__)
sock = Sock(app)
config_manager = ConfigurationManager()
# Lista global para mantener las conexiones WebSocket activas
websocket_connections = set()
@sock.route("/ws")
def handle_websocket(ws):
try:
websocket_connections.add(ws)
while True:
message = ws.receive()
if message:
broadcast_message(message)
except Exception as e:
print(f"WebSocket error: {e}")
finally:
websocket_connections.remove(ws)
def broadcast_message(message):
"""Envía un mensaje a todas las conexiones WebSocket activas."""
dead_connections = set()
for ws in websocket_connections:
try:
ws.send(message)
except Exception:
dead_connections.add(ws)
# Limpiar conexiones muertas
websocket_connections.difference_update(dead_connections)
@app.route("/api/execute_script", methods=["POST"])
def execute_script():
try:
script_group = request.json["group"]
script_name = request.json["script"]
# Ejecutar el script y obtener resultado
result = config_manager.execute_script(
script_group, script_name, broadcast_message
)
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script: {str(e)}"
broadcast_message(error_msg)
return jsonify({"error": error_msg})
@app.route("/")
def index():
script_groups = config_manager.get_script_groups()
return render_template("index.html", script_groups=script_groups)
@app.route("/api/config/<level>", methods=["GET", "POST"])
def handle_config(level):
group = request.args.get("group")
if request.method == "GET":
try:
return jsonify(config_manager.get_config(level, group))
except FileNotFoundError:
return jsonify({})
else:
data = request.json
config_manager.update_config(level, data, group)
return jsonify({"status": "success"})
@app.route("/api/schema/<level>", methods=["GET", "POST"])
def handle_schema(level):
group = request.args.get("group")
if request.method == "GET":
return jsonify(config_manager.get_schema(level, group))
else:
data = request.json
config_manager.update_schema(level, data, group)
return jsonify({"status": "success"})
@app.route("/api/scripts/<group>")
def get_scripts(group):
return jsonify(config_manager.list_scripts(group))
@app.route("/api/working-directory", methods=["POST"])
def set_working_directory():
data = request.json
if not data:
return jsonify({"status": "error", "message": "No data provided"})
path = data.get("path")
group = data.get("group")
if not path or not group:
return jsonify(
{
"status": "error",
"message": f"Missing required fields. Path: {path}, Group: {group}",
}
)
print(f"Setting working directory - Path: {path}, Group: {group}") # Debug line
return jsonify(config_manager.set_work_dir(group, path))
@app.route("/api/working-directory/<group>", methods=["GET"])
def get_working_directory(group):
path = config_manager.get_work_dir(group)
return jsonify({"path": path, "status": "success" if path else "not_set"})
@app.route("/api/browse-directories")
def browse_directories():
import tkinter as tk
from tkinter import filedialog
# Obtener el directorio inicial
current_dir = request.args.get("current_path")
if not current_dir or not os.path.exists(current_dir):
current_dir = os.path.dirname(os.path.abspath(__file__))
# Crear y configurar la ventana principal de tkinter
root = tk.Tk()
root.attributes("-topmost", True) # Mantener la ventana siempre arriba
root.withdraw()
# Abrir el diálogo de selección de directorio
directory = filedialog.askdirectory(
initialdir=current_dir, title="Seleccionar Directorio de Trabajo"
)
# Destruir la ventana de tkinter
root.destroy()
if directory:
return jsonify({"status": "success", "path": directory})
return jsonify({"status": "cancelled"})
if __name__ == "__main__":
app.run(debug=True)

View File

@ -0,0 +1,20 @@
{
"properties": {
"batch_size": {
"description": "N\u00c3\u00bamero de elementos a procesar por lote",
"title": "Tama\u00c3\u00b1o de Lote",
"type": "number"
},
"input_dir": {
"description": "Ruta al directorio de archivos de entrada",
"title": "Directorio de Entrada",
"type": "string"
},
"output_dir": {
"description": "Ruta al directorio para archivos generados",
"title": "Directorio de Salida",
"type": "string"
}
},
"type": "object"
}

View File

@ -0,0 +1,5 @@
{
"input_dir": "D:/Datos/Entrada",
"output_dir": "D:/Datos/Salida",
"batch_size": 50
}

View File

@ -0,0 +1,21 @@
{
"type": "object",
"properties": {
"project_name": {
"type": "string",
"title": "Nombre del Proyecto",
"description": "Identificador único del proyecto"
},
"process_type": {
"type": "string",
"title": "Tipo de Proceso",
"enum": ["basic", "advanced", "full"],
"description": "Nivel de procesamiento a aplicar"
},
"debug_mode": {
"type": "boolean",
"title": "Modo Debug",
"description": "Activar logging detallado"
}
}
}

View File

@ -0,0 +1,3 @@
{
"path": "C:/Estudio"
}

View File

@ -0,0 +1,28 @@
"""
Script de prueba que imprime las configuraciones y realiza una tarea simple.
Este script demuestra cómo acceder a las configuraciones de los tres niveles.
"""
import json
import os
import time
def main():
# Cargar configuraciones desde variable de entorno
configs = json.loads(os.environ.get('SCRIPT_CONFIGS', '{}'))
print("=== Ejecutando Script de Prueba 1 ===")
print("\nConfiguraciones cargadas:")
print("Nivel 1:", json.dumps(configs.get('level1', {}), indent=2))
print("Nivel 2:", json.dumps(configs.get('level2', {}), indent=2))
print("Nivel 3:", json.dumps(configs.get('level3', {}), indent=2))
print("\nSimulando procesamiento...")
for i in range(5):
time.sleep(1)
print(f"Progreso: {(i+1)*20}%")
print("\n¡Proceso completado!")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,36 @@
"""
Script de prueba que simula un proceso de análisis de datos.
Demuestra el manejo de errores y logging.
"""
import json
import os
import time
import random
def main():
configs = json.loads(os.environ.get('SCRIPT_CONFIGS', '{}'))
print("=== Ejecutando Script de Prueba 2 ===")
print("\nIniciando análisis de datos simulado...")
# Simular proceso con posible error
try:
for i in range(3):
time.sleep(1)
print(f"Analizando lote {i+1}...")
# Simular error aleatorio
if random.random() < 0.3:
raise Exception("Error simulado en el procesamiento")
print(f"Lote {i+1} completado exitosamente")
print("\n¡Análisis completado sin errores!")
except Exception as e:
print(f"\nERROR: {str(e)}")
print("El proceso se detuvo debido a un error")
if __name__ == '__main__':
main()

171
claude_file_organizer.py Normal file
View File

@ -0,0 +1,171 @@
import os
import shutil
from pathlib import Path
import re
class ClaudeProjectOrganizer:
def __init__(self):
self.source_dir = Path.cwd()
self.claude_dir = self.source_dir / 'claude'
self.file_mapping = {}
def should_skip_directory(self, dir_name):
skip_dirs = {'.git', '__pycache__', 'venv', 'env', '.pytest_cache', '.vscode', 'claude'}
return dir_name in skip_dirs
def get_comment_prefix(self, file_extension):
"""Determina el prefijo de comentario según la extensión del archivo"""
comment_styles = {
'.py': '#',
'.js': '//',
'.css': '/*',
'.html': '<!--',
'.scss': '//',
'.less': '//',
'.tsx': '//',
'.ts': '//',
'.jsx': '//',
}
return comment_styles.get(file_extension.lower(), None)
def get_comment_suffix(self, file_extension):
"""Determina el sufijo de comentario si es necesario"""
comment_suffixes = {
'.css': ' */',
'.html': ' -->',
}
return comment_suffixes.get(file_extension.lower(), '')
def normalize_path(self, path_str: str) -> str:
"""Normaliza la ruta usando forward slashes"""
return str(path_str).replace('\\', '/')
def check_existing_path_comment(self, content: str, normalized_path: str, comment_prefix: str) -> bool:
"""Verifica si ya existe un comentario con la ruta en el archivo"""
# Escapar caracteres especiales en el prefijo de comentario para regex
escaped_prefix = re.escape(comment_prefix)
# Crear patrones para buscar tanto forward como backward slashes
forward_pattern = f"{escaped_prefix}\\s*{re.escape(normalized_path)}\\b"
backward_path = normalized_path.replace('/', '\\\\') # Doble backslash para el patrón
backward_pattern = f"{escaped_prefix}\\s*{re.escape(backward_path)}"
# Buscar en las primeras líneas del archivo
first_lines = content.split('\n')[:5]
for line in first_lines:
if (re.search(forward_pattern, line) or
re.search(backward_pattern, line)):
return True
return False
def add_path_comment(self, file_path: Path, content: str) -> str:
"""Agrega un comentario con la ruta al inicio del archivo si no existe"""
relative_path = file_path.relative_to(self.source_dir)
normalized_path = self.normalize_path(relative_path)
comment_prefix = self.get_comment_prefix(file_path.suffix)
if comment_prefix is None:
return content
comment_suffix = self.get_comment_suffix(file_path.suffix)
# Verificar si ya existe el comentario
if self.check_existing_path_comment(content, normalized_path, comment_prefix):
print(f" - Comentario de ruta ya existe en {file_path}")
return content
path_comment = f"{comment_prefix} {normalized_path}{comment_suffix}\n"
# Para archivos HTML, insertar después del doctype si existe
if file_path.suffix.lower() == '.html':
if content.lower().startswith('<!doctype'):
doctype_end = content.find('>') + 1
return content[:doctype_end] + '\n' + path_comment + content[doctype_end:]
return path_comment + content
def clean_claude_directory(self):
if self.claude_dir.exists():
shutil.rmtree(self.claude_dir)
self.claude_dir.mkdir()
print(f"Directorio claude limpiado: {self.claude_dir}")
def copy_files(self):
self.clean_claude_directory()
for root, dirs, files in os.walk(self.source_dir):
dirs[:] = [d for d in dirs if not self.should_skip_directory(d)]
current_path = Path(root)
for file in files:
file_path = current_path / file
if file.endswith(('.py', '.js', '.css', '.html', '.json', '.yml', '.yaml',
'.tsx', '.ts', '.jsx', '.scss', '.less')):
target_path = self.claude_dir / file
# Si el archivo ya existe en el directorio claude, agregar un sufijo numérico
if target_path.exists():
base = target_path.stem
ext = target_path.suffix
counter = 1
while target_path.exists():
target_path = self.claude_dir / f"{base}_{counter}{ext}"
counter += 1
try:
# Leer el contenido del archivo
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Agregar el comentario con la ruta si no existe
modified_content = self.add_path_comment(file_path, content)
# Escribir el nuevo contenido
with open(target_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(modified_content)
self.file_mapping[str(file_path)] = target_path.name
print(f"Copiado: {file_path} -> {target_path}")
except UnicodeDecodeError:
print(f"Advertencia: No se pudo procesar {file_path} como texto. Copiando sin modificar...")
shutil.copy2(file_path, target_path)
except Exception as e:
print(f"Error procesando {file_path}: {str(e)}")
def generate_tree_report(self):
"""Genera el reporte en formato árbol visual"""
report = ["Estructura del proyecto original:\n"]
def add_to_report(path, prefix="", is_last=True):
report.append(prefix + ("└── " if is_last else "├── ") + path.name)
if path.is_dir() and not self.should_skip_directory(path.name):
children = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name))
children = [c for c in children if not (c.is_dir() and self.should_skip_directory(c.name))]
for i, child in enumerate(children):
is_last_child = i == len(children) - 1
new_prefix = prefix + (" " if is_last else "")
add_to_report(child, new_prefix, is_last_child)
add_to_report(self.source_dir)
report_path = self.claude_dir / "project_structure.txt"
with open(report_path, "w", encoding="utf-8") as f:
f.write("\n".join(report))
print(f"\nReporte generado en: {report_path}")
def main():
try:
print("Iniciando organización de archivos para Claude...")
organizer = ClaudeProjectOrganizer()
organizer.copy_files()
organizer.generate_tree_report()
print("\n¡Proceso completado exitosamente!")
except Exception as e:
print(f"\nError durante la ejecución: {str(e)}")
if __name__ == "__main__":
main()

261
config_manager.py Normal file
View File

@ -0,0 +1,261 @@
import os
import json
import subprocess
import re
from typing import Dict, Any, List
class ConfigurationManager:
def __init__(self):
self.base_path = os.path.dirname(os.path.abspath(__file__))
self.data_path = os.path.join(self.base_path, "data")
self.script_groups_path = os.path.join(
self.base_path, "backend", "script_groups"
)
self.working_directory = None
def set_working_directory(self, path: str) -> Dict[str, str]:
"""Set and validate working directory."""
if not os.path.exists(path):
return {"status": "error", "message": "Directory does not exist"}
self.working_directory = path
# Create default data.json if it doesn't exist
data_path = os.path.join(path, "data.json")
if not os.path.exists(data_path):
with open(data_path, "w") as f:
json.dump({}, f, indent=2)
return {"status": "success", "path": path}
def get_script_groups(self) -> List[str]:
"""Returns list of available script groups."""
return [
d
for d in os.listdir(self.script_groups_path)
if os.path.isdir(os.path.join(self.script_groups_path, d))
]
def get_config(self, level: str, group: str = None) -> Dict[str, Any]:
"""Get configuration for specified level."""
if level == "1":
path = os.path.join(self.data_path, "data.json")
elif level == "2":
path = os.path.join(self.script_groups_path, group, "data.json")
elif level == "3":
if not self.working_directory:
return {} # Return empty config if working directory not set
path = os.path.join(self.working_directory, "data.json")
try:
with open(path, "r") as f:
return json.load(f)
except FileNotFoundError:
return {} # Return empty config if file doesn't exist
def get_schema(self, level: str, group: str = None) -> Dict[str, Any]:
"""Get schema for specified level."""
# Clean level parameter (remove -form suffix if present)
level = level.split("-")[0]
try:
if level == "1":
path = os.path.join(self.data_path, "esquema.json")
elif level == "2":
path = os.path.join(self.script_groups_path, "esquema.json")
elif level == "3":
if not group:
return {} # Return empty schema if no group is specified
path = os.path.join(self.script_groups_path, group, "esquema.json")
else:
return {} # Return empty schema for invalid levels
with open(path, "r") as f:
return json.load(f)
except FileNotFoundError:
return {} # Return empty schema if file doesn't exist
except json.JSONDecodeError:
return {} # Return empty schema if file is invalid JSON
def update_config(
self, level: str, data: Dict[str, Any], group: str = None
) -> Dict[str, str]:
"""Update configuration for specified level."""
if level == "3" and not self.working_directory:
return {"status": "error", "message": "Working directory not set"}
if level == "1":
path = os.path.join(self.data_path, "data.json")
elif level == "2":
path = os.path.join(self.script_groups_path, group, "data.json")
elif level == "3":
path = os.path.join(self.working_directory, "data.json")
with open(path, "w") as f:
json.dump(data, f, indent=2)
def update_schema(
self, level: str, data: Dict[str, Any], group: str = None
) -> None:
"""Update schema for specified level."""
if level == "1":
path = os.path.join(self.data_path, "esquema.json")
elif level == "2":
path = os.path.join(self.script_groups_path, "esquema.json")
elif level == "3":
path = os.path.join(self.script_groups_path, group, "esquema.json")
with open(path, "w") as f:
json.dump(data, f, indent=2)
def list_scripts(self, group: str) -> List[Dict[str, str]]:
"""List all scripts in a group with their descriptions."""
try:
scripts_dir = os.path.join(self.script_groups_path, group)
scripts = []
if not os.path.exists(scripts_dir):
print(f"Directory not found: {scripts_dir}")
return []
for file in os.listdir(scripts_dir):
# Modificar la condición para incluir cualquier archivo .py
if file.endswith(".py"):
path = os.path.join(scripts_dir, file)
description = self._extract_script_description(path)
print(
f"Found script: {file} with description: {description}"
) # Debug line
scripts.append({"name": file, "description": description})
print(f"Total scripts found: {len(scripts)}") # Debug line
return scripts
except Exception as e:
print(f"Error listing scripts: {str(e)}") # Debug line
return []
def _extract_script_description(self, script_path: str) -> str:
"""Extract description from script's docstring or initial comments."""
try:
with open(script_path, "r", encoding="utf-8") as f:
content = f.read()
# Try to find docstring
docstring_match = re.search(r'"""(.*?)"""', content, re.DOTALL)
if docstring_match:
return docstring_match.group(1).strip()
# Try to find initial comment
comment_match = re.search(r"^#\s*(.*?)$", content, re.MULTILINE)
if comment_match:
return comment_match.group(1).strip()
return "No description available"
except Exception as e:
print(
f"Error extracting description from {script_path}: {str(e)}"
) # Debug line
return "Error reading script description"
def execute_script(
self, group: str, script_name: str, broadcast_fn=None
) -> Dict[str, Any]:
"""Execute script with real-time logging via WebSocket broadcast function."""
script_path = os.path.join(self.script_groups_path, group, script_name)
if not os.path.exists(script_path):
return {"error": "Script not found"}
# Obtener el directorio de trabajo del grupo
working_dir = self.get_work_dir(group)
if not working_dir:
return {"error": "Working directory not set for this script group"}
configs = {
"level1": self.get_config("1"),
"level2": self.get_config("2", group),
"level3": self.get_config("3") if self.working_directory else {},
}
try:
if broadcast_fn:
broadcast_fn(f"\nIniciando ejecución de {script_name}...\n")
process = subprocess.Popen(
["python", script_path],
cwd=working_dir or self.base_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
env=dict(os.environ, **{"SCRIPT_CONFIGS": json.dumps(configs)}),
)
output = []
while True:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
output.append(line)
if broadcast_fn:
broadcast_fn(line)
stderr = process.stderr.read()
if stderr:
if broadcast_fn:
broadcast_fn(f"\nERROR: {stderr}\n")
output.append(f"ERROR: {stderr}")
if broadcast_fn:
broadcast_fn("\nEjecución completada.\n")
return {
"output": "".join(output),
"error": stderr if stderr else None,
"status": "success" if process.returncode == 0 else "error",
}
except Exception as e:
error_msg = str(e)
if broadcast_fn:
broadcast_fn(f"\nError inesperado: {error_msg}\n")
return {"error": error_msg}
def get_work_dir(self, group: str) -> str:
"""Get working directory path for a script group."""
work_dir_path = os.path.join(self.script_groups_path, group, "work_dir.json")
try:
with open(work_dir_path, "r") as f:
data = json.load(f)
path = data.get("path", "")
# Actualizar la variable de instancia si hay una ruta válida
if path and os.path.exists(path):
self.working_directory = path
return path
except (FileNotFoundError, json.JSONDecodeError):
return ""
def set_work_dir(self, group: str, path: str) -> Dict[str, str]:
"""Set working directory path for a script group."""
if not os.path.exists(path):
return {"status": "error", "message": "Directory does not exist"}
work_dir_path = os.path.join(self.script_groups_path, group, "work_dir.json")
try:
# Guardar la ruta en work_dir.json
with open(work_dir_path, "w") as f:
json.dump({"path": path}, f, indent=2)
# Actualizar la variable de instancia
self.working_directory = path
# Crear data.json en el directorio de trabajo si no existe
data_path = os.path.join(path, "data.json")
if not os.path.exists(data_path):
with open(data_path, "w") as f:
json.dump({}, f, indent=2)
return {"status": "success", "path": path}
except Exception as e:
return {"status": "error", "message": str(e)}

6
data/data.json Normal file
View File

@ -0,0 +1,6 @@
{
"api_key": "your-api-key-here",
"model": "gpt-3.5-turbo",
"max_tokens": 1000,
"temperature": 0.7
}

32
data/esquema.json Normal file
View File

@ -0,0 +1,32 @@
{
"properties": {
"api_key": {
"description": "Tu clave de API para servicios externos",
"title": "API Key",
"type": "string"
},
"max_tokens": {
"description": "N\u00c3\u00bamero m\u00c3\u00a1ximo de tokens por respuesta",
"title": "Tokens M\u00c3\u00a1ximos",
"type": "number"
},
"model": {
"description": "Modelo de lenguaje a utilizar",
"enum": [
"gpt-3.5-turbo",
"gpt-4",
"claude-v1"
],
"title": "Modelo LLM",
"type": "string"
},
"temperature": {
"description": "Creatividad de las respuestas (0-1)",
"maximum": 1,
"minimum": 0,
"title": "Temperatura",
"type": "number"
}
},
"type": "object"
}

504
templates/index.html Normal file
View File

@ -0,0 +1,504 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Script Parameter Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
let socket;
let currentGroup;
// Initialize WebSocket connection
function initWebSocket() {
socket = new WebSocket(`ws://${location.host}/ws`);
socket.onmessage = function(event) {
const logArea = document.getElementById('log-area');
logArea.innerHTML += event.data;
logArea.scrollTop = logArea.scrollHeight;
};
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 => `
<div class="mb-4 p-4 border rounded">
<div class="font-bold">${script.name}</div>
<div class="text-gray-600 text-sm">${script.description}</div>
<button onclick="executeScript('${script.name}')"
class="mt-2 bg-green-500 text-white px-3 py-1 rounded">
Ejecutar
</button>
</div>
`).join('');
}
// Execute a script
async function executeScript(scriptName) {
// Mostrar mensaje de inicio en los logs
const logArea = document.getElementById('log-area');
logArea.innerHTML += `\nEjecutando script: ${scriptName}...\n`;
logArea.scrollTop = logArea.scrollHeight;
const response = await fetch('/api/execute_script', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group: currentGroup, script: scriptName })
});
const result = await response.json();
if (result.error) {
logArea.innerHTML += `\nError: ${result.error}\n`;
}
logArea.scrollTop = logArea.scrollHeight;
}
// Form rendering functionality
async function renderForm(containerId, data) {
console.log(`Rendering form for ${containerId}`); // Debug line
const container = document.getElementById(containerId);
const level = containerId.replace('level', '').split('-')[0];
try {
const schemaResponse = await fetch(`/api/schema/${level}?group=${currentGroup}`);
const schema = await schemaResponse.json();
console.log(`Schema for level ${level}:`, schema); // Debug line
if (Object.keys(schema).length === 0) {
container.innerHTML = '<p class="text-gray-500">No hay esquema definido para este nivel.</p>';
} else {
const formHtml = generateFormFields(schema, data, '', level);
console.log(`Generated HTML for ${containerId}:`, formHtml.substring(0, 100) + '...'); // Debug line
container.innerHTML = formHtml;
}
} catch (error) {
console.error(`Error rendering form ${containerId}:`, error);
container.innerHTML = '<p class="text-red-500">Error cargando el esquema.</p>';
}
}
function generateFormFields(schema, data, prefix, level) {
let html = '';
for (const [key, def] of Object.entries(schema.properties)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = getValue(data, fullKey);
html += `<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">${def.title || key}</label>`;
if (def.type === 'object') {
html += `<div class="pl-4 border-l-2 border-gray-200">
${generateFormFields(def, data, fullKey, level)}
</div>`;
} else {
html += generateInputField(def, fullKey, value, level);
}
if (def.description) {
html += `<p class="text-gray-500 text-xs mt-1">${def.description}</p>`;
}
html += '</div>';
}
return html;
}
function generateInputField(def, key, value, level) {
const baseClasses = "w-full p-2 border rounded bg-green-50"; // Agregado bg-green-50 para fondo verde claro
switch (def.type) {
case 'string':
if (def.enum) {
return `<select class="${baseClasses}"
onchange="updateConfig(${level}, '${key}', this.value)">
${def.enum.map(opt => `<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt}</option>`).join('')}
</select>`;
}
return `<input type="text" value="${value || ''}"
class="${baseClasses}"
onchange="updateConfig(${level}, '${key}', this.value)">`;
case 'number':
return `<input type="number" value="${value || 0}"
class="${baseClasses}"
onchange="updateConfig(${level}, '${key}', Number(this.value))">`;
case 'boolean':
return `<div class="flex items-center">
<input type="checkbox" ${value ? 'checked' : ''}
class="form-checkbox h-5 w-5 bg-green-50"
onchange="updateConfig(${level}, '${key}', this.checked)">
</div>`;
default:
return `<input type="text" value="${value || ''}"
class="${baseClasses}"
onchange="updateConfig(${level}, '${key}', this.value)">`;
}
}
async function modifySchema(level) {
const response = await fetch(`/api/schema/${level}?group=${currentGroup}`);
const schema = await response.json();
// Show schema editor modal
const modal = document.getElementById('schema-editor');
modal.classList.remove('hidden');
document.getElementById('schema-content').value = JSON.stringify(schema, null, 2);
document.getElementById('schema-level').value = level;
}
async function saveSchema() {
const level = document.getElementById('schema-level').value;
const content = document.getElementById('schema-content').value;
try {
const schema = JSON.parse(content);
await fetch(`/api/schema/${level}?group=${currentGroup}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(schema)
});
// Refresh form
const response = await fetch(`/api/config/${level}?group=${currentGroup}`);
const data = await response.json();
renderForm(`level${level}-form`, data);
// Hide modal
document.getElementById('schema-editor').classList.add('hidden');
} catch (e) {
alert('Invalid JSON schema: ' + e.message);
}
}
function getValue(data, path) {
return path.split('.').reduce((obj, key) => obj?.[key], data);
}
// Actualizar configuración cuando cambia un campo
async function updateConfig(level, key, value) {
const group = currentGroup;
// Obtener configuración actual
const response = await fetch(`/api/config/${level}?group=${group}`);
const config = await response.json();
// Actualizar el valor usando la ruta del campo
setNestedValue(config, key, value);
// Guardar configuración actualizada
await fetch(`/api/config/${level}?group=${group}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
}
// Función auxiliar para establecer valor en objeto anidado
function setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current)) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
async function setWorkingDirectory() {
if (!currentGroup) {
alert('Por favor, seleccione un grupo de scripts primero');
return;
}
const path = document.getElementById('working-directory').value;
await updateWorkingDirectory(path);
}
async function initWorkingDirectory() {
if (!currentGroup) return;
const response = await fetch(`/api/working-directory/${currentGroup}`);
const result = await response.json();
if (result.status === 'success' && result.path) {
await updateWorkingDirectory(result.path);
}
}
async function browseDirectory() {
console.log('Current group when browsing:', currentGroup); // Debug line
if (!currentGroup) {
alert('Por favor, seleccione un grupo de scripts primero');
return;
}
const currentPath = document.getElementById('working-directory').value;
const response = await fetch(`/api/browse-directories?current_path=${encodeURIComponent(currentPath)}`);
const result = await response.json();
if (result.status === 'success') {
await updateWorkingDirectory(result.path);
}
}
// Nueva función auxiliar para actualizar el directorio de trabajo
async function updateWorkingDirectory(path) {
console.log('Updating working directory:', { path, group: currentGroup }); // Debug line
const response = await fetch('/api/working-directory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: path,
group: currentGroup
})
});
const result = await response.json();
console.log('Update result:', result); // Debug line
if (result.status === 'success') {
// Actualizar input
document.getElementById('working-directory').value = path;
// Recargar configuración de nivel 3
const configResponse = await fetch(`/api/config/3?group=${currentGroup}`);
const data = await configResponse.json();
await renderForm('level3-form', data);
} else {
alert('Error: ' + (result.message || 'No se pudo actualizar el directorio de trabajo'));
}
}
// Función para alternar visibilidad de una sección
function toggleConfig(sectionId) {
const content = document.getElementById(sectionId);
const button = document.querySelector(`[onclick="toggleConfig('${sectionId}')"]`);
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
button.innerText = 'Ocultar Configuración';
// Recargar la configuración al mostrar
const level = sectionId.replace('level', '').replace('-content', '');
const formId = `level${level}-form`;
console.log(`Reloading config for level ${level}`); // Debug line
fetch(`/api/config/${level}?group=${currentGroup}`)
.then(response => response.json())
.then(data => renderForm(formId, data))
.catch(error => console.error('Error reloading config:', error));
} else {
content.classList.add('hidden');
button.innerText = 'Mostrar Configuración';
}
}
function clearLogs() {
document.getElementById('log-area').innerHTML = '';
}
// Initialize on page load
async function initializeApp() {
try {
initWebSocket();
// Primero establecer el grupo actual
const group = localStorage.getItem('selectedGroup');
const selectElement = document.getElementById('script-group');
if (group) {
selectElement.value = group;
}
currentGroup = selectElement.value; // Siempre establecer currentGroup con el valor actual del select
console.log('Current group initialized as:', currentGroup); // Debug line
// Luego cargar el directorio de trabajo
await initWorkingDirectory();
// Finalmente cargar las configuraciones
await loadConfigs();
// Configurar el evento de cambio de grupo
selectElement.addEventListener('change', async (e) => {
currentGroup = e.value;
localStorage.setItem('selectedGroup', e.value);
console.log('Group changed to:', currentGroup); // Debug line
await initWorkingDirectory();
await loadConfigs();
});
} catch (error) {
console.error('Error during initialization:', error);
}
}
// Modificar la inicialización para usar la nueva función async
document.addEventListener('DOMContentLoaded', () => {
initializeApp().catch(console.error);
});
</script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<!-- Level 1 Configuration -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración General (Nivel 1)</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level1-content')">
Mostrar Configuración
</button>
</div>
<div id="level1-content" class="hidden">
<div id="level1-form"></div>
<button class="bg-blue-500 text-white px-4 py-2 rounded mt-4" onclick="modifySchema(1)">
Modificar Esquema
</button>
</div>
</div>
<!-- Script Group Selection -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Grupo de Scripts</h2>
<select id="script-group" class="w-full p-2 border rounded">
{% for group in script_groups %}
<option value="{{ group }}">{{ group }}</option>
{% endfor %}
</select>
</div>
<!-- Level 2 Configuration -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Grupo (Nivel 2)</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level2-content')">
Mostrar Configuración
</button>
</div>
<div id="level2-content" class="hidden">
<div id="level2-form"></div>
<button class="bg-blue-500 text-white px-4 py-2 rounded mt-4" onclick="modifySchema(2)">
Modificar Esquema
</button>
</div>
</div>
<!-- Working Directory -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Directorio de Trabajo</h2>
<div class="flex gap-4">
<div class="flex-1 flex gap-2">
<input type="text" id="working-directory" class="flex-1 p-2 border rounded bg-green-50">
<button class="bg-gray-500 text-white px-4 py-2 rounded" onclick="browseDirectory()">
Explorar
</button>
</div>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
Confirmar
</button>
</div>
</div>
<!-- Level 3 Configuration -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Directorio (Nivel 3)</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level3-content')">
Mostrar Configuración
</button>
</div>
<div id="level3-content" class="hidden">
<div id="level3-form"></div>
<button class="bg-blue-500 text-white px-4 py-2 rounded mt-4" onclick="modifySchema(3)">
Modificar Esquema
</button>
</div>
</div>
<!-- Scripts List -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Scripts Disponibles</h2>
<div id="scripts-list"></div>
</div>
<!-- Logs -->
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Logs</h2>
<button class="bg-red-500 text-white px-4 py-2 rounded" onclick="clearLogs()">
Limpiar
</button>
</div>
<div id="log-area" class="bg-gray-100 p-4 rounded h-64 overflow-y-auto font-mono text-sm">
</div>
</div>
</div>
<!-- Schema Editor Modal -->
<div id="schema-editor" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
<div class="bg-white p-6 rounded-lg shadow-lg w-3/4 max-h-screen overflow-auto">
<h3 class="text-xl font-bold mb-4">Editor de Esquema</h3>
<textarea id="schema-content" class="w-full h-96 font-mono p-2 border rounded mb-4"></textarea>
<input type="hidden" id="schema-level">
<div class="flex justify-end gap-4">
<button onclick="document.getElementById('schema-editor').classList.add('hidden')"
class="bg-gray-500 text-white px-4 py-2 rounded">
Cancelar
</button>
<button onclick="saveSchema()"
class="bg-blue-500 text-white px-4 py-2 rounded">
Guardar
</button>
</div>
</div>
</div>
</body>
</html>