diff --git a/backend/app.py b/backend/app.py index 20bc044..1bf0035 100644 --- a/backend/app.py +++ b/backend/app.py @@ -84,17 +84,6 @@ def get_script_groups(): except Exception as e: return jsonify({"error": str(e)}), 500 -@app.route('/api/script-groups//scripts', methods=['GET']) -def get_group_scripts(group_id): - """Get scripts for a specific group""" - try: - scripts = script_manager.get_group_scripts(group_id) - return jsonify(scripts) - except ValueError as e: - return jsonify({"error": str(e)}), 404 - except Exception as e: - return jsonify({"error": str(e)}), 500 - # Directory handling endpoints @app.route('/api/select-directory', methods=['GET']) def handle_select_directory(): @@ -162,5 +151,43 @@ def update_group_config(work_dir, group_id): except Exception as e: return jsonify({"error": str(e)}), 400 +@app.route('/api/script-groups//config-schema', methods=['PUT']) +def update_group_config_schema(group_id): + """Update configuration schema for a script group""" + try: + schema = request.json + config_file = Path(script_manager.script_groups_dir) / group_id / "config.json" + + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(schema, f, indent=4) + + return jsonify({"status": "success"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/script-groups//scripts', methods=['GET']) +def get_group_scripts(group_id): + """Get scripts for a specific group""" + try: + print(f"Loading scripts for group: {group_id}") # Debug + scripts = script_manager.get_group_scripts(group_id) + print(f"Scripts found: {scripts}") # Debug + return jsonify(scripts) + except Exception as e: + print(f"Error loading scripts: {str(e)}") # Debug + return jsonify({"error": str(e)}), 500 + +@app.route('/api/script-groups//config-schema', methods=['GET']) +def get_group_config_schema(group_id): + """Get configuration schema for a script group""" + try: + print(f"Loading config schema for group: {group_id}") # Debug + schema = script_manager.get_group_config_schema(group_id) + print(f"Schema loaded: {schema}") # Debug + return jsonify(schema) + except Exception as e: + print(f"Error loading schema: {str(e)}") # Debug + return jsonify({"error": str(e)}), 500 + if __name__ == '__main__': app.run(debug=True, port=5000) diff --git a/backend/core/__pycache__/directory_handler.cpython-310.pyc b/backend/core/__pycache__/directory_handler.cpython-310.pyc index 8f1efa0..b82437b 100644 Binary files a/backend/core/__pycache__/directory_handler.cpython-310.pyc and b/backend/core/__pycache__/directory_handler.cpython-310.pyc differ diff --git a/backend/core/__pycache__/script_manager.cpython-310.pyc b/backend/core/__pycache__/script_manager.cpython-310.pyc index 97eb113..bbb3859 100644 Binary files a/backend/core/__pycache__/script_manager.cpython-310.pyc and b/backend/core/__pycache__/script_manager.cpython-310.pyc differ diff --git a/backend/core/directory_handler.py b/backend/core/directory_handler.py index ea44391..53f5ad9 100644 --- a/backend/core/directory_handler.py +++ b/backend/core/directory_handler.py @@ -6,24 +6,18 @@ from tkinter import filedialog from flask import jsonify def select_directory(): - """ - Show directory selection dialog and return selected path - """ + """Show directory selection dialog and return selected path""" root = tk.Tk() - root.withdraw() # Hide the main window + root.withdraw() + root.attributes('-topmost', True) # Hace que el diálogo siempre esté encima try: - print("Opening directory dialog...") # Debug directory = filedialog.askdirectory( title="Select Work Directory", initialdir=os.path.expanduser("~") ) - print(f"Selected directory: {directory}") # Debug - result = {"path": directory} if directory else {"error": "No directory selected"} - print(f"Returning result: {result}") # Debug - return result + return {"path": directory} if directory else {"error": "No directory selected"} except Exception as e: - print(f"Error in select_directory: {str(e)}") # Debug return {"error": str(e)} finally: root.destroy() \ No newline at end of file diff --git a/backend/core/script_manager.py b/backend/core/script_manager.py index 72609a1..404f670 100644 --- a/backend/core/script_manager.py +++ b/backend/core/script_manager.py @@ -6,6 +6,30 @@ from typing import Dict, List, Any, Optional import json class ScriptManager: + + def get_group_config_schema(self, group_id: str) -> Dict[str, Any]: + """Get configuration schema for a script group""" + config_file = self.script_groups_dir / group_id / "config.json" + print(f"Looking for config file: {config_file}") # Debug + + if config_file.exists(): + try: + with open(config_file, 'r', encoding='utf-8') as f: + schema = json.load(f) + print(f"Loaded schema: {schema}") # Debug + return schema + except Exception as e: + print(f"Error loading group config schema: {e}") # Debug + else: + print(f"Config file not found: {config_file}") # Debug + + # Retornar un schema vacío si no existe el archivo + return { + "group_name": group_id, + "description": "", + "config_schema": {} + } + def get_available_groups(self) -> List[Dict[str, Any]]: """Get list of available script groups""" groups = [] @@ -23,18 +47,20 @@ class ScriptManager: def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: """Get scripts for a specific group""" group_dir = self.script_groups_dir / group_id + print(f"Looking for scripts in: {group_dir}") # Debug if not group_dir.exists() or not group_dir.is_dir(): + print(f"Directory not found: {group_dir}") # Debug raise ValueError(f"Script group '{group_id}' not found") - + scripts = [] for script_file in group_dir.glob('x[0-9].py'): + print(f"Found script file: {script_file}") # Debug script_info = self._analyze_script(script_file) if script_info: scripts.append(script_info) - + return sorted(scripts, key=lambda x: x['id']) - """Manages script discovery and execution""" def __init__(self, script_groups_dir: Path): self.script_groups_dir = script_groups_dir @@ -92,10 +118,19 @@ class ScriptManager: break if script_class: + # Extraer la primera línea del docstring como nombre + docstring = inspect.getdoc(script_class) + if docstring: + name, *description = docstring.split('\n', 1) + description = description[0] if description else '' + else: + name = script_file.stem + description = '' + return { "id": script_file.stem, - "name": script_class.__doc__.split('\n')[0].strip() if script_class.__doc__ else script_file.stem, - "description": inspect.getdoc(script_class), + "name": name.strip(), + "description": description.strip(), "file": str(script_file.relative_to(self.script_groups_dir)) } diff --git a/backend/script_groups/example_group/__pycache__/x1.cpython-310.pyc b/backend/script_groups/example_group/__pycache__/x1.cpython-310.pyc index ce249e2..f90b7e7 100644 Binary files a/backend/script_groups/example_group/__pycache__/x1.cpython-310.pyc and b/backend/script_groups/example_group/__pycache__/x1.cpython-310.pyc differ diff --git a/backend/script_groups/example_group/__pycache__/x2.cpython-310.pyc b/backend/script_groups/example_group/__pycache__/x2.cpython-310.pyc index e02ef0f..c3e7d9c 100644 Binary files a/backend/script_groups/example_group/__pycache__/x2.cpython-310.pyc and b/backend/script_groups/example_group/__pycache__/x2.cpython-310.pyc differ diff --git a/backend/script_groups/example_group/config.json b/backend/script_groups/example_group/config.json new file mode 100644 index 0000000..36be0b3 --- /dev/null +++ b/backend/script_groups/example_group/config.json @@ -0,0 +1,32 @@ +{ + "group_name": "System Analysis", + "description": "Scripts for system analysis and file management", + "config_schema": { + "exclude_dirs": { + "type": "string", + "description": "Directories to exclude (comma separated)", + "default": "venv,__pycache__,.git" + }, + "count_hidden": { + "type": "boolean", + "description": "Include hidden files in count", + "default": false + }, + "min_size": { + "type": "number", + "description": "Minimum file size to count (in bytes)", + "default": 0 + }, + "save_report": { + "type": "boolean", + "description": "Save results to file", + "default": true + }, + "report_format": { + "type": "select", + "options": ["txt", "json", "csv"], + "description": "Format for saved reports", + "default": "json" + } + } +} \ No newline at end of file diff --git a/backend/script_groups/example_group/x1.py b/backend/script_groups/example_group/x1.py index f5ba050..257c8de 100644 --- a/backend/script_groups/example_group/x1.py +++ b/backend/script_groups/example_group/x1.py @@ -2,22 +2,33 @@ from backend.script_groups.base_script import BaseScript import os from pathlib import Path +import json +import csv +from datetime import datetime class FileCounter(BaseScript): """ - Count Files in Directory - Lists and counts files in the working directory by extension + File Analysis + Analyzes files in directory with configurable filters and reporting """ def run(self, work_dir: str, profile: dict) -> dict: try: - # Get configuration if any + # Get configuration config = self.get_config(work_dir, "example_group") - exclude_dirs = config.get("exclude_dirs", []) + + # Process configuration values + exclude_dirs = [d.strip() for d in config.get("exclude_dirs", "").split(",") if d.strip()] + count_hidden = config.get("count_hidden", False) + min_size = config.get("min_size", 0) + save_report = config.get("save_report", True) + report_format = config.get("report_format", "json") # Initialize counters extension_counts = {} total_files = 0 + total_size = 0 + skipped_files = 0 # Walk through directory for root, dirs, files in os.walk(work_dir): @@ -25,18 +36,67 @@ class FileCounter(BaseScript): dirs[:] = [d for d in dirs if d not in exclude_dirs] for file in files: + file_path = Path(root) / file + + # Skip hidden files if not counting them + if not count_hidden and file.startswith('.'): + skipped_files += 1 + continue + + # Check file size + try: + file_size = file_path.stat().st_size + if file_size < min_size: + skipped_files += 1 + continue + except: + continue + + # Count file total_files += 1 - ext = Path(file).suffix.lower() or 'no extension' + total_size += file_size + ext = file_path.suffix.lower() or 'no extension' extension_counts[ext] = extension_counts.get(ext, 0) + 1 + # Prepare results + results = { + "scan_time": datetime.now().isoformat(), + "total_files": total_files, + "total_size": total_size, + "skipped_files": skipped_files, + "extension_counts": extension_counts + } + + # Save report if configured + if save_report: + report_path = Path(work_dir) / f"file_analysis.{report_format}" + if report_format == "json": + with open(report_path, 'w') as f: + json.dump(results, f, indent=2) + elif report_format == "csv": + with open(report_path, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(["Extension", "Count"]) + for ext, count in sorted(extension_counts.items()): + writer.writerow([ext, count]) + else: # txt + with open(report_path, 'w') as f: + f.write(f"File Analysis Report\n") + f.write(f"Generated: {results['scan_time']}\n\n") + f.write(f"Total Files: {total_files}\n") + f.write(f"Total Size: {total_size:,} bytes\n") + f.write(f"Skipped Files: {skipped_files}\n\n") + f.write("Extension Counts:\n") + for ext, count in sorted(extension_counts.items()): + f.write(f"{ext}: {count}\n") + return { "status": "success", - "data": { - "total_files": total_files, - "extension_counts": extension_counts - }, - "output": f"Found {total_files} files\n" + "\n".join( - f"{ext}: {count} files" + "data": results, + "output": f"Found {total_files:,} files ({total_size:,} bytes)\n" + + f"Skipped {skipped_files} files\n\n" + + "Extensions:\n" + "\n".join( + f"{ext}: {count:,} files" for ext, count in sorted(extension_counts.items()) ) } @@ -45,5 +105,4 @@ class FileCounter(BaseScript): return { "status": "error", "error": str(e) - } - + } \ No newline at end of file diff --git a/backend/script_groups/example_group/x2.py b/backend/script_groups/example_group/x2.py index 917c107..c3b805b 100644 --- a/backend/script_groups/example_group/x2.py +++ b/backend/script_groups/example_group/x2.py @@ -3,46 +3,93 @@ from backend.script_groups.base_script import BaseScript import psutil import json from datetime import datetime +from pathlib import Path class SystemInfo(BaseScript): """ - System Information - Collects and displays basic system information + System Monitor + Collects and analyzes system performance metrics """ def run(self, work_dir: str, profile: dict) -> dict: try: + # Get configuration from the same config.json + config = self.get_config(work_dir, "example_group") + save_report = config.get("save_report", True) + report_format = config.get("report_format", "json") + # Collect system information + cpu_freq = psutil.cpu_freq() + memory = psutil.virtual_memory() + disk = psutil.disk_usage(work_dir) + info = { + "timestamp": datetime.now().isoformat(), "cpu": { "cores": psutil.cpu_count(), - "usage": psutil.cpu_percent(interval=1), + "physical_cores": psutil.cpu_count(logical=False), + "frequency": { + "current": round(cpu_freq.current, 2) if cpu_freq else None, + "min": round(cpu_freq.min, 2) if cpu_freq else None, + "max": round(cpu_freq.max, 2) if cpu_freq else None + }, + "usage_percent": psutil.cpu_percent(interval=1) }, "memory": { - "total": psutil.virtual_memory().total, - "available": psutil.virtual_memory().available, - "percent": psutil.virtual_memory().percent, + "total": memory.total, + "available": memory.available, + "used": memory.used, + "percent": memory.percent }, "disk": { - "total": psutil.disk_usage(work_dir).total, - "free": psutil.disk_usage(work_dir).free, - "percent": psutil.disk_usage(work_dir).percent, + "total": disk.total, + "used": disk.used, + "free": disk.free, + "percent": disk.percent }, - "timestamp": datetime.now().isoformat() + "network": { + "interfaces": list(psutil.net_if_addrs().keys()), + "connections": len(psutil.net_connections()) + } } - # Save to work directory if configured - config = self.get_config(work_dir, "example_group") - if config.get("save_system_info", False): - output_file = Path(work_dir) / "system_info.json" - with open(output_file, 'w') as f: - json.dump(info, f, indent=2) + # Save report if configured + if save_report: + report_path = Path(work_dir) / f"system_info.{report_format}" + if report_format == "json": + with open(report_path, 'w') as f: + json.dump(info, f, indent=2) + elif report_format == "csv": + with open(report_path, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(["Metric", "Value"]) + writer.writerow(["CPU Cores", info["cpu"]["cores"]]) + writer.writerow(["CPU Usage", f"{info['cpu']['usage_percent']}%"]) + writer.writerow(["Memory Total", f"{info['memory']['total']:,} bytes"]) + writer.writerow(["Memory Used", f"{info['memory']['percent']}%"]) + writer.writerow(["Disk Total", f"{info['disk']['total']:,} bytes"]) + writer.writerow(["Disk Used", f"{info['disk']['percent']}%"]) + else: # txt + with open(report_path, 'w') as f: + f.write(f"System Information Report\n") + f.write(f"Generated: {info['timestamp']}\n\n") + f.write(f"CPU:\n") + f.write(f" Cores: {info['cpu']['cores']}\n") + f.write(f" Usage: {info['cpu']['usage_percent']}%\n\n") + f.write(f"Memory:\n") + f.write(f" Total: {info['memory']['total']:,} bytes\n") + f.write(f" Used: {info['memory']['percent']}%\n\n") + f.write(f"Disk:\n") + f.write(f" Total: {info['disk']['total']:,} bytes\n") + f.write(f" Used: {info['disk']['percent']}%\n") # Format output output = f"""System Information: -CPU: {info['cpu']['cores']} cores ({info['cpu']['usage']}% usage) -Memory: {info['memory']['percent']}% used -Disk: {info['disk']['percent']}% used""" +CPU: {info['cpu']['cores']} cores ({info['cpu']['usage_percent']}% usage) +Memory: {info['memory']['percent']}% used ({info['memory']['available']:,} bytes available) +Disk: {info['disk']['percent']}% used ({info['disk']['free']:,} bytes free) +Network Interfaces: {', '.join(info['network']['interfaces'])} +Active Connections: {info['network']['connections']}""" return { "status": "success", @@ -54,4 +101,4 @@ Disk: {info['disk']['percent']}% used""" return { "status": "error", "error": str(e) - } + } \ No newline at end of file diff --git a/claude_file_organizer.py b/claude_file_organizer.py new file mode 100644 index 0000000..2889dae --- /dev/null +++ b/claude_file_organizer.py @@ -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': '', + } + 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('') + 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() \ No newline at end of file diff --git a/files.txt b/files.txt index 8b81891..544d4dd 100644 Binary files a/files.txt and b/files.txt differ diff --git a/frontend/static/css/style.css b/frontend/static/css/style.css index 60220d9..5ab1743 100644 --- a/frontend/static/css/style.css +++ b/frontend/static/css/style.css @@ -1,100 +1,12 @@ -/* frontend/static/css/style.css - Add these styles */ - -.script-group { - background: white; - border-radius: 8px; - padding: 1rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.script-group-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--border-color); -} - -.script-group-header h3 { - margin: 0; - color: var(--primary-color); -} - -.script-list { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.script-item { - display: flex; - justify-content: space-between; - align-items: start; - padding: 1rem; - background: var(--bg-color); - border-radius: 4px; -} - -.script-info { - flex: 1; -} - -.script-info h4 { - margin: 0 0 0.5rem 0; - color: var(--secondary-color); -} - -.script-info p { - margin: 0; - font-size: 0.9rem; - color: #666; -} - -.script-actions { - display: flex; - gap: 0.5rem; -} - -.config-btn { - background-color: var(--secondary-color); -} - -.run-btn { - background-color: #27ae60; -} - -.run-btn:hover { - background-color: #219a52; -} - -.config-editor { - width: 100%; - font-family: monospace; - padding: 0.5rem; - border: 1px solid var(--border-color); - border-radius: 4px; -} +/* frontend/static/css/style.css */ +/* Solo mantenemos estilos específicos que no podemos lograr fácilmente con Tailwind */ .output-area { - background: white; - padding: 1rem; - border-radius: 4px; - border: 1px solid var(--border-color); - height: 200px; - overflow-y: auto; - font-family: monospace; white-space: pre-wrap; + word-wrap: break-word; } -.no-scripts { - text-align: center; - color: #666; - padding: 2rem; -} - -/* Add to your existing style.css */ - +/* Estilos para modales */ .modal { position: fixed; top: 0; @@ -111,82 +23,9 @@ .modal-content { background: white; padding: 2rem; - border-radius: 8px; + border-radius: 0.5rem; max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; -} - -.form-group { - margin-bottom: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; -} - -.form-group input, -.form-group select, -.form-group textarea { - width: 100%; -} - -.button-group { - display: flex; - justify-content: flex-end; - gap: 1rem; - margin-top: 2rem; -} - -.config-info { - margin-bottom: 1.5rem; -} - -.group-configs { - margin-top: 1.5rem; -} - -.group-config { - margin-top: 1rem; - padding: 1rem; - background: var(--bg-color); - border-radius: 4px; -} - -.group-config pre { - margin-top: 0.5rem; - white-space: pre-wrap; - font-size: 0.9rem; -} - -/* frontend/static/css/style.css */ - -.script-group-selector { - margin-bottom: 20px; - padding: 1rem; - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.script-group-selector label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; -} - -.script-group-selector select { - width: 100%; - padding: 0.5rem; - border: 1px solid var(--border-color); - border-radius: 4px; - background: white; -} - -#scriptList { - display: none; - margin-top: 1rem; } \ No newline at end of file diff --git a/frontend/static/js/main.js b/frontend/static/js/main.js index 3ca3e02..61b8e54 100644 --- a/frontend/static/js/main.js +++ b/frontend/static/js/main.js @@ -3,17 +3,86 @@ // Global state let currentProfile = null; -// Initialize when page loads -document.addEventListener('DOMContentLoaded', async () => { +async function initializeApp() { try { - await loadProfiles(); - await loadScriptGroups(); + console.log('Initializing app...'); + + // Cargar perfiles + const profiles = await apiRequest('/profiles'); + console.log('Profiles loaded:', profiles); + + // Obtener último perfil usado + const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; + console.log('Last profile ID:', lastProfileId); + + // Actualizar selector de perfiles + updateProfileSelector(profiles); + + // Seleccionar el último perfil usado + const selectedProfile = profiles.find(p => p.id === lastProfileId) || profiles[0]; + if (selectedProfile) { + console.log('Selecting profile:', selectedProfile.id); + await selectProfile(selectedProfile.id); + } + + // Cargar grupos de scripts y restaurar la última selección + await restoreScriptGroup(); + + // Actualizar la interfaz updateWorkDirDisplay(); + } catch (error) { - console.error('Initialization error:', error); + console.error('Error initializing app:', error); showError('Failed to initialize application'); } -}); +} + +async function restoreScriptGroup() { + try { + // Primero cargar los grupos disponibles + await loadScriptGroups(); + + // Luego intentar restaurar el último grupo seleccionado + const lastGroupId = localStorage.getItem('lastGroupId'); + if (lastGroupId) { + console.log('Restoring last group:', lastGroupId); + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + if (groupSelect.value) { // Verifica que el valor se haya establecido correctamente + await loadGroupScripts(lastGroupId); + } else { + console.log('Selected group no longer exists:', lastGroupId); + localStorage.removeItem('lastGroupId'); + } + } + } + } catch (error) { + console.error('Error restoring script group:', error); + } +} + +// Función para restaurar el último estado +async function restoreLastState() { + const lastProfileId = localStorage.getItem('lastProfileId'); + const lastGroupId = localStorage.getItem('lastGroupId'); + + console.log('Restoring last state:', { lastProfileId, lastGroupId }); + + if (lastProfileId) { + const profileSelect = document.getElementById('profileSelect'); + profileSelect.value = lastProfileId; + await selectProfile(lastProfileId); + } + + if (lastGroupId) { + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + await loadGroupScripts(lastGroupId); + } + } +} // API functions async function apiRequest(endpoint, options = {}) { @@ -39,40 +108,69 @@ async function apiRequest(endpoint, options = {}) { } } -// Profile functions async function loadProfiles() { try { const profiles = await apiRequest('/profiles'); updateProfileSelector(profiles); - // Select first profile if none selected - if (!currentProfile) { - const defaultProfile = profiles.find(p => p.id === 'default') || profiles[0]; - if (defaultProfile) { - await selectProfile(defaultProfile.id); - } + // Obtener último perfil usado + const lastProfileId = localStorage.getItem('lastProfileId'); + + // Seleccionar perfil guardado o el default + const defaultProfile = profiles.find(p => p.id === (lastProfileId || 'default')) || profiles[0]; + if (defaultProfile) { + await selectProfile(defaultProfile.id); } } catch (error) { showError('Failed to load profiles'); } } +async function selectProfile(profileId) { + try { + console.log('Selecting profile:', profileId); + currentProfile = await apiRequest(`/profiles/${profileId}`); + + // Guardar en localStorage + localStorage.setItem('lastProfileId', profileId); + console.log('Profile ID saved to storage:', profileId); + + // Actualizar explícitamente el valor del combo + const select = document.getElementById('profileSelect'); + if (select) { + select.value = profileId; + console.log('Updated profileSelect value to:', profileId); + } + + updateWorkDirDisplay(); + + // Recargar scripts con el último grupo seleccionado + await restoreScriptGroup(); + } catch (error) { + console.error('Error in selectProfile:', error); + showError('Failed to load profile'); + } +} + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', initializeApp); + function updateProfileSelector(profiles) { const select = document.getElementById('profileSelect'); + const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; + + console.log('Updating profile selector. Last profile ID:', lastProfileId); + + // Construir las opciones select.innerHTML = profiles.map(profile => ` - `).join(''); -} - -async function selectProfile(profileId) { - try { - currentProfile = await apiRequest(`/profiles/${profileId}`); - updateWorkDirDisplay(); - } catch (error) { - showError('Failed to load profile'); - } + + // Asegurar que el valor seleccionado sea correcto + select.value = lastProfileId; + console.log('Set profileSelect value to:', lastProfileId); } async function changeProfile() { diff --git a/frontend/static/js/modal.js b/frontend/static/js/modal.js new file mode 100644 index 0000000..f83a0eb --- /dev/null +++ b/frontend/static/js/modal.js @@ -0,0 +1,38 @@ +// static/js/modal.js +function createModal(title, content, onSave = null) { + const modal = document.createElement('div'); + modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50'; + + modal.innerHTML = ` +
+
+

${title}

+
+
+ ${content} +
+
+ + ${onSave ? ` + + ` : ''} +
+
+ `; + + document.body.appendChild(modal); + return modal; +} + +function closeModal(button) { + const modal = button.closest('.fixed'); + if (modal) { + modal.remove(); + } +} \ No newline at end of file diff --git a/frontend/static/js/profile.js b/frontend/static/js/profile.js index 07e4417..0a3d18b 100644 --- a/frontend/static/js/profile.js +++ b/frontend/static/js/profile.js @@ -1,4 +1,4 @@ -// frontend/static/js/profile.js +let selectedProfileId = localStorage.getItem('selectedProfileId') || 'default'; // Profile functions async function loadProfiles() { @@ -6,13 +6,19 @@ async function loadProfiles() { const profiles = await apiRequest('/profiles'); updateProfileSelector(profiles); - // Select first profile if none selected - if (!currentProfile) { - const defaultProfile = profiles.find(p => p.id === 'default') || profiles[0]; - if (defaultProfile) { - await selectProfile(defaultProfile.id); - } + // Asegurarse de que se seleccione el perfil guardado + const selectElement = document.getElementById('profileSelect'); + if (profiles[selectedProfileId]) { + selectElement.value = selectedProfileId; + await selectProfile(selectedProfileId); // Cargar el perfil seleccionado + } else { + // Si el perfil guardado ya no existe, usar el default + selectedProfileId = 'default'; + selectElement.value = 'default'; + await selectProfile('default'); } + + localStorage.setItem('selectedProfileId', selectedProfileId); } catch (error) { showError('Failed to load profiles'); } @@ -156,14 +162,94 @@ async function saveProfile(event) { } } -function editProfile() { +// static/js/profile.js +async function editProfile() { if (!currentProfile) { showError('No profile selected'); return; } - showProfileEditor(currentProfile); + + const content = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + const modal = createModal('Edit Profile', content, true); + modal.querySelector('[onclick="saveModal(this)"]').onclick = async () => { + await saveProfile(modal); + }; +} + +async function saveProfile(modal) { + const form = modal.querySelector('#profileForm'); + const formData = new FormData(form); + + const profileData = { + id: formData.get('id'), + name: formData.get('name'), + work_dir: formData.get('work_dir'), + llm_settings: { + model: formData.get('llm_model'), + api_key: formData.get('api_key'), + temperature: parseFloat(formData.get('temperature')) + } + }; + + try { + await apiRequest(`/profiles/${currentProfile.id}`, { + method: 'PUT', + body: JSON.stringify(profileData) + }); + + await loadProfiles(); + closeModal(modal.querySelector('button')); + showSuccess('Profile updated successfully'); + } catch (error) { + showError('Failed to update profile'); + } } function newProfile() { showProfileEditor(); +} + +async function onProfileChange(event) { + selectedProfileId = event.target.value; + localStorage.setItem('selectedProfileId', selectedProfileId); + await selectProfile(selectedProfileId); } \ No newline at end of file diff --git a/frontend/static/js/scripts.js b/frontend/static/js/scripts.js index 6450434..cf1bb0f 100644 --- a/frontend/static/js/scripts.js +++ b/frontend/static/js/scripts.js @@ -13,52 +13,228 @@ document.addEventListener('DOMContentLoaded', async () => { await loadScriptGroups(); }); -// Load available script groups async function loadScriptGroups() { try { + // Obtener los grupos desde el servidor const groups = await apiRequest('/script-groups'); + console.log('Loaded script groups:', groups); + + // Obtener el selector y el último grupo seleccionado const select = document.getElementById('groupSelect'); - + const lastGroupId = localStorage.getItem('lastGroupId'); + console.log('Last group ID:', lastGroupId); + + // Remover event listener anterior si existe + select.removeEventListener('change', handleGroupChange); + + // Construir las opciones select.innerHTML = ` ${groups.map(group => ` - + `).join('')} `; + + // Agregar event listener para cambios + select.addEventListener('change', handleGroupChange); + console.log('Added change event listener to groupSelect'); + + // Si hay un grupo guardado, cargarlo + if (lastGroupId) { + console.log('Loading last group scripts:', lastGroupId); + await loadGroupScripts(lastGroupId); + } + } catch (error) { + console.error('Failed to load script groups:', error); showError('Failed to load script groups'); } } -// Load scripts for selected group + +// Función para manejar el cambio de grupo +async function handleGroupChange(event) { + const groupId = event.target.value; + console.log('Group selection changed:', groupId); + + if (groupId) { + localStorage.setItem('lastGroupId', groupId); + console.log('Saved lastGroupId:', groupId); + } else { + localStorage.removeItem('lastGroupId'); + console.log('Removed lastGroupId'); + } + + await loadGroupScripts(groupId); +} + +// Actualizar función de cambio de perfil para mantener la persistencia +async function changeProfile() { + const select = document.getElementById('profileSelect'); + if (select.value) { + await selectProfile(select.value); + localStorage.setItem('lastProfileId', select.value); + + // Al cambiar de perfil, intentamos mantener el último grupo seleccionado + const lastGroupId = localStorage.getItem('lastGroupId'); + if (lastGroupId) { + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + await loadGroupScripts(lastGroupId); + } + } + } +} + async function loadGroupScripts(groupId) { const scriptList = document.getElementById('scriptList'); if (!groupId) { scriptList.style.display = 'none'; + localStorage.removeItem('lastGroupId'); // Limpiar selección + return; + } + + // Guardar grupo seleccionado + localStorage.setItem('lastGroupId', groupId); + console.log('Group saved:', groupId); + + if (!currentProfile?.work_dir) { + scriptList.innerHTML = ` +
+
+
+

+ Please select a work directory first +

+
+
+
+ `; + scriptList.style.display = 'block'; return; } try { - const scripts = await apiRequest(`/script-groups/${groupId}/scripts`); + console.log('Loading data for group:', groupId); + + // Actualizar el selector para reflejar la selección actual + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect && groupSelect.value !== groupId) { + groupSelect.value = groupId; + } - scriptList.innerHTML = scripts.map(script => ` -
-
-

${script.name}

-

${script.description || 'No description available'}

-
-
-
+
+
+ ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` +
+ + ${generateFormField(key, field, currentConfig[key])} +
+ `).join('')} +
+ +
+
+
- `).join(''); - + + +
+ ${groupScripts.map(script => ` +
+
+
+

${script.name || script.id}

+

${script.description || 'No description available'}

+
+ +
+
+ `).join('')} +
`; + scriptList.style.display = 'block'; + + // Agregar evento para guardar configuración + const form = document.getElementById('groupConfigForm'); + form.addEventListener('submit', async (e) => { + e.preventDefault(); + await saveGroupConfig(groupId, form); + }); + } catch (error) { - showError('Failed to load scripts for group'); + console.error('Error in loadGroupScripts:', error); + showError('Failed to load scripts and configuration'); } } @@ -172,18 +348,443 @@ function showGroupConfigEditor(groupId, config) { document.body.appendChild(modal); } -// Save group configuration -async function saveGroupConfig(event, groupId) { - event.preventDefault(); +function generateFormField(key, field, value) { + const currentValue = value !== undefined ? value : field.default; + + switch (field.type) { + case 'string': + return ` + + `; + case 'number': + return ` + + `; + case 'boolean': + return ` +
+ + Enable +
+ `; + case 'select': + return ` + + `; + default: + return ``; + } +} + +async function loadGroupScripts(groupId) { + const scriptList = document.getElementById('scriptList'); + + if (!groupId) { + scriptList.style.display = 'none'; + localStorage.removeItem('lastGroupId'); + return; + } + + if (!currentProfile?.work_dir) { + scriptList.innerHTML = ` +
+
+
+

+ Please select a work directory first +

+
+
+
+ `; + scriptList.style.display = 'block'; + return; + } try { - const configText = document.getElementById('configData').value; - const config = JSON.parse(configText); + console.log('Loading data for group:', groupId); + + // Cargar y loguear scripts + let groupScripts, configSchema; + try { + groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); + console.log('Scripts loaded:', groupScripts); + } catch (e) { + console.error('Error loading scripts:', e); + throw e; + } + + try { + configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); + console.log('Config schema loaded:', configSchema); + } catch (e) { + console.error('Error loading config schema:', e); + throw e; + } + + // Intentar cargar configuración actual + let currentConfig = {}; + try { + console.log('Loading current config for work_dir:', currentProfile.work_dir); + currentConfig = await apiRequest( + `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` + ); + console.log('Current config loaded:', currentConfig); + } catch (e) { + console.warn('No existing configuration found, using defaults'); + } + + // Verificar que tenemos los datos necesarios + if (!groupScripts || !configSchema) { + throw new Error('Failed to load required data'); + } + + console.log('Rendering UI with:', { + groupScripts, + configSchema, + currentConfig + }); + + scriptList.innerHTML = ` + +
+
+
+

+ ${configSchema.group_name || 'Configuration'} +

+

${configSchema.description || ''}

+
+ +
+
+
+ ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` +
+ + ${generateFormField(key, field, currentConfig[key])} +
+ `).join('')} +
+ +
+
+
+
+ + +
+ ${groupScripts.map(script => ` +
+
+
+

${script.name || script.id}

+

${script.description || 'No description available'}

+
+ +
+
+ `).join('')} +
`; + + scriptList.style.display = 'block'; + + // Agregar evento para guardar configuración + const form = document.getElementById('groupConfigForm'); + form.addEventListener('submit', async (e) => { + e.preventDefault(); + await saveGroupConfig(groupId, form); + }); - await updateGroupConfig(groupId, config); - closeModal(event.target); - showSuccess('Group configuration updated successfully'); } catch (error) { - showError(`Failed to save configuration: ${error.message}`); + console.error('Error in loadGroupScripts:', error); + showError('Failed to load scripts and configuration'); + } +} + +async function editConfigSchema(groupId) { + try { + const schema = await apiRequest(`/script-groups/${groupId}/config-schema`); + const configSection = document.createElement('div'); + configSection.id = 'schemaEditor'; + configSection.className = 'mb-6 bg-white shadow sm:rounded-lg'; + + configSection.innerHTML = ` +
+

Edit Schema Configuration

+ +
+
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+

Parameters

+
+ ${Object.entries(schema.config_schema).map(([key, param]) => ` +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ ${param.type === 'select' ? ` +
+ + +
+ ` : ''} +
+
+ `).join('')} +
+
+ + +
+
+
+ `; + + // Insertamos el editor justo después del botón "Edit Schema" + const scriptList = document.getElementById('scriptList'); + const existingEditor = document.getElementById('schemaEditor'); + if (existingEditor) { + existingEditor.remove(); + } + scriptList.insertBefore(configSection, scriptList.firstChild); + + } catch (error) { + showError('Failed to load configuration schema'); + } +} + +function createModal(title, content, onSave = null) { + const modal = document.createElement('div'); + modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4'; + + modal.innerHTML = ` +
+
+

${title}

+
+
+ ${content} +
+
+ + ${onSave ? ` + + ` : ''} +
+
+ `; + + document.body.appendChild(modal); + return modal; +} + +function addParameter(button) { + const parametersDiv = button.closest('.space-y-4').querySelector('#parameters'); + const newParam = document.createElement('div'); + newParam.className = 'parameter-item bg-gray-50 p-4 rounded-md relative'; + newParam.innerHTML = ` + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + parametersDiv.appendChild(newParam); +} + +function removeParameter(button) { + button.closest('.parameter-item').remove(); +} + +function handleTypeChange(select) { + const paramItem = select.closest('.parameter-item'); + const optionsDiv = paramItem.querySelector('[name="param_options"]')?.closest('div'); + + if (select.value === 'select') { + if (!optionsDiv) { + const div = document.createElement('div'); + div.className = 'col-span-2'; + div.innerHTML = ` + + + `; + paramItem.querySelector('.grid').appendChild(div); + } + } else { + optionsDiv?.remove(); + } +} + +async function saveConfigSchema(groupId, modal) { + const form = modal.querySelector('div'); + const schema = { + group_name: form.querySelector('[name="group_name"]').value, + description: form.querySelector('[name="description"]').value, + config_schema: {} + }; + + // Recopilar parámetros + form.querySelectorAll('.parameter-item').forEach(item => { + const name = item.querySelector('[name="param_name"]').value; + const type = item.querySelector('[name="param_type"]').value; + const description = item.querySelector('[name="param_description"]').value; + const defaultValue = item.querySelector('[name="param_default"]').value; + + const param = { + type, + description, + default: type === 'boolean' ? defaultValue === 'true' : defaultValue + }; + + if (type === 'select') { + const options = item.querySelector('[name="param_options"]').value + .split(',') + .map(opt => opt.trim()) + .filter(Boolean); + param.options = options; + } + + schema.config_schema[name] = param; + }); + + try { + await apiRequest(`/script-groups/${groupId}/config-schema`, { + method: 'PUT', + body: JSON.stringify(schema) + }); + + closeModal(modal.querySelector('button')); + showSuccess('Configuration schema updated successfully'); + // Recargar la página para mostrar los cambios + loadGroupScripts(groupId); + } catch (error) { + showError('Failed to update configuration schema'); } } \ No newline at end of file diff --git a/frontend/static/js/workdir_config.js b/frontend/static/js/workdir_config.js index 6a8bb6c..e960845 100644 --- a/frontend/static/js/workdir_config.js +++ b/frontend/static/js/workdir_config.js @@ -52,42 +52,43 @@ async function updateGroupConfig(groupId, settings) { } } +// static/js/workdir_config.js async function showWorkDirConfig() { if (!currentProfile?.work_dir) { showError('No work directory selected'); return; } - const config = await getWorkDirConfig(); - if (config) { - const modal = document.createElement('div'); - modal.className = 'modal active'; + try { + const config = await getWorkDirConfig(); - modal.innerHTML = ` -