intentando hacer funcionar los 3 niveles

This commit is contained in:
Miguel 2025-02-08 19:17:08 +01:00
parent 7e267f3509
commit 37abdf8cfd
46 changed files with 1630 additions and 4512 deletions

View File

@ -4,11 +4,11 @@ import sys
from pathlib import Path from pathlib import Path
# Add the parent directory to Python path # Add the parent directory to Python path
backend_dir = Path(__file__).parent.parent # Sube un nivel más para incluir la carpeta raíz backend_dir = Path(__file__).parent.parent
if str(backend_dir) not in sys.path: if str(backend_dir) not in sys.path:
sys.path.append(str(backend_dir)) sys.path.append(str(backend_dir))
from flask import Flask, render_template, jsonify, request, send_from_directory from flask import Flask, render_template, jsonify, request
from core.directory_handler import select_directory from core.directory_handler import select_directory
from core.script_manager import ScriptManager from core.script_manager import ScriptManager
from core.profile_manager import ProfileManager from core.profile_manager import ProfileManager
@ -46,9 +46,8 @@ def get_profile(profile_id):
@app.route('/api/profiles', methods=['POST']) @app.route('/api/profiles', methods=['POST'])
def create_profile(): def create_profile():
"""Create new profile""" """Create new profile"""
profile_data = request.json
try: try:
profile = profile_manager.create_profile(profile_data) profile = profile_manager.create_profile(request.json)
return jsonify(profile) return jsonify(profile)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
@ -57,13 +56,9 @@ def create_profile():
def update_profile(profile_id): def update_profile(profile_id):
"""Update existing profile""" """Update existing profile"""
try: try:
profile_data = request.json profile = profile_manager.update_profile(profile_id, request.json)
print(f"Received update request for profile {profile_id}: {profile_data}") # Debug
profile = profile_manager.update_profile(profile_id, profile_data)
print(f"Profile updated: {profile}") # Debug
return jsonify(profile) return jsonify(profile)
except Exception as e: except Exception as e:
print(f"Error updating profile: {e}") # Debug
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
@app.route('/api/profiles/<profile_id>', methods=['DELETE']) @app.route('/api/profiles/<profile_id>', methods=['DELETE'])
@ -75,6 +70,7 @@ def delete_profile(profile_id):
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
# Script group endpoints
@app.route('/api/script-groups', methods=['GET']) @app.route('/api/script-groups', methods=['GET'])
def get_script_groups(): def get_script_groups():
"""Get all available script groups""" """Get all available script groups"""
@ -84,109 +80,93 @@ def get_script_groups():
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Directory handling endpoints @app.route('/api/script-groups/<group_id>/config', methods=['GET'])
@app.route('/api/select-directory', methods=['GET']) def get_group_config(group_id):
def handle_select_directory(): """Get script group configuration"""
"""Handle directory selection"""
print("Handling directory selection request") # Debug
result = select_directory()
print(f"Directory selection result: {result}") # Debug
if "error" in result:
return jsonify(result), 400
return jsonify(result)
# Script management endpoints
@app.route('/api/scripts', methods=['GET'])
def get_scripts():
"""Get all available script groups"""
try: try:
groups = script_manager.discover_groups() config = script_manager.get_group_data(group_id)
return jsonify(groups) return jsonify(config)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@app.route('/api/scripts/<group_id>/<script_id>/run', methods=['POST']) @app.route('/api/script-groups/<group_id>/config', methods=['PUT'])
def run_script(group_id, script_id): def update_group_config(group_id):
"""Execute a specific script""" """Update script group configuration"""
data = request.json
work_dir = data.get('work_dir')
profile = data.get('profile')
if not work_dir:
return jsonify({"error": "Work directory not specified"}), 400
if not profile:
return jsonify({"error": "Profile not specified"}), 400
try: try:
result = script_manager.execute_script(group_id, script_id, work_dir, profile) config = script_manager.update_group_data(group_id, request.json)
return jsonify(result) return jsonify(config)
except Exception as e:
return jsonify({"error": str(e)}), 500
# Work directory configuration endpoints
@app.route('/api/workdir-config/<path:work_dir>', methods=['GET'])
def get_workdir_config(work_dir):
"""Get work directory configuration"""
from core.workdir_config import WorkDirConfigManager
config_manager = WorkDirConfigManager(work_dir)
return jsonify(config_manager.get_config())
@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['GET'])
def get_group_config(work_dir, group_id):
"""Get group configuration from work directory"""
from core.workdir_config import WorkDirConfigManager
config_manager = WorkDirConfigManager(work_dir)
return jsonify(config_manager.get_group_config(group_id))
@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['PUT'])
def update_group_config(work_dir, group_id):
"""Update group configuration in work directory"""
from core.workdir_config import WorkDirConfigManager
config_manager = WorkDirConfigManager(work_dir)
try:
settings = request.json
config_manager.update_group_config(group_id, settings)
return jsonify({"status": "success"})
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
@app.route('/api/script-groups/<group_id>/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/<group_id>/scripts', methods=['GET']) @app.route('/api/script-groups/<group_id>/scripts', methods=['GET'])
def get_group_scripts(group_id): def get_group_scripts(group_id):
"""Get scripts for a specific group""" """Get scripts for a specific group"""
try: try:
print(f"Loading scripts for group: {group_id}") # Debug
scripts = script_manager.get_group_scripts(group_id) scripts = script_manager.get_group_scripts(group_id)
print(f"Scripts found: {scripts}") # Debug
return jsonify(scripts) return jsonify(scripts)
except Exception as e: except Exception as e:
print(f"Error loading scripts: {str(e)}") # Debug
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@app.route('/api/script-groups/<group_id>/config-schema', methods=['GET']) @app.route('/api/script-groups/<group_id>/schema', methods=['GET'])
def get_group_config_schema(group_id): def get_group_schema(group_id):
"""Get configuration schema for a script group""" """Get script group schema"""
try: try:
print(f"Loading config schema for group: {group_id}") # Debug schema = script_manager.get_global_schema()
schema = script_manager.get_group_config_schema(group_id)
print(f"Schema loaded: {schema}") # Debug
return jsonify(schema) return jsonify(schema)
except Exception as e: except Exception as e:
print(f"Error loading schema: {str(e)}") # Debug return jsonify({"error": str(e)}), 500
# Directory handling endpoints
@app.route('/api/select-directory', methods=['GET'])
def handle_select_directory():
"""Handle directory selection"""
result = select_directory()
if "error" in result:
return jsonify(result), 400
return jsonify(result)
# Work directory configuration endpoints
@app.route('/api/workdir-config/<group_id>', methods=['GET'])
def get_workdir_config(group_id):
"""Get work directory configuration for a group"""
try:
group_data = script_manager.get_group_data(group_id)
work_dir = group_data.get('work_dir')
if not work_dir:
return jsonify({"error": "Work directory not configured"}), 400
from core.workdir_config import WorkDirConfigManager
workdir_manager = WorkDirConfigManager(work_dir, group_id)
return jsonify(workdir_manager.get_group_config())
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/workdir-config/<group_id>', methods=['PUT'])
def update_workdir_config(group_id):
"""Update work directory configuration for a group"""
try:
group_data = script_manager.get_group_data(group_id)
work_dir = group_data.get('work_dir')
if not work_dir:
return jsonify({"error": "Work directory not configured"}), 400
from core.workdir_config import WorkDirConfigManager
workdir_manager = WorkDirConfigManager(work_dir, group_id)
workdir_manager.update_group_config(request.json)
return jsonify({"status": "success"})
except Exception as e:
return jsonify({"error": str(e)}), 400
# Script execution endpoint
@app.route('/api/script-groups/<group_id>/scripts/<script_id>/run', methods=['POST'])
def run_script(group_id, script_id):
"""Execute a specific script"""
try:
result = script_manager.execute_script(group_id, script_id, request.json.get('profile', {}))
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,297 @@
# backend/core/config_manager.py
from pathlib import Path
import json
from typing import Dict, Any, Optional, List
from datetime import datetime
class BaseConfigManager:
"""Base class for all configuration managers"""
def __init__(self, config_path: Path, schema_path: Optional[Path] = None):
self.config_path = Path(config_path)
self.schema_path = Path(schema_path) if schema_path else None
self._schema = None
self._config = None
def _load_schema(self) -> Dict[str, Any]:
"""Load configuration schema"""
if not self.schema_path or not self.schema_path.exists():
return {"config_schema": {}}
try:
with open(self.schema_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading schema: {e}")
return {"config_schema": {}}
def _load_config(self) -> Dict[str, Any]:
"""Load configuration data"""
if not self.config_path.exists():
return {}
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading config: {e}")
return {}
def _save_config(self, config: Dict[str, Any]):
"""Save configuration data"""
self.config_path.parent.mkdir(parents=True, exist_ok=True)
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
except Exception as e:
print(f"Error saving config: {e}")
raise
def _validate_config(self, config: Dict[str, Any], schema: Dict[str, Any]) -> Dict[str, Any]:
"""Validate configuration against schema"""
validated = {}
schema_fields = schema.get("config_schema", {})
for key, field_schema in schema_fields.items():
if key in config:
validated[key] = self._validate_field(key, config[key], field_schema)
elif field_schema.get("required", False):
raise ValueError(f"Required field '{key}' is missing")
else:
validated[key] = field_schema.get("default")
return validated
def _validate_field(self, key: str, value: Any, field_schema: Dict[str, Any]) -> Any:
"""Validate a single field value"""
field_type = field_schema.get("type")
if value is None or value == "":
if field_schema.get("required", False):
raise ValueError(f"Field '{key}' is required")
return field_schema.get("default")
try:
if field_type == "string":
return str(value)
elif field_type == "number":
return float(value) if "." in str(value) else int(value)
elif field_type == "boolean":
if isinstance(value, str):
return value.lower() == "true"
return bool(value)
elif field_type == "directory":
path = Path(value)
if not path.is_absolute():
path = Path(self.config_path).parent / path
path.mkdir(parents=True, exist_ok=True)
return str(path)
elif field_type == "select":
if value not in field_schema.get("options", []):
raise ValueError(f"Invalid option '{value}' for field '{key}'")
return value
else:
return value
except Exception as e:
raise ValueError(f"Invalid value for field '{key}': {str(e)}")
def get_schema(self) -> Dict[str, Any]:
"""Get configuration schema"""
if self._schema is None:
self._schema = self._load_schema()
return self._schema
def get_config(self) -> Dict[str, Any]:
"""Get current configuration"""
if self._config is None:
self._config = self._load_config()
return self._config
def update_schema(self, schema: Dict[str, Any]):
"""Update configuration schema"""
if not self.schema_path:
raise ValueError("No schema path configured")
try:
self.schema_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.schema_path, 'w', encoding='utf-8') as f:
json.dump(schema, f, indent=4)
self._schema = schema
except Exception as e:
print(f"Error saving schema: {e}")
raise
class ProfileManager(BaseConfigManager):
"""Manager for application profiles"""
DEFAULT_PROFILE = {
"id": "default",
"name": "Default Profile",
"llm_settings": {
"model": "gpt-4",
"temperature": 0.7,
"api_key": ""
}
}
def __init__(self, data_dir: Path):
super().__init__(
config_path=data_dir / "profiles.json",
schema_path=data_dir / "profile_schema.json"
)
self.profiles = self._load_profiles()
def _load_profiles(self) -> Dict[str, Dict]:
"""Load all profiles"""
if self.config_path.exists():
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
profiles = json.load(f)
if "default" not in profiles:
profiles["default"] = self._create_default_profile()
return profiles
except Exception as e:
print(f"Error loading profiles: {e}")
return {"default": self._create_default_profile()}
else:
profiles = {"default": self._create_default_profile()}
self._save_profiles(profiles)
return profiles
def _create_default_profile(self) -> Dict[str, Any]:
"""Create default profile"""
profile = self.DEFAULT_PROFILE.copy()
now = datetime.now().isoformat()
profile["created_at"] = now
profile["updated_at"] = now
return profile
def _save_profiles(self, profiles: Dict[str, Dict]):
"""Save all profiles"""
try:
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(profiles, f, indent=4)
except Exception as e:
print(f"Error saving profiles: {e}")
raise
def get_all_profiles(self) -> List[Dict[str, Any]]:
"""Get all profiles"""
return list(self.profiles.values())
def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]:
"""Get specific profile"""
return self.profiles.get(profile_id)
def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create new profile"""
if "id" not in profile_data:
raise ValueError("Profile must have an id")
profile_id = profile_data["id"]
if profile_id in self.profiles:
raise ValueError(f"Profile {profile_id} already exists")
# Add timestamps
now = datetime.now().isoformat()
profile_data["created_at"] = now
profile_data["updated_at"] = now
# Validate against schema
schema = self.get_schema()
validated_data = self._validate_config(profile_data, schema)
# Add to profiles
self.profiles[profile_id] = validated_data
self._save_profiles(self.profiles)
return validated_data
def update_profile(self, profile_id: str, profile_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update existing profile"""
if profile_id not in self.profiles:
raise ValueError(f"Profile {profile_id} not found")
if profile_id == "default" and "id" in profile_data:
raise ValueError("Cannot change id of default profile")
# Update timestamp
profile_data["updated_at"] = datetime.now().isoformat()
# Validate against schema
schema = self.get_schema()
validated_data = self._validate_config(profile_data, schema)
# Update profile
self.profiles[profile_id].update(validated_data)
self._save_profiles(self.profiles)
return self.profiles[profile_id]
def delete_profile(self, profile_id: str):
"""Delete profile"""
if profile_id == "default":
raise ValueError("Cannot delete default profile")
if profile_id not in self.profiles:
raise ValueError(f"Profile {profile_id} not found")
del self.profiles[profile_id]
self._save_profiles(self.profiles)
class ScriptGroupManager(BaseConfigManager):
"""Manager for script group configuration"""
def __init__(self, group_dir: Path):
super().__init__(
config_path=group_dir / "data.json",
schema_path=group_dir.parent / "config.json"
)
def get_work_dir(self) -> Optional[str]:
"""Get work directory from configuration"""
config = self.get_config()
return config.get("work_dir")
def update_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update configuration"""
# Add timestamp
config_data["updated_at"] = datetime.now().isoformat()
# Validate against schema
schema = self.get_schema()
validated_data = self._validate_config(config_data, schema)
# Save configuration
self._save_config(validated_data)
self._config = validated_data
return validated_data
class WorkDirConfigManager(BaseConfigManager):
"""Manager for work directory configuration"""
def __init__(self, work_dir: str, group_id: str):
self.work_dir = Path(work_dir)
self.group_id = group_id
super().__init__(
config_path=self.work_dir / "script_config.json",
schema_path=None # Schema is loaded from group configuration
)
def get_group_config(self) -> Dict[str, Any]:
"""Get configuration for current group"""
config = self.get_config()
return config.get("group_settings", {}).get(self.group_id, {})
def update_group_config(self, settings: Dict[str, Any]):
"""Update configuration for current group"""
config = self.get_config()
if "group_settings" not in config:
config["group_settings"] = {}
config["group_settings"][self.group_id] = settings
config["updated_at"] = datetime.now().isoformat()
self._save_config(config)
self._config = config

View File

@ -1,4 +1,3 @@
# backend/core/profile_manager.py
from pathlib import Path from pathlib import Path
import json import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
@ -6,44 +5,55 @@ from datetime import datetime
class ProfileManager: class ProfileManager:
"""Manages configuration profiles""" """Manages application profiles"""
DEFAULT_PROFILE = { DEFAULT_PROFILE = {
"id": "default", "id": "default",
"name": "Default Profile", "name": "Default Profile",
"llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""}, "llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""},
"created_at": "",
"updated_at": "",
} }
def __init__(self, data_dir: Path): def __init__(self, data_dir: Path):
self.data_dir = data_dir self.data_dir = data_dir
self.profiles_file = data_dir / "profiles.json" self.profiles_file = data_dir / "profiles.json"
self.schema_file = data_dir / "profile_schema.json"
self.profiles: Dict[str, Dict] = self._load_profiles() self.profiles: Dict[str, Dict] = self._load_profiles()
self._schema = self._load_schema()
def _load_schema(self) -> Dict[str, Any]:
"""Load profile schema"""
if self.schema_file.exists():
try:
with open(self.schema_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading profile schema: {e}")
return {"config_schema": {}}
def _load_profiles(self) -> Dict[str, Dict]: def _load_profiles(self) -> Dict[str, Dict]:
"""Load profiles from file""" """Load all profiles and ensure default profile exists"""
if self.profiles_file.exists(): profiles = {}
try:
# Crear perfil por defecto si no existe
default_profile = self._create_default_profile()
try:
if self.profiles_file.exists():
with open(self.profiles_file, "r", encoding="utf-8") as f: with open(self.profiles_file, "r", encoding="utf-8") as f:
profiles = json.load(f) loaded_profiles = json.load(f)
# Ensure default profile exists profiles.update(loaded_profiles)
if "default" not in profiles: except Exception as e:
profiles["default"] = self._create_default_profile() print(f"Error loading profiles: {e}")
return profiles
except Exception as e: # Asegurar que existe el perfil por defecto
print(f"Error loading profiles: {e}") if "default" not in profiles:
return {"default": self._create_default_profile()} profiles["default"] = default_profile
else:
# Create directory if it doesn't exist
self.profiles_file.parent.mkdir(parents=True, exist_ok=True)
# Create default profile
profiles = {"default": self._create_default_profile()}
self._save_profiles(profiles) self._save_profiles(profiles)
return profiles
return profiles
def _create_default_profile(self) -> Dict[str, Any]: def _create_default_profile(self) -> Dict[str, Any]:
"""Create default profile with timestamp""" """Create default profile"""
profile = self.DEFAULT_PROFILE.copy() profile = self.DEFAULT_PROFILE.copy()
now = datetime.now().isoformat() now = datetime.now().isoformat()
profile["created_at"] = now profile["created_at"] = now
@ -51,25 +61,87 @@ class ProfileManager:
return profile return profile
def _save_profiles(self, profiles: Optional[Dict] = None): def _save_profiles(self, profiles: Optional[Dict] = None):
"""Save profiles to file""" """Save all profiles"""
if profiles is None: if profiles is None:
profiles = self.profiles profiles = self.profiles
try: try:
print(f"Saving profiles to: {self.profiles_file}") # Agregar debug self.profiles_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.profiles_file, "w", encoding="utf-8") as f: with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(profiles, f, indent=4) json.dump(profiles, f, indent=4)
print("Profiles saved successfully") # Agregar debug
except Exception as e: except Exception as e:
print(f"Error saving profiles: {e}") # Agregar debug print(f"Error saving profiles: {e}")
raise # Re-lanzar la excepción para que se maneje arriba raise
def _validate_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
"""Validate profile data against schema"""
schema = self._schema.get("config_schema", {})
validated = {}
for key, field_schema in schema.items():
if key in profile_data:
validated[key] = self._validate_field(
key, profile_data[key], field_schema
)
elif field_schema.get("required", False):
raise ValueError(f"Required field '{key}' is missing")
else:
validated[key] = field_schema.get("default")
# Pass through non-schema fields
for key, value in profile_data.items():
if key not in schema:
validated[key] = value
return validated
def _validate_field(
self, key: str, value: Any, field_schema: Dict[str, Any]
) -> Any:
"""Validate a single field value"""
field_type = field_schema.get("type")
if value is None or value == "":
if field_schema.get("required", False):
raise ValueError(f"Field '{key}' is required")
return field_schema.get("default")
try:
if field_type == "string":
return str(value)
elif field_type == "number":
return float(value) if "." in str(value) else int(value)
elif field_type == "boolean":
if isinstance(value, str):
return value.lower() == "true"
return bool(value)
elif field_type == "select":
if value not in field_schema.get("options", []):
raise ValueError(f"Invalid option '{value}' for field '{key}'")
return value
else:
return value
except Exception as e:
raise ValueError(f"Invalid value for field '{key}': {str(e)}")
def get_all_profiles(self) -> List[Dict[str, Any]]: def get_all_profiles(self) -> List[Dict[str, Any]]:
"""Get all profiles""" """Get all profiles"""
return list(self.profiles.values()) return list(self.profiles.values())
def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]: def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]:
"""Get specific profile""" """Get specific profile with fallback to default"""
return self.profiles.get(profile_id) profile = self.profiles.get(profile_id)
if not profile and profile_id != "default":
# Si no se encuentra el perfil y no es el perfil por defecto,
# intentar retornar el perfil por defecto
profile = self.profiles.get("default")
if not profile:
# Si tampoco existe el perfil por defecto, crearlo
profile = self._create_default_profile()
self.profiles["default"] = profile
self._save_profiles(self.profiles)
return profile
def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create new profile""" """Create new profile"""
@ -85,41 +157,36 @@ class ProfileManager:
profile_data["created_at"] = now profile_data["created_at"] = now
profile_data["updated_at"] = now profile_data["updated_at"] = now
# Ensure required fields # Validate profile data
for key in ["name", "llm_settings"]: validated_data = self._validate_profile(profile_data)
if key not in profile_data:
profile_data[key] = self.DEFAULT_PROFILE[key]
self.profiles[profile_id] = profile_data # Save profile
self.profiles[profile_id] = validated_data
self._save_profiles() self._save_profiles()
return profile_data return validated_data
def update_profile( def update_profile(
self, profile_id: str, profile_data: Dict[str, Any] self, profile_id: str, profile_data: Dict[str, Any]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Update existing profile""" """Update existing profile"""
try: if profile_id not in self.profiles:
print(f"Updating profile {profile_id} with data: {profile_data}") raise ValueError(f"Profile {profile_id} not found")
if profile_id not in self.profiles:
raise ValueError(f"Profile {profile_id} not found")
if profile_id == "default" and "id" in profile_data: if profile_id == "default" and "id" in profile_data:
raise ValueError("Cannot change id of default profile") raise ValueError("Cannot change id of default profile")
# Update timestamp # Update timestamp
profile_data["updated_at"] = datetime.now().isoformat() profile_data["updated_at"] = datetime.now().isoformat()
# Update profile # Validate profile data
current_profile = self.profiles[profile_id].copy() # Hacer una copia validated_data = self._validate_profile(profile_data)
current_profile.update(profile_data) # Actualizar la copia
self.profiles[profile_id] = current_profile # Asignar la copia actualizada
print(f"Updated profile: {self.profiles[profile_id]}") # Update profile
self._save_profiles() current_profile = self.profiles[profile_id].copy()
return self.profiles[profile_id] current_profile.update(validated_data)
except Exception as e: self.profiles[profile_id] = current_profile
print(f"Error in update_profile: {e}") # Agregar debug self._save_profiles()
raise return current_profile
def delete_profile(self, profile_id: str): def delete_profile(self, profile_id: str):
"""Delete profile""" """Delete profile"""
@ -131,3 +198,18 @@ class ProfileManager:
del self.profiles[profile_id] del self.profiles[profile_id]
self._save_profiles() self._save_profiles()
def get_schema(self) -> Dict[str, Any]:
"""Get profile schema"""
return self._schema
def update_schema(self, schema: Dict[str, Any]):
"""Update profile schema"""
try:
self.schema_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.schema_file, "w", encoding="utf-8") as f:
json.dump(schema, f, indent=4)
self._schema = schema
except Exception as e:
print(f"Error saving schema: {e}")
raise

View File

@ -1,131 +1,163 @@
# backend/core/script_manager.py
from pathlib import Path from pathlib import Path
import importlib.util import importlib.util
import inspect import inspect
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional
import json import json
from .group_settings_manager import GroupSettingsManager # Agregar esta importación from datetime import datetime
class ScriptManager: class ScriptManager:
"""Manages script groups and their execution"""
def __init__(self, script_groups_dir: Path): def __init__(self, script_groups_dir: Path):
self.script_groups_dir = script_groups_dir self.script_groups_dir = script_groups_dir
self.group_settings = GroupSettingsManager(script_groups_dir) self._global_schema = self._load_global_schema()
def get_group_settings(self, group_id: str) -> Dict[str, Any]: def _load_global_schema(self) -> Dict[str, Any]:
"""Get settings for a script group""" """Load global configuration schema for script groups"""
return self.group_settings.get_group_settings(group_id) schema_file = self.script_groups_dir / "config.json"
try:
with open(schema_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading global schema: {e}")
return {"config_schema": {}}
def update_group_settings(self, group_id: str, settings: Dict[str, Any]): def _load_group_data(self, group_id: str) -> Dict[str, Any]:
"""Update settings for a script group""" """Load group data"""
return self.group_settings.update_group_settings(group_id, settings) data_file = self.script_groups_dir / group_id / "data.json"
try:
with open(data_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading group data: {e}")
return {}
def get_group_config_schema(self, group_id: str) -> Dict[str, Any]: def _save_group_data(self, group_id: str, data: Dict[str, Any]):
"""Get configuration schema for a script group""" """Save group data"""
config_file = self.script_groups_dir / group_id / "config.json" data_file = self.script_groups_dir / group_id / "data.json"
print(f"Looking for config file: {config_file}") # Debug try:
data_file.parent.mkdir(parents=True, exist_ok=True)
with open(data_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4)
except Exception as e:
print(f"Error saving group data: {e}")
raise
if config_file.exists(): def _validate_group_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
try: """Validate group data against schema"""
with open(config_file, "r", encoding="utf-8") as f: schema = self._global_schema.get("config_schema", {})
schema = json.load(f) validated = {}
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 for key, field_schema in schema.items():
return {"group_name": group_id, "description": "", "config_schema": {}} if key in data:
validated[key] = self._validate_field(key, data[key], field_schema)
elif field_schema.get("required", False):
raise ValueError(f"Required field '{key}' is missing")
else:
validated[key] = field_schema.get("default")
return validated
def _validate_field(self, key: str, value: Any, field_schema: Dict[str, Any]) -> Any:
"""Validate a single field value"""
field_type = field_schema.get("type")
if value is None or value == "":
if field_schema.get("required", False):
raise ValueError(f"Field '{key}' is required")
return field_schema.get("default")
try:
if field_type == "string":
return str(value)
elif field_type == "number":
return float(value) if "." in str(value) else int(value)
elif field_type == "boolean":
if isinstance(value, str):
return value.lower() == "true"
return bool(value)
elif field_type == "directory":
path = Path(value)
if not path.is_absolute():
path = self.script_groups_dir / path
path.mkdir(parents=True, exist_ok=True)
return str(path)
elif field_type == "select":
if value not in field_schema.get("options", []):
raise ValueError(f"Invalid option '{value}' for field '{key}'")
return value
else:
return value
except Exception as e:
raise ValueError(f"Invalid value for field '{key}': {str(e)}")
def get_available_groups(self) -> List[Dict[str, Any]]: def get_available_groups(self) -> List[Dict[str, Any]]:
"""Get list of available script groups""" """Get all available script groups"""
groups = [] groups = []
for group_dir in self.script_groups_dir.iterdir(): for group_dir in self.script_groups_dir.iterdir():
if group_dir.is_dir() and not group_dir.name.startswith("_"): if group_dir.is_dir() and not group_dir.name.startswith("_"):
groups.append( group_data = self._load_group_data(group_dir.name)
{ if group_data:
groups.append({
"id": group_dir.name, "id": group_dir.name,
"name": group_dir.name.replace("_", " ").title(), "name": group_data.get("name", group_dir.name),
"path": str(group_dir), "description": group_data.get("description", ""),
} "work_dir": group_data.get("work_dir", ""),
) "enabled": group_data.get("enabled", True)
})
return groups return groups
def get_group_data(self, group_id: str) -> Dict[str, Any]:
"""Get group configuration data"""
return self._load_group_data(group_id)
def update_group_data(self, group_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""Update group configuration data"""
# Validar datos
validated_data = self._validate_group_data(data)
# Actualizar timestamps
validated_data["updated_at"] = datetime.now().isoformat()
if not self._load_group_data(group_id):
validated_data["created_at"] = validated_data["updated_at"]
# Guardar datos
self._save_group_data(group_id, validated_data)
return validated_data
def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]:
"""Get scripts for a specific group""" """Get scripts for a specific group"""
group_dir = self.script_groups_dir / group_id 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(): 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") raise ValueError(f"Script group '{group_id}' not found")
scripts = [] scripts = []
for script_file in group_dir.glob("x[0-9].py"): 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) script_info = self._analyze_script(script_file)
if script_info: if script_info:
scripts.append(script_info) scripts.append(script_info)
return sorted(scripts, key=lambda x: x["id"]) return sorted(scripts, key=lambda x: x["id"])
def discover_groups(self) -> List[Dict[str, Any]]:
"""Discover all script groups"""
groups = []
for group_dir in self.script_groups_dir.iterdir():
if group_dir.is_dir() and not group_dir.name.startswith("_"):
group_info = self._analyze_group(group_dir)
if group_info:
groups.append(group_info)
return groups
def _analyze_group(self, group_dir: Path) -> Optional[Dict[str, Any]]:
"""Analyze a script group directory"""
scripts = []
for script_file in group_dir.glob("x[0-9].py"):
try:
script_info = self._analyze_script(script_file)
if script_info:
scripts.append(script_info)
except Exception as e:
print(f"Error analyzing script {script_file}: {e}")
if scripts:
return {
"id": group_dir.name,
"name": group_dir.name.replace("_", " ").title(),
"scripts": sorted(scripts, key=lambda x: x["id"]),
}
return None
def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]: def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]:
"""Analyze a single script file""" """Analyze a single script file"""
try: try:
# Import script module # Importar módulo del script
spec = importlib.util.spec_from_file_location(script_file.stem, script_file) spec = importlib.util.spec_from_file_location(script_file.stem, script_file)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
# Find script class # Encontrar la clase del script
script_class = None script_class = None
for name, obj in inspect.getmembers(module): for name, obj in inspect.getmembers(module):
if ( if (inspect.isclass(obj) and
inspect.isclass(obj) obj.__module__ == module.__name__ and
and obj.__module__ == module.__name__ hasattr(obj, "run")):
and hasattr(obj, "run")
):
script_class = obj script_class = obj
break break
if script_class: if script_class:
# Extraer la primera línea del docstring como nombre # Extraer nombre y descripción del docstring
docstring = inspect.getdoc(script_class) docstring = inspect.getdoc(script_class)
if docstring: if docstring:
name, *description = docstring.split("\n", 1) name, *description = docstring.split("\n", 1)
@ -138,44 +170,38 @@ class ScriptManager:
"id": script_file.stem, "id": script_file.stem,
"name": name.strip(), "name": name.strip(),
"description": description.strip(), "description": description.strip(),
"file": str(script_file.relative_to(self.script_groups_dir)), "file": str(script_file.relative_to(self.script_groups_dir))
} }
except Exception as e: except Exception as e:
print(f"Error loading script {script_file}: {e}") print(f"Error loading script {script_file}: {e}")
return None
return None def execute_script(self, group_id: str, script_id: str, profile: Dict[str, Any]) -> Dict[str, Any]:
def execute_script(
self, group_id: str, script_id: str, profile: Dict[str, Any]
) -> Dict[str, Any]:
"""Execute a specific script""" """Execute a specific script"""
# Get group settings first # Obtener datos del grupo
group_settings = self.group_settings.get_group_settings(group_id) group_data = self._load_group_data(group_id)
work_dir = group_settings.get("work_dir") work_dir = group_data.get("work_dir")
if not work_dir: if not work_dir:
raise ValueError(f"No work directory configured for group {group_id}") raise ValueError(f"No work directory configured for group {group_id}")
script_file = self.script_groups_dir / group_id / f"{script_id}.py" script_file = self.script_groups_dir / group_id / f"{script_id}.py"
if not script_file.exists(): if not script_file.exists():
raise ValueError(f"Script {script_id} not found in group {group_id}") raise ValueError(f"Script {script_id} not found in group {group_id}")
try: try:
# Import script module # Importar módulo del script
spec = importlib.util.spec_from_file_location(script_id, script_file) spec = importlib.util.spec_from_file_location(script_id, script_file)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
# Find and instantiate script class # Encontrar e instanciar la clase del script
script_class = None script_class = None
for name, obj in inspect.getmembers(module): for name, obj in inspect.getmembers(module):
if ( if (inspect.isclass(obj) and
inspect.isclass(obj) obj.__module__ == module.__name__ and
and obj.__module__ == module.__name__ hasattr(obj, "run")):
and hasattr(obj, "run")
):
script_class = obj script_class = obj
break break
@ -187,3 +213,7 @@ class ScriptManager:
except Exception as e: except Exception as e:
return {"status": "error", "error": str(e)} return {"status": "error", "error": str(e)}
def get_global_schema(self) -> Dict[str, Any]:
"""Get global configuration schema"""
return self._global_schema

View File

@ -2,6 +2,7 @@
from typing import Dict, Any from typing import Dict, Any
from pathlib import Path from pathlib import Path
import json import json
from datetime import datetime
class BaseScript: class BaseScript:
"""Base class for all scripts""" """Base class for all scripts"""
@ -19,8 +20,8 @@ class BaseScript:
""" """
raise NotImplementedError("Script must implement run method") raise NotImplementedError("Script must implement run method")
def get_config(self, work_dir: str, group_id: str) -> Dict[str, Any]: def get_work_config(self, work_dir: str, group_id: str) -> Dict[str, Any]:
"""Get group configuration from work directory""" """Get configuration from work directory"""
config_file = Path(work_dir) / "script_config.json" config_file = Path(work_dir) / "script_config.json"
if config_file.exists(): if config_file.exists():
@ -29,33 +30,49 @@ class BaseScript:
config = json.load(f) config = json.load(f)
return config.get("group_settings", {}).get(group_id, {}) return config.get("group_settings", {}).get(group_id, {})
except Exception as e: except Exception as e:
print(f"Error loading config: {e}") print(f"Error loading work directory config: {e}")
return {} return {}
def save_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]): def save_work_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]):
"""Save group configuration to work directory""" """Save configuration to work directory"""
config_file = Path(work_dir) / "script_config.json" config_file = Path(work_dir) / "script_config.json"
try: try:
# Load existing config or create new # Cargar configuración existente o crear nueva
if config_file.exists(): if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f: with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f) config = json.load(f)
else: else:
config = { config = {
"version": "1.0", "version": "1.0",
"group_settings": {} "group_settings": {},
"created_at": datetime.now().isoformat()
} }
# Update settings # Actualizar configuración
if "group_settings" not in config: if "group_settings" not in config:
config["group_settings"] = {} config["group_settings"] = {}
config["group_settings"][group_id] = settings config["group_settings"][group_id] = settings
config["updated_at"] = datetime.now().isoformat()
# Save config # Guardar configuración
with open(config_file, 'w', encoding='utf-8') as f: with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4) json.dump(config, f, indent=4)
except Exception as e: except Exception as e:
print(f"Error saving config: {e}") print(f"Error saving work directory config: {e}")
raise
def get_group_data(self, group_dir: Path) -> Dict[str, Any]:
"""Get group configuration data"""
data_file = group_dir / "data.json"
if data_file.exists():
try:
with open(data_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading group data: {e}")
return {}

View File

@ -2,29 +2,46 @@
"description": "Configuration schema for script groups", "description": "Configuration schema for script groups",
"config_schema": { "config_schema": {
"work_dir": { "work_dir": {
"type": "string", "type": "directory",
"description": "Working directory for this script group", "description": "Working directory for this script group",
"required": true "required": true
}, },
"name": {
"type": "string",
"description": "Display name for the script group",
"required": true
},
"description": { "description": {
"type": "string", "type": "string",
"description": "Description of this script group", "description": "Detailed description of the script group",
"default": "" "default": ""
}, },
"backup_dir": { "enabled": {
"type": "directory",
"description": "Backup directory path",
"default": ""
},
"max_files": {
"type": "number",
"description": "Maximum number of files to process",
"default": 1000
},
"enable_backup": {
"type": "boolean", "type": "boolean",
"description": "Enable automatic backups", "description": "Whether this script group is enabled",
"default": false "default": true
},
"execution_mode": {
"type": "select",
"description": "Execution mode for scripts in this group",
"options": ["sequential", "parallel"],
"default": "sequential"
},
"max_parallel": {
"type": "number",
"description": "Maximum number of parallel executions (if applicable)",
"default": 4
},
"timeout": {
"type": "number",
"description": "Script execution timeout in seconds",
"default": 3600
},
"logging": {
"type": "select",
"description": "Logging level for script execution",
"options": ["debug", "info", "warning", "error"],
"default": "info"
} }
} }
} }

View File

@ -0,0 +1,12 @@
{
"name": "System Analysis",
"description": "Scripts for system analysis and file management",
"work_dir": "",
"enabled": true,
"execution_mode": "sequential",
"max_parallel": 4,
"timeout": 3600,
"logging": "info",
"created_at": "2025-02-08T12:00:00.000Z",
"updated_at": "2025-02-08T12:00:00.000Z"
}

View File

@ -1 +0,0 @@
# backend/__init__.py

View File

@ -1 +0,0 @@
# backend/core/__init__.py

View File

@ -1 +0,0 @@
# backend/script_groups/__init__.py

View File

@ -1 +0,0 @@
# backend/script_groups/example_group/__init__.py

View File

@ -1,193 +0,0 @@
# backend/app.py
import os
import sys
from pathlib import Path
# Add the parent directory to Python path
backend_dir = Path(__file__).parent.parent # Sube un nivel más para incluir la carpeta raíz
if str(backend_dir) not in sys.path:
sys.path.append(str(backend_dir))
from flask import Flask, render_template, jsonify, request, send_from_directory
from core.directory_handler import select_directory
from core.script_manager import ScriptManager
from core.profile_manager import ProfileManager
app = Flask(__name__,
template_folder='../frontend/templates',
static_folder='../frontend/static')
# Initialize managers
data_dir = Path(__file__).parent.parent / 'data'
script_groups_dir = Path(__file__).parent / 'script_groups'
profile_manager = ProfileManager(data_dir)
script_manager = ScriptManager(script_groups_dir)
@app.route('/')
def index():
"""Render main page"""
return render_template('index.html')
# Profile endpoints
@app.route('/api/profiles', methods=['GET'])
def get_profiles():
"""Get all profiles"""
return jsonify(profile_manager.get_all_profiles())
@app.route('/api/profiles/<profile_id>', methods=['GET'])
def get_profile(profile_id):
"""Get specific profile"""
profile = profile_manager.get_profile(profile_id)
if profile:
return jsonify(profile)
return jsonify({"error": "Profile not found"}), 404
@app.route('/api/profiles', methods=['POST'])
def create_profile():
"""Create new profile"""
profile_data = request.json
try:
profile = profile_manager.create_profile(profile_data)
return jsonify(profile)
except Exception as e:
return jsonify({"error": str(e)}), 400
@app.route('/api/profiles/<profile_id>', methods=['PUT'])
def update_profile(profile_id):
"""Update existing profile"""
try:
profile_data = request.json
print(f"Received update request for profile {profile_id}: {profile_data}") # Debug
profile = profile_manager.update_profile(profile_id, profile_data)
print(f"Profile updated: {profile}") # Debug
return jsonify(profile)
except Exception as e:
print(f"Error updating profile: {e}") # Debug
return jsonify({"error": str(e)}), 400
@app.route('/api/profiles/<profile_id>', methods=['DELETE'])
def delete_profile(profile_id):
"""Delete profile"""
try:
profile_manager.delete_profile(profile_id)
return jsonify({"status": "success"})
except Exception as e:
return jsonify({"error": str(e)}), 400
@app.route('/api/script-groups', methods=['GET'])
def get_script_groups():
"""Get all available script groups"""
try:
groups = script_manager.get_available_groups()
return jsonify(groups)
except Exception as e:
return jsonify({"error": str(e)}), 500
# Directory handling endpoints
@app.route('/api/select-directory', methods=['GET'])
def handle_select_directory():
"""Handle directory selection"""
print("Handling directory selection request") # Debug
result = select_directory()
print(f"Directory selection result: {result}") # Debug
if "error" in result:
return jsonify(result), 400
return jsonify(result)
# Script management endpoints
@app.route('/api/scripts', methods=['GET'])
def get_scripts():
"""Get all available script groups"""
try:
groups = script_manager.discover_groups()
return jsonify(groups)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/scripts/<group_id>/<script_id>/run', methods=['POST'])
def run_script(group_id, script_id):
"""Execute a specific script"""
data = request.json
work_dir = data.get('work_dir')
profile = data.get('profile')
if not work_dir:
return jsonify({"error": "Work directory not specified"}), 400
if not profile:
return jsonify({"error": "Profile not specified"}), 400
try:
result = script_manager.execute_script(group_id, script_id, work_dir, profile)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
# Work directory configuration endpoints
@app.route('/api/workdir-config/<path:work_dir>', methods=['GET'])
def get_workdir_config(work_dir):
"""Get work directory configuration"""
from core.workdir_config import WorkDirConfigManager
config_manager = WorkDirConfigManager(work_dir)
return jsonify(config_manager.get_config())
@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['GET'])
def get_group_config(work_dir, group_id):
"""Get group configuration from work directory"""
from core.workdir_config import WorkDirConfigManager
config_manager = WorkDirConfigManager(work_dir)
return jsonify(config_manager.get_group_config(group_id))
@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['PUT'])
def update_group_config(work_dir, group_id):
"""Update group configuration in work directory"""
from core.workdir_config import WorkDirConfigManager
config_manager = WorkDirConfigManager(work_dir)
try:
settings = request.json
config_manager.update_group_config(group_id, settings)
return jsonify({"status": "success"})
except Exception as e:
return jsonify({"error": str(e)}), 400
@app.route('/api/script-groups/<group_id>/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/<group_id>/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/<group_id>/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)

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<!-- frontend/templates/base.html -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Local Scripts Web</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View File

@ -1,61 +0,0 @@
# backend/script_groups/base_script.py
from typing import Dict, Any
from pathlib import Path
import json
class BaseScript:
"""Base class for all scripts"""
def run(self, work_dir: str, profile: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute the script
Args:
work_dir (str): Working directory path
profile (Dict[str, Any]): Current profile configuration
Returns:
Dict[str, Any]: Execution results
"""
raise NotImplementedError("Script must implement run method")
def get_config(self, work_dir: str, group_id: str) -> Dict[str, Any]:
"""Get group configuration from work directory"""
config_file = Path(work_dir) / "script_config.json"
if config_file.exists():
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("group_settings", {}).get(group_id, {})
except Exception as e:
print(f"Error loading config: {e}")
return {}
def save_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]):
"""Save group configuration to work directory"""
config_file = Path(work_dir) / "script_config.json"
try:
# Load existing config or create new
if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
else:
config = {
"version": "1.0",
"group_settings": {}
}
# Update settings
if "group_settings" not in config:
config["group_settings"] = {}
config["group_settings"][group_id] = settings
# Save config
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
except Exception as e:
print(f"Error saving config: {e}")

View File

@ -1,172 +0,0 @@
# claude_file_organizer.py
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()

View File

@ -1,30 +0,0 @@
{
"description": "Configuration schema for script groups",
"config_schema": {
"work_dir": {
"type": "string",
"description": "Working directory for this script group",
"required": true
},
"description": {
"type": "string",
"description": "Description of this script group",
"default": ""
},
"backup_dir": {
"type": "directory",
"description": "Backup directory path",
"default": ""
},
"max_files": {
"type": "number",
"description": "Maximum number of files to process",
"default": 1000
},
"enable_backup": {
"type": "boolean",
"description": "Enable automatic backups",
"default": false
}
}
}

View File

@ -1,32 +0,0 @@
{
"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"
}
}
}

View File

@ -1,23 +0,0 @@
# backend/core/directory_handler.py
import os
from pathlib import Path
import tkinter as tk
from tkinter import filedialog
from flask import jsonify
def select_directory():
"""Show directory selection dialog and return selected path"""
root = tk.Tk()
root.withdraw()
root.attributes('-topmost', True) # Hace que el diálogo siempre esté encima
try:
directory = filedialog.askdirectory(
title="Select Work Directory",
initialdir=os.path.expanduser("~")
)
return {"path": directory} if directory else {"error": "No directory selected"}
except Exception as e:
return {"error": str(e)}
finally:
root.destroy()

View File

@ -1,122 +0,0 @@
# backend/core/group_settings_manager.py
from pathlib import Path
import json
from typing import Dict, Any
from datetime import datetime
import os
class GroupSettingsManager:
"""Manages settings for script groups"""
def __init__(self, script_groups_dir: Path):
self.script_groups_dir = script_groups_dir
self.config_schema = self._load_config_schema()
def _load_config_schema(self) -> Dict[str, Any]:
"""Load the main configuration schema for script groups"""
schema_file = self.script_groups_dir / "config.json"
try:
with open(schema_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading group config schema: {e}")
return {
"config_schema": {
"work_dir": {
"type": "string",
"description": "Working directory for this script group",
"required": True,
},
"description": {
"type": "string",
"description": "Description of this script group",
"default": "",
},
}
}
def _validate_setting(
self, key: str, value: Any, field_schema: Dict[str, Any]
) -> Any:
"""Validate and convert a single setting value"""
field_type = field_schema.get("type")
if value is None or value == "":
if field_schema.get("required", False):
raise ValueError(f"Field '{key}' is required")
return field_schema.get("default")
try:
if field_type == "string":
return str(value)
elif field_type == "number":
return float(value) if "." in str(value) else int(value)
elif field_type == "boolean":
if isinstance(value, str):
return value.lower() == "true"
return bool(value)
elif field_type == "directory":
path = Path(value)
if not path.is_absolute():
path = Path(self.script_groups_dir) / path
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
return str(path)
else:
return value
except Exception as e:
raise ValueError(f"Invalid value for field '{key}': {str(e)}")
def get_group_settings(self, group_id: str) -> Dict[str, Any]:
"""Get settings for a specific script group"""
settings_file = self.script_groups_dir / group_id / "group.json"
if settings_file.exists():
try:
with open(settings_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"Error loading group settings: {e}")
return {
"work_dir": "",
"description": "",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
}
def update_group_settings(self, group_id: str, settings: Dict[str, Any]):
"""Update settings for a specific script group"""
schema = self.config_schema.get("config_schema", {})
validated_settings = {}
# Validate each setting against schema
for key, field_schema in schema.items():
if key in settings:
validated_settings[key] = self._validate_setting(
key, settings[key], field_schema
)
elif field_schema.get("required", False):
raise ValueError(f"Required field '{key}' is missing")
else:
validated_settings[key] = field_schema.get("default")
# Add non-schema fields
for key, value in settings.items():
if key not in schema:
validated_settings[key] = value
# Update timestamps
validated_settings["updated_at"] = datetime.now().isoformat()
group_dir = self.script_groups_dir / group_id
settings_file = group_dir / "group.json"
if not settings_file.exists():
validated_settings["created_at"] = validated_settings["updated_at"]
group_dir.mkdir(parents=True, exist_ok=True)
# Save settings
with open(settings_file, "w", encoding="utf-8") as f:
json.dump(validated_settings, f, indent=4)

View File

@ -1,115 +0,0 @@
<!DOCTYPE html>
<!-- frontend/templates/index.html -->
<html lang="en" class="h-full bg-gray-50">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Local Scripts Web</title>
<!-- Tailwind y Alpine.js desde CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- HeroIcons -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.0.18/solid/index.min.js"></script>
</head>
<body class="h-full">
<div class="min-h-full">
<!-- Navbar -->
<nav class="bg-white shadow-sm">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
<div class="flex flex-shrink-0 items-center">
<h1 class="text-xl font-semibold text-gray-900">Local Scripts Web</h1>
</div>
</div>
<div class="flex items-center gap-4">
<select id="profileSelect"
class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600"
onchange="changeProfile()">
<option value="">Select Profile</option>
</select>
<button onclick="editProfile()"
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Edit Profile
</button>
<button onclick="newProfile()"
class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
New Profile
</button>
</div>
</div>
</div>
</nav>
<!-- Main content -->
<main>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<div class="space-y-6">
<!-- Work Directory Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Work Directory</h3>
<div class="mt-4 flex gap-4">
<input type="text" id="workDirPath" readonly
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<button onclick="selectWorkDir()"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Browse
</button>
</div>
</div>
</div>
<!-- Scripts Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Scripts</h3>
<div class="mt-4 space-y-4">
<select id="groupSelect"
class="w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<option value="">Select Script Group</option>
</select>
<div id="scriptList" class="hidden space-y-4">
<!-- Scripts will be loaded here -->
</div>
</div>
</div>
</div>
<!-- Output Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center">
<h3 class="text-base font-semibold leading-6 text-gray-900">Output</h3>
<button onclick="clearOutput()"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
Clear
</button>
</div>
<div id="outputArea"
class="mt-4 h-64 overflow-y-auto p-4 font-mono text-sm bg-gray-50 rounded-md border border-gray-200">
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Scripts -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/workdir_config.js') }}"></script>
<script src="{{ url_for('static', filename='js/profile.js') }}"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
<script src="{{ url_for('static', filename='js/modal.js') }}"></script>
<!-- Al final del body -->
<script>
// Initialización cuando la página carga
document.addEventListener('DOMContentLoaded', async () => {
console.log('DOM loaded, initializing...');
await initializeApp();
});
</script>
</body>
</html>

View File

@ -1,264 +0,0 @@
// frontend/static/js/main.js
// Global state
let currentProfile = null;
// Definir clases comunes para inputs
const STYLES = {
editableInput: "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
readonlyInput: "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm",
button: "px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600",
buttonSecondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300"
};
async function initializeApp() {
try {
console.log('Inicializando aplicación...');
// 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('Error al inicializar la aplicación:', error);
showError('Error al inicializar la aplicación');
}
}
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 = {}) {
try {
const response = await fetch(`/api${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error en la solicitud API');
}
return await response.json();
} catch (error) {
console.error('Error API:', error);
showError(error.message);
throw error;
}
}
async function loadProfiles() {
try {
const profiles = await apiRequest('/profiles');
updateProfileSelector(profiles);
// 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('Error al cargar los perfiles');
}
}
async function selectProfile(profileId) {
try {
console.log('Seleccionando perfil:', 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 al seleccionar perfil:', error);
showError('Error al cargar el perfil');
}
}
// 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 => `
<option value="${profile.id}" ${profile.id === lastProfileId ? 'selected' : ''}>
${profile.name}
</option>
`).join('');
// Asegurar que el valor seleccionado sea correcto
select.value = lastProfileId;
console.log('Set profileSelect value to:', lastProfileId);
}
async function changeProfile() {
const select = document.getElementById('profileSelect');
if (select.value) {
await selectProfile(select.value);
await loadScriptGroups(); // Reload scripts when profile changes
}
}
// Work directory functions
function updateWorkDirDisplay() {
const input = document.getElementById('workDirPath');
if (input && currentProfile) {
input.value = currentProfile.work_dir || '';
}
}
async function selectWorkDir() {
try {
console.log('Requesting directory selection...'); // Debug
const response = await apiRequest('/select-directory');
console.log('Directory selection response:', response); // Debug
if (response.path) {
console.log('Updating profile with new work_dir:', response.path); // Debug
const updateResponse = await apiRequest(`/profiles/${currentProfile.id}`, {
method: 'PUT',
body: JSON.stringify({
...currentProfile,
work_dir: response.path
})
});
console.log('Profile update response:', updateResponse); // Debug
await selectProfile(currentProfile.id);
showSuccess('Directorio de trabajo actualizado correctamente');
}
} catch (error) {
console.error('Error al seleccionar directorio:', error); // Debug
showError('Error al actualizar el directorio de trabajo');
}
}
// Output functions
function showError(message) {
const output = document.getElementById('outputArea');
const timestamp = new Date().toLocaleTimeString();
output.innerHTML += `\n[${timestamp}] ERROR: ${message}`;
output.scrollTop = output.scrollHeight;
}
function showSuccess(message) {
const output = document.getElementById('outputArea');
const timestamp = new Date().toLocaleTimeString();
output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`;
output.scrollTop = output.scrollHeight;
}
function clearOutput() {
const output = document.getElementById('outputArea');
output.innerHTML = '';
}
// Modal helper functions
function closeModal(button) {
const modal = button.closest('.modal');
if (modal) {
modal.remove();
}
}
// Global error handler
window.addEventListener('unhandledrejection', function(event) {
console.error('Unhandled promise rejection:', event.reason);
showError('An unexpected error occurred');
});
// Export functions for use in other modules
window.showError = showError;
window.showSuccess = showSuccess;
window.closeModal = closeModal;
window.currentProfile = currentProfile;

View File

@ -1,39 +0,0 @@
// frontend/static/js/modal.js
// 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 = `
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">${title}</h3>
</div>
<div class="px-6 py-4">
${content}
</div>
<div class="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end gap-3">
<button onclick="closeModal(this)"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Cancel
</button>
${onSave ? `
<button onclick="saveModal(this)"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Save
</button>
` : ''}
</div>
</div>
`;
document.body.appendChild(modal);
return modal;
}
function closeModal(button) {
const modal = button.closest('.fixed');
if (modal) {
modal.remove();
}
}

View File

@ -1,324 +0,0 @@
// frontend/static/js/profile.js
let selectedProfileId = localStorage.getItem('selectedProfileId') || 'default';
let editingProfile = null;
// Profile functions
async function loadProfiles() {
try {
const response = await apiRequest('/profiles');
const profiles = Object.values(response);
// Actualizar el selector manteniendo el valor seleccionado
const select = document.getElementById('profileSelect');
select.innerHTML = profiles.map(profile => `
<option value="${profile.id}">
${profile.name}
</option>
`).join('');
// Establecer el valor seleccionado después de actualizar las opciones
if (response[selectedProfileId]) {
select.value = selectedProfileId;
await selectProfile(selectedProfileId);
} else {
selectedProfileId = 'default';
select.value = 'default';
await selectProfile('default');
}
// Asegurarse de que el evento change no sobrescriba la selección
select.addEventListener('change', onProfileChange, { once: true });
} catch (error) {
showError('Error al cargar los perfiles');
}
}
async function selectProfile(profileId) {
try {
currentProfile = await apiRequest(`/profiles/${profileId}`);
updateWorkDirDisplay();
} catch (error) {
showError('Failed to load profile');
}
}
async function changeProfile() {
const select = document.getElementById('profileSelect');
await selectProfile(select.value);
}
async function selectWorkDir() {
try {
const response = await apiRequest('/select-directory');
if (response.path) {
await apiRequest(`/profiles/${currentProfile.id}`, {
method: 'PUT',
body: JSON.stringify({
...currentProfile,
work_dir: response.path
})
});
await selectProfile(currentProfile.id);
showSuccess('Work directory updated successfully');
}
} catch (error) {
showError('Failed to update work directory');
}
}
// Profile editor modal
function showProfileEditor(profile = null) {
editingProfile = profile;
const modal = document.createElement('div');
modal.className = 'modal active';
const editableInputClass = "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500";
const readonlyInputClass = "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm";
modal.innerHTML = `
<div class="modal-content">
<h2 class="text-xl font-bold mb-4">${profile ? 'Editar Perfil' : 'Nuevo Perfil'}</h2>
<form id="profileForm" onsubmit="saveProfile(event)">
<div class="form-group">
<label for="profileId" class="block text-sm font-medium text-gray-700">ID del Perfil</label>
<input type="text" id="profileId" name="id"
class="${profile ? readonlyInputClass : editableInputClass}"
value="${profile?.id || ''}"
${profile ? 'readonly' : ''}
required pattern="[a-zA-Z0-9_-]+"
title="Solo se permiten letras, números, guión bajo y guión">
</div>
<div class="form-group">
<label for="profileName" class="block text-sm font-medium text-gray-700">Nombre</label>
<input type="text" id="profileName" name="name"
class="${editableInputClass}"
value="${profile?.name || ''}" required>
</div>
<div class="form-group">
<label for="workDir" class="block text-sm font-medium text-gray-700">Directorio de Trabajo</label>
<input type="text" id="workDir" name="work_dir"
class="${readonlyInputClass}"
value="${profile?.work_dir || ''}" readonly>
</div>
<div class="form-group">
<label for="llmModel" class="block text-sm font-medium text-gray-700">LLM Model</label>
<select id="llmModel" name="llm_model"
class="${editableInputClass}">
<option value="gpt-4" ${profile?.llm_settings?.model === 'gpt-4' ? 'selected' : ''}>GPT-4</option>
<option value="gpt-3.5-turbo" ${profile?.llm_settings?.model === 'gpt-3.5-turbo' ? 'selected' : ''}>GPT-3.5 Turbo</option>
</select>
</div>
<div class="form-group">
<label for="apiKey" class="block text-sm font-medium text-gray-700">API Key</label>
<input type="password" id="apiKey" name="api_key"
class="${editableInputClass}"
value="${profile?.llm_settings?.api_key || ''}">
</div>
<div class="form-group">
<label for="temperature" class="block text-sm font-medium text-gray-700">Temperature</label>
<input type="number" id="temperature" name="temperature"
class="${editableInputClass}"
value="${profile?.llm_settings?.temperature || 0.7}"
min="0" max="2" step="0.1">
</div>
<div class="mt-4 flex justify-end space-x-3">
<button type="button" onclick="closeModal(this)"
class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancelar</button>
<button type="submit"
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Guardar</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
}
async function saveProfile(event) {
event.preventDefault();
const form = event.target;
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 {
if (editingProfile) {
await apiRequest(`/profiles/${editingProfile.id}`, {
method: 'PUT',
body: JSON.stringify(profileData)
});
} else {
await apiRequest('/profiles', {
method: 'POST',
body: JSON.stringify(profileData)
});
}
await loadProfiles();
closeModal(event.target);
showSuccess(`Perfil ${editingProfile ? 'actualizado' : 'creado'} correctamente`);
} catch (error) {
showError(`Error al ${editingProfile ? 'actualizar' : 'crear'} el perfil`);
}
}
// static/js/profile.js
async function editProfile() {
if (!currentProfile) {
showError('No profile selected');
return;
}
const content = `
<form id="profileForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Profile ID</label>
<input type="text" name="id" value="${currentProfile.id}"
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
${currentProfile.id === 'default' ? 'readonly' : ''}>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Name</label>
<input type="text" name="name" value="${currentProfile.name}"
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Work Directory</label>
<input type="text" name="work_dir" value="${currentProfile.work_dir}" readonly
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">LLM Model</label>
<select name="llm_model"
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="gpt-4" ${currentProfile.llm_settings?.model === 'gpt-4' ? 'selected' : ''}>GPT-4</option>
<option value="gpt-3.5-turbo" ${currentProfile.llm_settings?.model === 'gpt-3.5-turbo' ? 'selected' : ''}>GPT-3.5 Turbo</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">API Key</label>
<input type="password" name="api_key" value="${currentProfile.llm_settings?.api_key || ''}"
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Temperature</label>
<input type="number" name="temperature" value="${currentProfile.llm_settings?.temperature || 0.7}"
min="0" max="2" step="0.1"
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
</form>
`;
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 {
if (editingProfile) {
await apiRequest(`/profiles/${editingProfile.id}`, {
method: 'PUT',
body: JSON.stringify(profileData)
});
} else {
await apiRequest('/profiles', {
method: 'POST',
body: JSON.stringify(profileData)
});
}
await loadProfiles();
closeModal(modal);
showSuccess(`Perfil ${editingProfile ? 'actualizado' : 'creado'} correctamente`);
} catch (error) {
showError(`Error al ${editingProfile ? 'actualizar' : 'crear'} el perfil`);
}
}
function newProfile() {
const editableInputClass = "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500";
const readonlyInputClass = "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm";
const content = `
<form id="profileForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Profile ID</label>
<input type="text" name="id" required pattern="[a-zA-Z0-9_-]+"
class="${editableInputClass}"
title="Only letters, numbers, underscore and dash allowed">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Name</label>
<input type="text" name="name" required
class="${editableInputClass}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Work Directory</label>
<input type="text" name="work_dir" readonly
class="${readonlyInputClass}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">LLM Model</label>
<select name="llm_model"
class="${editableInputClass}">
<option value="gpt-4">GPT-4</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">API Key</label>
<input type="password" name="api_key"
class="${editableInputClass}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Temperature</label>
<input type="number" name="temperature" value="0.7"
min="0" max="2" step="0.1"
class="${editableInputClass}">
</div>
</form>
`;
const modal = createModal('New Profile', content, true);
editingProfile = null;
modal.querySelector('[onclick="saveModal(this)"]').onclick = async () => {
await saveProfile(modal);
};
}
async function onProfileChange(event) {
const newProfileId = event.target.value;
if (newProfileId !== selectedProfileId) {
selectedProfileId = newProfileId;
localStorage.setItem('selectedProfileId', selectedProfileId);
await selectProfile(selectedProfileId);
}
}

View File

@ -1,133 +0,0 @@
# backend/core/profile_manager.py
from pathlib import Path
import json
from typing import Dict, Any, List, Optional
from datetime import datetime
class ProfileManager:
"""Manages configuration profiles"""
DEFAULT_PROFILE = {
"id": "default",
"name": "Default Profile",
"llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""},
"created_at": "",
"updated_at": "",
}
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.profiles_file = data_dir / "profiles.json"
self.profiles: Dict[str, Dict] = self._load_profiles()
def _load_profiles(self) -> Dict[str, Dict]:
"""Load profiles from file"""
if self.profiles_file.exists():
try:
with open(self.profiles_file, "r", encoding="utf-8") as f:
profiles = json.load(f)
# Ensure default profile exists
if "default" not in profiles:
profiles["default"] = self._create_default_profile()
return profiles
except Exception as e:
print(f"Error loading profiles: {e}")
return {"default": self._create_default_profile()}
else:
# Create directory if it doesn't exist
self.profiles_file.parent.mkdir(parents=True, exist_ok=True)
# Create default profile
profiles = {"default": self._create_default_profile()}
self._save_profiles(profiles)
return profiles
def _create_default_profile(self) -> Dict[str, Any]:
"""Create default profile with timestamp"""
profile = self.DEFAULT_PROFILE.copy()
now = datetime.now().isoformat()
profile["created_at"] = now
profile["updated_at"] = now
return profile
def _save_profiles(self, profiles: Optional[Dict] = None):
"""Save profiles to file"""
if profiles is None:
profiles = self.profiles
try:
print(f"Saving profiles to: {self.profiles_file}") # Agregar debug
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(profiles, f, indent=4)
print("Profiles saved successfully") # Agregar debug
except Exception as e:
print(f"Error saving profiles: {e}") # Agregar debug
raise # Re-lanzar la excepción para que se maneje arriba
def get_all_profiles(self) -> List[Dict[str, Any]]:
"""Get all profiles"""
return list(self.profiles.values())
def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]:
"""Get specific profile"""
return self.profiles.get(profile_id)
def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create new profile"""
if "id" not in profile_data:
raise ValueError("Profile must have an id")
profile_id = profile_data["id"]
if profile_id in self.profiles:
raise ValueError(f"Profile {profile_id} already exists")
# Add timestamps
now = datetime.now().isoformat()
profile_data["created_at"] = now
profile_data["updated_at"] = now
# Ensure required fields
for key in ["name", "llm_settings"]:
if key not in profile_data:
profile_data[key] = self.DEFAULT_PROFILE[key]
self.profiles[profile_id] = profile_data
self._save_profiles()
return profile_data
def update_profile(
self, profile_id: str, profile_data: Dict[str, Any]
) -> Dict[str, Any]:
"""Update existing profile"""
try:
print(f"Updating profile {profile_id} with data: {profile_data}")
if profile_id not in self.profiles:
raise ValueError(f"Profile {profile_id} not found")
if profile_id == "default" and "id" in profile_data:
raise ValueError("Cannot change id of default profile")
# Update timestamp
profile_data["updated_at"] = datetime.now().isoformat()
# Update profile
current_profile = self.profiles[profile_id].copy() # Hacer una copia
current_profile.update(profile_data) # Actualizar la copia
self.profiles[profile_id] = current_profile # Asignar la copia actualizada
print(f"Updated profile: {self.profiles[profile_id]}")
self._save_profiles()
return self.profiles[profile_id]
except Exception as e:
print(f"Error in update_profile: {e}") # Agregar debug
raise
def delete_profile(self, profile_id: str):
"""Delete profile"""
if profile_id == "default":
raise ValueError("Cannot delete default profile")
if profile_id not in self.profiles:
raise ValueError(f"Profile {profile_id} not found")
del self.profiles[profile_id]
self._save_profiles()

View File

@ -1,26 +0,0 @@
{
"default": {
"id": "default",
"name": "Default Profile",
"work_dir": "",
"llm_settings": {
"model": "gpt-4",
"temperature": 0.7,
"api_key": ""
},
"created_at": "2025-02-07T12:47:49.766608",
"updated_at": "2025-02-07T12:47:49.766608"
},
"1": {
"id": "1",
"name": "Base",
"work_dir": "C:/Estudio",
"llm_settings": {
"api_key": "333333333333",
"model": "gpt-4",
"temperature": 0.7
},
"created_at": "2025-02-07T13:00:43.541932",
"updated_at": "2025-02-07T23:34:43.039269"
}
}

View File

@ -1,39 +0,0 @@
Estructura del proyecto original:
└── LocalScriptsWeb
├── backend
│ ├── core
│ │ ├── __init__.py
│ │ ├── directory_handler.py
│ │ ├── group_settings_manager.py
│ │ ├── profile_manager.py
│ │ ├── script_manager.py
│ │ └── workdir_config.py
│ ├── script_groups
│ │ ├── example_group
│ │ │ ├── __init__.py
│ │ │ ├── config.json
│ │ │ ├── x1.py
│ │ │ └── x2.py
│ │ ├── __init__.py
│ │ ├── base_script.py
│ │ └── config.json
│ ├── __init__.py
│ └── app.py
├── data
│ └── profiles.json
├── frontend
│ ├── static
│ │ ├── css
│ │ │ └── style.css
│ │ └── js
│ │ ├── main.js
│ │ ├── modal.js
│ │ ├── profile.js
│ │ ├── scripts.js
│ │ └── workdir_config.js
│ └── templates
│ ├── base.html
│ └── index.html
├── claude_file_organizer.py
└── files.txt

View File

@ -1,189 +0,0 @@
# backend/core/script_manager.py
from pathlib import Path
import importlib.util
import inspect
from typing import Dict, List, Any, Optional
import json
from .group_settings_manager import GroupSettingsManager # Agregar esta importación
class ScriptManager:
def __init__(self, script_groups_dir: Path):
self.script_groups_dir = script_groups_dir
self.group_settings = GroupSettingsManager(script_groups_dir)
def get_group_settings(self, group_id: str) -> Dict[str, Any]:
"""Get settings for a script group"""
return self.group_settings.get_group_settings(group_id)
def update_group_settings(self, group_id: str, settings: Dict[str, Any]):
"""Update settings for a script group"""
return self.group_settings.update_group_settings(group_id, settings)
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 = []
for group_dir in self.script_groups_dir.iterdir():
if group_dir.is_dir() and not group_dir.name.startswith("_"):
groups.append(
{
"id": group_dir.name,
"name": group_dir.name.replace("_", " ").title(),
"path": str(group_dir),
}
)
return groups
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"])
def discover_groups(self) -> List[Dict[str, Any]]:
"""Discover all script groups"""
groups = []
for group_dir in self.script_groups_dir.iterdir():
if group_dir.is_dir() and not group_dir.name.startswith("_"):
group_info = self._analyze_group(group_dir)
if group_info:
groups.append(group_info)
return groups
def _analyze_group(self, group_dir: Path) -> Optional[Dict[str, Any]]:
"""Analyze a script group directory"""
scripts = []
for script_file in group_dir.glob("x[0-9].py"):
try:
script_info = self._analyze_script(script_file)
if script_info:
scripts.append(script_info)
except Exception as e:
print(f"Error analyzing script {script_file}: {e}")
if scripts:
return {
"id": group_dir.name,
"name": group_dir.name.replace("_", " ").title(),
"scripts": sorted(scripts, key=lambda x: x["id"]),
}
return None
def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]:
"""Analyze a single script file"""
try:
# Import script module
spec = importlib.util.spec_from_file_location(script_file.stem, script_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find script class
script_class = None
for name, obj in inspect.getmembers(module):
if (
inspect.isclass(obj)
and obj.__module__ == module.__name__
and hasattr(obj, "run")
):
script_class = obj
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": name.strip(),
"description": description.strip(),
"file": str(script_file.relative_to(self.script_groups_dir)),
}
except Exception as e:
print(f"Error loading script {script_file}: {e}")
return None
def execute_script(
self, group_id: str, script_id: str, profile: Dict[str, Any]
) -> Dict[str, Any]:
"""Execute a specific script"""
# Get group settings first
group_settings = self.group_settings.get_group_settings(group_id)
work_dir = group_settings.get("work_dir")
if not work_dir:
raise ValueError(f"No work directory configured for group {group_id}")
script_file = self.script_groups_dir / group_id / f"{script_id}.py"
if not script_file.exists():
raise ValueError(f"Script {script_id} not found in group {group_id}")
try:
# Import script module
spec = importlib.util.spec_from_file_location(script_id, script_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Find and instantiate script class
script_class = None
for name, obj in inspect.getmembers(module):
if (
inspect.isclass(obj)
and obj.__module__ == module.__name__
and hasattr(obj, "run")
):
script_class = obj
break
if not script_class:
raise ValueError(f"No valid script class found in {script_id}")
script = script_class()
return script.run(work_dir, profile)
except Exception as e:
return {"status": "error", "error": str(e)}

View File

@ -1,815 +0,0 @@
// frontend/static/js/scripts.js
// Script groups state
let scriptGroups = [];
// Load script groups when page loads
document.addEventListener('DOMContentLoaded', async () => {
await loadScriptGroups();
});
// Load script groups when page loads
document.addEventListener('DOMContentLoaded', async () => {
await loadScriptGroups();
});
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 = `
<option value="">Seleccionar grupo...</option>
${groups.map(group => `
<option value="${group.id}" ${group.id === lastGroupId ? 'selected' : ''}>
${group.name}
</option>
`).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('Error al cargar grupos de scripts:', error);
showError('Error al cargar grupos de scripts');
}
}
// 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 = `
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-yellow-700">
Por favor, seleccione primero un directorio de trabajo
</p>
</div>
</div>
</div>
`;
scriptList.style.display = 'block';
return;
}
try {
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;
}
// 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 {
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 = `
<!-- Sección de Configuración -->
<div class="mb-6 bg-white shadow sm:rounded-lg">
<div class="border-b border-gray-200 p-4 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium text-gray-900">
${configSchema.group_name || 'Configuración'}
</h3>
<p class="mt-1 text-sm text-gray-500">${configSchema.description || ''}</p>
</div>
<button onclick="editConfigSchema('${groupId}')"
class="rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-900 hover:bg-gray-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Editar Esquema
</button>
</div>
<div class="p-4">
<form id="groupConfigForm" class="grid grid-cols-2 gap-4">
${Object.entries(configSchema.config_schema || {}).map(([key, field]) => `
<div class="space-y-2 col-span-2">
<label class="block text-sm font-medium text-gray-700">
${field.description}
</label>
${generateFormField(key, field, currentConfig[key])}
</div>
`).join('')}
<div class="col-span-2 flex justify-end pt-4">
<button type="submit"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Guardar Configuración
</button>
</div>
</form>
</div>
</div>
<!-- Lista de Scripts -->
<div class="space-y-4">
${groupScripts.map(script => `
<div class="bg-white px-4 py-3 rounded-md border border-gray-200 hover:border-gray-300 shadow sm:rounded-lg">
<div class="flex justify-between items-start">
<div>
<h4 class="text-sm font-medium text-gray-900">${script.name || script.id}</h4>
<p class="mt-1 text-sm text-gray-500">${script.description || 'Sin descripción disponible'}</p>
</div>
<button onclick="runScript('${groupId}', '${script.id}')"
class="ml-4 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Ejecutar
</button>
</div>
</div>
`).join('')}
</div>`;
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) {
console.error('Error in loadGroupScripts:', error);
showError('Failed to load scripts and configuration');
}
}
// Update script groups display
function updateScriptGroupsDisplay() {
const container = document.getElementById('scriptGroups');
if (!scriptGroups.length) {
container.innerHTML = '<p class="no-scripts">No script groups available</p>';
return;
}
container.innerHTML = scriptGroups.map(group => `
<div class="script-group" data-group-id="${group.id}">
<div class="script-group-header">
<h3>${group.name}</h3>
<button onclick="configureGroup('${group.id}')" class="config-btn">
Configure
</button>
</div>
<div class="script-list">
${group.scripts.map(script => `
<div class="script-item">
<div class="script-info">
<h4>${script.name}</h4>
<p>${script.description || 'No description available'}</p>
</div>
<div class="script-actions">
<button onclick="runScript('${group.id}', '${script.id}')" class="run-btn">
Run
</button>
</div>
</div>
`).join('')}
</div>
</div>
`).join('');
}
// Run a script
async function runScript(groupId, scriptId) {
if (!currentProfile?.work_dir) {
showError('Please select a work directory first');
return;
}
try {
const result = await apiRequest(`/scripts/${groupId}/${scriptId}/run`, {
method: 'POST',
body: JSON.stringify({
work_dir: currentProfile.work_dir,
profile: currentProfile
})
});
if (result.status === 'error') {
showError(result.error);
} else {
showSuccess(`Script ${scriptId} executed successfully`);
if (result.output) {
const output = document.getElementById('outputArea');
output.innerHTML += `\n[${new Date().toLocaleTimeString()}] ${result.output}`;
output.scrollTop = output.scrollHeight;
}
}
} catch (error) {
showError(`Failed to run script: ${error.message}`);
}
}
// Configure script group
async function configureGroup(groupId) {
if (!currentProfile?.work_dir) {
showError('Please select a work directory first');
return;
}
try {
const config = await getGroupConfig(groupId);
showGroupConfigEditor(groupId, config);
} catch (error) {
showError('Failed to load group configuration');
}
}
// Show group configuration editor
function showGroupConfigEditor(groupId, config) {
const group = scriptGroups.find(g => g.id === groupId);
if (!group) return;
const modal = document.createElement('div');
modal.className = 'modal active';
modal.innerHTML = `
<div class="modal-content">
<h2>${group.name} Configuration</h2>
<form id="groupConfigForm" onsubmit="saveGroupConfig(event, '${groupId}')">
<div class="form-group">
<label for="configData">Configuration (JSON)</label>
<textarea id="configData" name="config" rows="10"
class="config-editor">${JSON.stringify(config || {}, null, 2)}</textarea>
</div>
<div class="button-group">
<button type="button" onclick="closeModal(this)">Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
}
function generateFormField(key, field, currentValue) {
switch (field.type) {
case 'string':
return `
<input type="text"
name="${key}"
value="${currentValue || ''}"
class="${STYLES.editableInput}">
`;
case 'number':
return `
<input type="number"
name="${key}"
value="${currentValue || 0}"
class="${STYLES.editableInput}">
`;
case 'boolean':
return `
<select name="${key}" class="${STYLES.editableInput}">
<option value="true" ${currentValue ? 'selected' : ''}>Yes</option>
<option value="false" ${!currentValue ? 'selected' : ''}>No</option>
</select>
`;
case 'select':
return `
<select name="${key}" class="${STYLES.editableInput}">
${field.options.map(opt => `
<option value="${opt}" ${currentValue === opt ? 'selected' : ''}>
${opt}
</option>
`).join('')}
</select>
`;
default:
return `<input type="text" name="${key}" value="${currentValue || ''}" class="${STYLES.editableInput}">`;
}
}
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 = `
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div class="flex">
<div class="ml-3">
<p class="text-sm text-yellow-700">
Por favor, seleccione primero un directorio de trabajo
</p>
</div>
</div>
</div>
`;
scriptList.style.display = 'block';
return;
}
try {
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 = `
<!-- Sección de Configuración -->
<div class="mb-6 bg-white shadow sm:rounded-lg">
<div class="border-b border-gray-200 p-4 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium text-gray-900">
${configSchema.group_name || 'Configuración'}
</h3>
<p class="mt-1 text-sm text-gray-500">${configSchema.description || ''}</p>
</div>
<button onclick="editConfigSchema('${groupId}')"
class="rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-900 hover:bg-gray-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Editar Esquema
</button>
</div>
<div class="p-4">
<form id="groupConfigForm" class="grid grid-cols-2 gap-4">
${Object.entries(configSchema.config_schema || {}).map(([key, field]) => `
<div class="space-y-2 col-span-2">
<label class="block text-sm font-medium text-gray-700">
${field.description}
</label>
${generateFormField(key, field, currentConfig[key])}
</div>
`).join('')}
<div class="col-span-2 flex justify-end pt-4">
<button type="submit"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Guardar Configuración
</button>
</div>
</form>
</div>
</div>
<!-- Lista de Scripts -->
<div class="space-y-4">
${groupScripts.map(script => `
<div class="bg-white px-4 py-3 rounded-md border border-gray-200 hover:border-gray-300 shadow sm:rounded-lg">
<div class="flex justify-between items-start">
<div>
<h4 class="text-sm font-medium text-gray-900">${script.name || script.id}</h4>
<p class="mt-1 text-sm text-gray-500">${script.description || 'Sin descripción disponible'}</p>
</div>
<button onclick="runScript('${groupId}', '${script.id}')"
class="ml-4 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Ejecutar
</button>
</div>
</div>
`).join('')}
</div>`;
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) {
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 = `
<div class="border-b border-gray-200 p-4 flex justify-between items-center bg-gray-50">
<h3 class="text-lg font-medium text-gray-900">Editar Configuración del Esquema</h3>
<button onclick="this.closest('#schemaEditor').remove()"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Cerrar Editor
</button>
</div>
<div class="p-4">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Nombre del Grupo</label>
<input type="text" name="group_name" value="${schema.group_name}"
class="${STYLES.editableInput}">
</div>
<div class="text-right">
<button onclick="addParameter(this)"
class="mt-6 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500">
Agregar Parámetro
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Descripción</label>
<input type="text" name="description" value="${schema.description}"
class="${STYLES.editableInput}">
</div>
<div id="parameters" class="space-y-2">
<div class="flex justify-between items-center">
<h4 class="font-medium text-gray-900">Parámetros</h4>
</div>
${Object.entries(schema.config_schema).map(([key, param]) => `
<div class="parameter-item bg-gray-50 p-4 rounded-md relative">
<button onclick="removeParameter(this)"
class="absolute top-2 right-2 text-red-600 hover:text-red-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Parameter Name</label>
<input type="text" name="param_name" value="${key}"
class="${STYLES.editableInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Type</label>
<select name="param_type"
onchange="handleTypeChange(this)"
class="${STYLES.editableInput}">
<option value="string" ${param.type === 'string' ? 'selected' : ''}>String</option>
<option value="number" ${param.type === 'number' ? 'selected' : ''}>Number</option>
<option value="boolean" ${param.type === 'boolean' ? 'selected' : ''}>Boolean</option>
<option value="select" ${param.type === 'select' ? 'selected' : ''}>Select</option>
</select>
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700">Description</label>
<input type="text" name="param_description" value="${param.description}"
class="${STYLES.editableInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Default Value</label>
<input type="text" name="param_default" value="${param.default}"
class="${STYLES.editableInput}">
</div>
${param.type === 'select' ? `
<div>
<label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label>
<input type="text" name="param_options" value="${param.options.join(', ')}"
class="${STYLES.editableInput}">
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button onclick="this.closest('#schemaEditor').remove()"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Cancelar
</button>
<button onclick="saveConfigSchema('${groupId}', this.closest('#schemaEditor'))"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Guardar Cambios
</button>
</div>
</div>
</div>
`;
// 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('Error al cargar el esquema de configuración');
}
}
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 = `
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">${title}</h3>
</div>
<div class="px-6 py-4 overflow-y-auto flex-1">
${content}
</div>
<div class="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end gap-3 border-t border-gray-200">
<button onclick="closeModal(this)"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Cancelar
</button>
${onSave ? `
<button onclick="saveModal(this)"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Guardar
</button>
` : ''}
</div>
</div>
`;
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 = `
<button onclick="removeParameter(this)"
class="absolute top-2 right-2 text-red-600 hover:text-red-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Parameter Name</label>
<input type="text" name="param_name" value=""
class="${STYLES.editableInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Type</label>
<select name="param_type"
onchange="handleTypeChange(this)"
class="${STYLES.editableInput}">
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="select">Select</option>
</select>
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700">Description</label>
<input type="text" name="param_description" value=""
class="${STYLES.editableInput}">
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700">Default Value</label>
<input type="text" name="param_default" value=""
class="${STYLES.editableInput}">
</div>
</div>
`;
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 = `
<label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label>
<input type="text" name="param_options" value=""
class="${STYLES.editableInput}">
`;
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');
}
}
function showScriptForm(script) {
const modal = document.createElement('div');
modal.className = 'modal active';
modal.innerHTML = `
<div class="modal-content">
<h2 class="text-xl font-bold mb-4">${script.name}</h2>
<form id="scriptForm" class="space-y-4">
<div class="form-group">
<label class="block text-sm font-medium text-gray-700">Parameters</label>
<textarea name="parameters" rows="4"
class="${STYLES.editableInput}"
placeholder="Enter script parameters (optional)"></textarea>
</div>
<div class="mt-4 flex justify-end space-x-3">
<button type="button" onclick="closeModal(this)"
class="${STYLES.buttonSecondary}">Cancel</button>
<button type="submit"
class="${STYLES.button}">Run</button>
</div>
</form>
<div id="scriptOutput" class="mt-4 hidden">
<h3 class="font-bold mb-2">Output:</h3>
<pre class="output-area p-4 bg-gray-100 rounded"></pre>
</div>
</div>
`;
document.body.appendChild(modal);
}

View File

@ -1,31 +0,0 @@
/* frontend/static/css/style.css */
/* Solo mantenemos estilos específicos que no podemos lograr fácilmente con Tailwind */
.output-area {
white-space: pre-wrap;
word-wrap: break-word;
}
/* Estilos para modales que no se pueden lograr fácilmente con Tailwind */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 0.5rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}

View File

@ -1,161 +0,0 @@
// frontend/static/js/workdir_config.js
async function getWorkDirConfig() {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return null;
}
try {
return await apiRequest(`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}`);
} catch (error) {
showError('Error al cargar la configuración del directorio de trabajo');
return null;
}
}
async function getGroupConfig(groupId) {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return null;
}
try {
return await apiRequest(
`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`
);
} catch (error) {
showError('Error al cargar la configuración del grupo');
return null;
}
}
async function updateGroupConfig(groupId, settings) {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return false;
}
try {
await apiRequest(
`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`,
{
method: 'PUT',
body: JSON.stringify(settings)
}
);
showSuccess('Group configuration updated successfully');
return true;
} catch (error) {
showError('Failed to update group configuration');
return false;
}
}
function showConfigEditor(config, schema) {
const modal = document.createElement('div');
modal.className = 'modal active';
const formContent = Object.entries(schema).map(([key, field]) => `
<div class="form-group">
<label class="block text-sm font-medium text-gray-700">${field.description || key}</label>
${getInputByType(key, field, config[key])}
</div>
`).join('');
modal.innerHTML = `
<div class="modal-content">
<h2 class="text-xl font-bold mb-4">Work Directory Configuration</h2>
<form id="configForm" class="space-y-4">
${formContent}
<div class="mt-4 flex justify-end space-x-3">
<button type="button" onclick="closeModal(this)"
class="${STYLES.buttonSecondary}">Cancel</button>
<button type="submit"
class="${STYLES.button}">Save</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
}
function getInputByType(key, field, value) {
switch (field.type) {
case 'select':
return `
<select name="${key}"
class="${STYLES.editableInput}">
${field.options.map(opt => `
<option value="${opt}" ${value === opt ? 'selected' : ''}>
${opt}
</option>
`).join('')}
</select>`;
case 'boolean':
return `
<select name="${key}"
class="${STYLES.editableInput}">
<option value="true" ${value ? 'selected' : ''}>Yes</option>
<option value="false" ${!value ? 'selected' : ''}>No</option>
</select>`;
case 'number':
return `
<input type="number" name="${key}"
value="${value || field.default || ''}"
class="${STYLES.editableInput}">`;
default:
return `
<input type="text" name="${key}"
value="${value || field.default || ''}"
class="${STYLES.editableInput}">`;
}
}
// static/js/workdir_config.js
async function showWorkDirConfig() {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return;
}
try {
const config = await getWorkDirConfig();
const content = `
<div class="space-y-4">
<div>
<h4 class="text-sm font-medium text-gray-900">Directory</h4>
<p class="mt-1 text-sm text-gray-500">${currentProfile.work_dir}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-900">Version</h4>
<p class="mt-1 text-sm text-gray-500">${config.version}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-900">Group Configurations</h4>
<div class="mt-2 space-y-3">
${Object.entries(config.group_settings || {}).map(([groupId, settings]) => `
<div class="rounded-md bg-gray-50 p-3">
<h5 class="text-sm font-medium text-gray-900">${groupId}</h5>
<pre class="mt-2 text-xs text-gray-500">${JSON.stringify(settings, null, 2)}</pre>
</div>
`).join('')}
</div>
</div>
</div>
`;
createModal('Work Directory Configuration', content);
} catch (error) {
showError('Error al cargar la configuración del directorio de trabajo');
}
}
function closeModal(button) {
const modal = button.closest('.modal');
if (modal) {
modal.remove();
}
}

View File

@ -1,72 +0,0 @@
# backend/core/workdir_config.py
from pathlib import Path
import json
from typing import Dict, Any, Optional
from datetime import datetime
class WorkDirConfigManager:
"""Manages configuration files in work directories"""
DEFAULT_CONFIG = {
"version": "1.0",
"created_at": "",
"updated_at": "",
"group_settings": {}
}
def __init__(self, work_dir: str):
self.work_dir = Path(work_dir)
self.config_file = self.work_dir / "script_config.json"
def get_config(self) -> Dict[str, Any]:
"""Get configuration for work directory"""
if self.config_file.exists():
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading work dir config: {e}")
return self._create_default_config()
return self._create_default_config()
def _create_default_config(self) -> Dict[str, Any]:
"""Create default configuration"""
config = self.DEFAULT_CONFIG.copy()
now = datetime.now().isoformat()
config["created_at"] = now
config["updated_at"] = now
return config
def save_config(self, config: Dict[str, Any]):
"""Save configuration to file"""
# Ensure work directory exists
self.work_dir.mkdir(parents=True, exist_ok=True)
# Update timestamp
config["updated_at"] = datetime.now().isoformat()
# Save config
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
def get_group_config(self, group_id: str) -> Dict[str, Any]:
"""Get configuration for specific script group"""
config = self.get_config()
return config["group_settings"].get(group_id, {})
def update_group_config(self, group_id: str, settings: Dict[str, Any]):
"""Update configuration for specific script group"""
config = self.get_config()
if "group_settings" not in config:
config["group_settings"] = {}
config["group_settings"][group_id] = settings
self.save_config(config)
def remove_group_config(self, group_id: str):
"""Remove configuration for specific script group"""
config = self.get_config()
if group_id in config.get("group_settings", {}):
del config["group_settings"][group_id]
self.save_config(config)

View File

@ -1,108 +0,0 @@
# backend/script_groups/example_group/x1.py
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):
"""
File Analysis
Analyzes files in directory with configurable filters and reporting
"""
def run(self, work_dir: str, profile: dict) -> dict:
try:
# Get configuration
config = self.get_config(work_dir, "example_group")
# 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):
# Skip excluded directories
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
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": 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())
)
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}

View File

@ -1,104 +0,0 @@
# backend/script_groups/example_group/x2.py
from backend.script_groups.base_script import BaseScript
import psutil
import json
from datetime import datetime
from pathlib import Path
class SystemInfo(BaseScript):
"""
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(),
"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": memory.total,
"available": memory.available,
"used": memory.used,
"percent": memory.percent
},
"disk": {
"total": disk.total,
"used": disk.used,
"free": disk.free,
"percent": disk.percent
},
"network": {
"interfaces": list(psutil.net_if_addrs().keys()),
"connections": len(psutil.net_connections())
}
}
# 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_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",
"data": info,
"output": output
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}

39
data/profile_schema.json Normal file
View File

@ -0,0 +1,39 @@
{
"description": "Configuration schema for application profiles",
"config_schema": {
"id": {
"type": "string",
"description": "Unique identifier for the profile",
"required": true
},
"name": {
"type": "string",
"description": "Display name for the profile",
"required": true
},
"llm_settings": {
"type": "object",
"description": "Language model settings",
"properties": {
"model": {
"type": "select",
"description": "Language model to use",
"options": ["gpt-4", "gpt-3.5-turbo"],
"default": "gpt-4"
},
"temperature": {
"type": "number",
"description": "Temperature for text generation",
"default": 0.7,
"min": 0,
"max": 2
},
"api_key": {
"type": "string",
"description": "API key for the language model",
"default": ""
}
}
}
}
}

View File

@ -2,25 +2,23 @@
"default": { "default": {
"id": "default", "id": "default",
"name": "Default Profile", "name": "Default Profile",
"work_dir": "",
"llm_settings": { "llm_settings": {
"model": "gpt-4", "model": "gpt-4",
"temperature": 0.7, "temperature": 0.7,
"api_key": "" "api_key": ""
}, },
"created_at": "2025-02-07T12:47:49.766608", "created_at": "2025-02-08T12:00:00.000Z",
"updated_at": "2025-02-07T12:47:49.766608" "updated_at": "2025-02-08T12:00:00.000Z"
}, },
"1": { "1": {
"id": "1", "id": "1",
"name": "Base", "name": "Base",
"work_dir": "C:/Estudio",
"llm_settings": { "llm_settings": {
"api_key": "333333333333", "api_key": "333333333333",
"model": "gpt-4", "model": "gpt-4",
"temperature": 0.7 "temperature": 0.7
}, },
"created_at": "2025-02-07T13:00:43.541932", "created_at": "2025-02-08T13:00:43.541932",
"updated_at": "2025-02-07T23:34:43.039269" "updated_at": "2025-02-08T23:34:43.039269"
} }
} }

View File

@ -1,12 +1,12 @@
/* frontend/static/css/style.css */ /* frontend/static/css/style.css */
/* Solo mantenemos estilos específicos que no podemos lograr fácilmente con Tailwind */ /* Estilos para el área de salida que requiere white-space específico */
.output-area { .output-area {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
/* Estilos para modales que no se pueden lograr fácilmente con Tailwind */ /* Estilos para modales - solo lo que no se puede hacer con Tailwind */
.modal { .modal {
position: fixed; position: fixed;
top: 0; top: 0;
@ -21,11 +21,6 @@
} }
.modal-content { .modal-content {
background: white;
padding: 2rem;
border-radius: 0.5rem;
max-width: 600px;
width: 90%;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
} }

View File

@ -1,9 +1,10 @@
// frontend/static/js/main.js // frontend/static/js/main.js
// Global state // Estado global
let currentProfile = null; let currentProfile = null;
let currentGroup = null;
// Definir clases comunes para inputs // Estilos comunes
const STYLES = { const STYLES = {
editableInput: "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500", editableInput: "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
readonlyInput: "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm", readonlyInput: "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm",
@ -11,9 +12,10 @@ const STYLES = {
buttonSecondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300" buttonSecondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300"
}; };
// Inicialización de la aplicación
async function initializeApp() { async function initializeApp() {
try { try {
console.log('Inicializando aplicación...'); console.log('Initializing application...');
// Cargar perfiles // Cargar perfiles
const profiles = await apiRequest('/profiles'); const profiles = await apiRequest('/profiles');
@ -33,66 +35,16 @@ async function initializeApp() {
await selectProfile(selectedProfile.id); await selectProfile(selectedProfile.id);
} }
// Cargar grupos de scripts y restaurar la última selección // Restaurar último estado
await restoreScriptGroup(); await restoreLastState();
// Actualizar la interfaz
updateWorkDirDisplay();
} catch (error) { } catch (error) {
console.error('Error al inicializar la aplicación:', error); console.error('Error initializing application:', error);
showError('Error al inicializar la aplicación'); showError('Error initializing application');
} }
} }
async function restoreScriptGroup() { // Funciones de API
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 = {}) { async function apiRequest(endpoint, options = {}) {
try { try {
const response = await fetch(`/api${endpoint}`, { const response = await fetch(`/api${endpoint}`, {
@ -105,127 +57,110 @@ async function apiRequest(endpoint, options = {}) {
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.error || 'Error en la solicitud API'); throw new Error(error.error || 'API request error');
} }
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error('Error API:', error); console.error('API Error:', error);
showError(error.message); showError(error.message);
throw error; throw error;
} }
} }
async function loadProfiles() { // Funciones de gestión de perfiles
try {
const profiles = await apiRequest('/profiles');
updateProfileSelector(profiles);
// 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('Error al cargar los perfiles');
}
}
async function selectProfile(profileId) { async function selectProfile(profileId) {
try { try {
console.log('Seleccionando perfil:', profileId); console.log('Selecting profile:', profileId);
// Cargar perfil
currentProfile = await apiRequest(`/profiles/${profileId}`); currentProfile = await apiRequest(`/profiles/${profileId}`);
// Guardar en localStorage // Guardar en localStorage
localStorage.setItem('lastProfileId', profileId); localStorage.setItem('lastProfileId', profileId);
console.log('Profile ID saved to storage:', profileId); console.log('Profile ID saved:', profileId);
// Actualizar explícitamente el valor del combo // Actualizar UI
const select = document.getElementById('profileSelect'); updateProfileSelector([currentProfile]);
if (select) { updateProfileDisplay();
select.value = profileId;
console.log('Updated profileSelect value to:', profileId);
}
updateWorkDirDisplay(); return currentProfile;
// Recargar scripts con el último grupo seleccionado
await restoreScriptGroup();
} catch (error) { } catch (error) {
console.error('Error al seleccionar perfil:', error); console.error('Error selecting profile:', error);
showError('Error al cargar el perfil'); showError('Error loading profile');
throw error;
} }
} }
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initializeApp);
function updateProfileSelector(profiles) { function updateProfileSelector(profiles) {
const select = document.getElementById('profileSelect'); const select = document.getElementById('profileSelect');
if (!select) return;
const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; const lastProfileId = localStorage.getItem('lastProfileId') || 'default';
console.log('Updating profile selector. Last profile ID:', lastProfileId);
// Construir las opciones
select.innerHTML = profiles.map(profile => ` select.innerHTML = profiles.map(profile => `
<option value="${profile.id}" ${profile.id === lastProfileId ? 'selected' : ''}> <option value="${profile.id}" ${profile.id === lastProfileId ? 'selected' : ''}>
${profile.name} ${profile.name}
</option> </option>
`).join(''); `).join('');
// Asegurar que el valor seleccionado sea correcto
select.value = lastProfileId;
console.log('Set profileSelect value to:', lastProfileId);
} }
async function changeProfile() { function updateProfileDisplay() {
const select = document.getElementById('profileSelect'); const container = document.getElementById('profileConfig');
if (select.value) { if (!container || !currentProfile) return;
await selectProfile(select.value);
await loadScriptGroups(); // Reload scripts when profile changes container.innerHTML = `
} <div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Profile ID</label>
<input type="text" value="${currentProfile.id}" readonly
class="${STYLES.readonlyInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Name</label>
<input type="text" value="${currentProfile.name}" readonly
class="${STYLES.readonlyInput}">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Model</label>
<input type="text" value="${currentProfile.llm_settings?.model || ''}" readonly
class="${STYLES.readonlyInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Temperature</label>
<input type="text" value="${currentProfile.llm_settings?.temperature || ''}" readonly
class="${STYLES.readonlyInput}">
</div>
</div>
</div>
`;
} }
// Work directory functions // Funciones de estado
function updateWorkDirDisplay() { async function restoreLastState() {
const input = document.getElementById('workDirPath');
if (input && currentProfile) {
input.value = currentProfile.work_dir || '';
}
}
async function selectWorkDir() {
try { try {
console.log('Requesting directory selection...'); // Debug // Restaurar último grupo seleccionado
const response = await apiRequest('/select-directory'); const lastGroupId = localStorage.getItem('lastGroupId');
console.log('Directory selection response:', response); // Debug if (lastGroupId) {
const groupSelect = document.getElementById('groupSelect');
if (response.path) { if (groupSelect) {
console.log('Updating profile with new work_dir:', response.path); // Debug groupSelect.value = lastGroupId;
const updateResponse = await apiRequest(`/profiles/${currentProfile.id}`, { await handleGroupChange({ target: { value: lastGroupId } });
method: 'PUT', }
body: JSON.stringify({
...currentProfile,
work_dir: response.path
})
});
console.log('Profile update response:', updateResponse); // Debug
await selectProfile(currentProfile.id);
showSuccess('Directorio de trabajo actualizado correctamente');
} }
} catch (error) { } catch (error) {
console.error('Error al seleccionar directorio:', error); // Debug console.error('Error restoring state:', error);
showError('Error al actualizar el directorio de trabajo');
} }
} }
// Output functions // Funciones de utilidad UI
function showError(message) { function showError(message) {
const output = document.getElementById('outputArea'); const output = document.getElementById('outputArea');
if (!output) return;
const timestamp = new Date().toLocaleTimeString(); const timestamp = new Date().toLocaleTimeString();
output.innerHTML += `\n[${timestamp}] ERROR: ${message}`; output.innerHTML += `\n[${timestamp}] ERROR: ${message}`;
output.scrollTop = output.scrollHeight; output.scrollTop = output.scrollHeight;
@ -233,6 +168,8 @@ function showError(message) {
function showSuccess(message) { function showSuccess(message) {
const output = document.getElementById('outputArea'); const output = document.getElementById('outputArea');
if (!output) return;
const timestamp = new Date().toLocaleTimeString(); const timestamp = new Date().toLocaleTimeString();
output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`; output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`;
output.scrollTop = output.scrollHeight; output.scrollTop = output.scrollHeight;
@ -240,10 +177,32 @@ function showSuccess(message) {
function clearOutput() { function clearOutput() {
const output = document.getElementById('outputArea'); const output = document.getElementById('outputArea');
output.innerHTML = ''; if (output) {
output.innerHTML = '';
}
} }
// Modal helper functions // Event Handlers
async function handleProfileChange(event) {
const profileId = event.target.value;
if (profileId) {
await selectProfile(profileId);
}
}
async function handleGroupChange(event) {
const groupId = event.target.value;
if (groupId) {
localStorage.setItem('lastGroupId', groupId);
await selectGroup(groupId);
} else {
localStorage.removeItem('lastGroupId');
currentGroup = null;
updateGroupDisplay();
}
}
// Modal Helpers
function closeModal(button) { function closeModal(button) {
const modal = button.closest('.modal'); const modal = button.closest('.modal');
if (modal) { if (modal) {
@ -251,14 +210,17 @@ function closeModal(button) {
} }
} }
// Global error handler // Error Handler Global
window.addEventListener('unhandledrejection', function(event) { window.addEventListener('unhandledrejection', function(event) {
console.error('Unhandled promise rejection:', event.reason); console.error('Unhandled promise rejection:', event.reason);
showError('An unexpected error occurred'); showError('An unexpected error occurred');
}); });
// Export functions for use in other modules // Exportar funciones globales
window.showError = showError; window.showError = showError;
window.showSuccess = showSuccess; window.showSuccess = showSuccess;
window.closeModal = closeModal; window.closeModal = closeModal;
window.currentProfile = currentProfile; window.STYLES = STYLES;
// Inicializar cuando la página carga
document.addEventListener('DOMContentLoaded', initializeApp);

View File

@ -5,66 +5,125 @@ let editingProfile = null;
async function loadProfiles() { async function loadProfiles() {
try { try {
const response = await apiRequest('/profiles'); const response = await apiRequest('/profiles');
const profiles = Object.values(response); if (!response || !Object.keys(response).length) {
throw new Error('No profiles available');
}
// Actualizar el selector manteniendo el valor seleccionado const profiles = Object.values(response);
const select = document.getElementById('profileSelect'); const select = document.getElementById('profileSelect');
// Actualizar el selector
select.innerHTML = profiles.map(profile => ` select.innerHTML = profiles.map(profile => `
<option value="${profile.id}"> <option value="${profile.id}" ${profile.id === selectedProfileId ? 'selected' : ''}>
${profile.name} ${profile.name || profile.id}
</option> </option>
`).join(''); `).join('');
// Establecer el valor seleccionado después de actualizar las opciones // Intentar seleccionar el perfil guardado o el predeterminado
if (response[selectedProfileId]) { const savedProfile = profiles.find(p => p.id === selectedProfileId);
if (savedProfile) {
await selectProfile(savedProfile.id);
} else {
// Si no se encuentra el perfil guardado, usar el primero disponible
selectedProfileId = profiles[0].id;
select.value = selectedProfileId; select.value = selectedProfileId;
await selectProfile(selectedProfileId); await selectProfile(selectedProfileId);
} else {
selectedProfileId = 'default';
select.value = 'default';
await selectProfile('default');
} }
// Asegurarse de que el evento change no sobrescriba la selección localStorage.setItem('selectedProfileId', selectedProfileId);
select.addEventListener('change', onProfileChange, { once: true });
} catch (error) { } catch (error) {
showError('Error al cargar los perfiles'); console.error('Error loading profiles:', error);
showError('Error loading profiles. Using default profile.');
await loadDefaultProfile();
}
}
async function loadDefaultProfile() {
try {
currentProfile = await apiRequest('/profiles/default');
selectedProfileId = 'default';
localStorage.setItem('selectedProfileId', 'default');
const select = document.getElementById('profileSelect');
select.innerHTML = `<option value="default">Default Profile</option>`;
select.value = 'default';
// Actualizar la visualización del perfil
updateProfileDisplay();
} catch (error) {
console.error('Error loading default profile:', error);
showError('Failed to load default profile');
} }
} }
async function selectProfile(profileId) { async function selectProfile(profileId) {
try { try {
currentProfile = await apiRequest(`/profiles/${profileId}`); const response = await apiRequest(`/profiles/${profileId}`);
updateWorkDirDisplay(); if (!response || response.error) {
throw new Error(response?.error || 'Profile not found');
}
currentProfile = response;
selectedProfileId = profileId;
localStorage.setItem('selectedProfileId', profileId);
// Actualizar la visualización del perfil
updateProfileDisplay();
} catch (error) { } catch (error) {
showError('Failed to load profile'); console.error('Failed to load profile:', error);
showError(`Failed to load profile: ${error.message}`);
// Intentar cargar el perfil por defecto si falla
if (profileId !== 'default') {
await loadDefaultProfile();
}
} }
} }
function updateProfileDisplay() {
const profileConfig = document.getElementById('profileConfig');
if (!profileConfig || !currentProfile) return;
profileConfig.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-500">Profile ID</label>
<div class="mt-1 text-sm">${currentProfile.id}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500">Name</label>
<div class="mt-1 text-sm">${currentProfile.name}</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-500">LLM Model</label>
<div class="mt-1 text-sm">${currentProfile.llm_settings?.model || 'Not set'}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500">Temperature</label>
<div class="mt-1 text-sm">${currentProfile.llm_settings?.temperature || 'Not set'}</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500">API Key</label>
<div class="mt-1 text-sm">
${currentProfile.llm_settings?.api_key ? '********' : 'Not set'}
</div>
</div>
</div>
`;
}
async function changeProfile() { async function changeProfile() {
const select = document.getElementById('profileSelect'); const select = document.getElementById('profileSelect');
await selectProfile(select.value); await selectProfile(select.value);
} }
async function selectWorkDir() { // Eliminar la función updateWorkDirDisplay y selectWorkDir
try {
const response = await apiRequest('/select-directory');
if (response.path) {
await apiRequest(`/profiles/${currentProfile.id}`, {
method: 'PUT',
body: JSON.stringify({
...currentProfile,
work_dir: response.path
})
});
await selectProfile(currentProfile.id);
showSuccess('Work directory updated successfully');
}
} catch (error) {
showError('Failed to update work directory');
}
}
// Profile editor modal // Profile editor modal
@ -95,12 +154,6 @@ function showProfileEditor(profile = null) {
class="${editableInputClass}" class="${editableInputClass}"
value="${profile?.name || ''}" required> value="${profile?.name || ''}" required>
</div> </div>
<div class="form-group">
<label for="workDir" class="block text-sm font-medium text-gray-700">Directorio de Trabajo</label>
<input type="text" id="workDir" name="work_dir"
class="${readonlyInputClass}"
value="${profile?.work_dir || ''}" readonly>
</div>
<div class="form-group"> <div class="form-group">
<label for="llmModel" class="block text-sm font-medium text-gray-700">LLM Model</label> <label for="llmModel" class="block text-sm font-medium text-gray-700">LLM Model</label>
<select id="llmModel" name="llm_model" <select id="llmModel" name="llm_model"
@ -143,7 +196,6 @@ async function saveProfile(event) {
const profileData = { const profileData = {
id: formData.get('id'), id: formData.get('id'),
name: formData.get('name'), name: formData.get('name'),
work_dir: formData.get('work_dir'),
llm_settings: { llm_settings: {
model: formData.get('llm_model'), model: formData.get('llm_model'),
api_key: formData.get('api_key'), api_key: formData.get('api_key'),
@ -192,11 +244,6 @@ async function editProfile() {
<input type="text" name="name" value="${currentProfile.name}" <input type="text" name="name" value="${currentProfile.name}"
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700">Work Directory</label>
<input type="text" name="work_dir" value="${currentProfile.work_dir}" readonly
class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm">
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">LLM Model</label> <label class="block text-sm font-medium text-gray-700">LLM Model</label>
<select name="llm_model" <select name="llm_model"
@ -232,7 +279,6 @@ async function saveProfile(modal) {
const profileData = { const profileData = {
id: formData.get('id'), id: formData.get('id'),
name: formData.get('name'), name: formData.get('name'),
work_dir: formData.get('work_dir'),
llm_settings: { llm_settings: {
model: formData.get('llm_model'), model: formData.get('llm_model'),
api_key: formData.get('api_key'), api_key: formData.get('api_key'),
@ -278,11 +324,6 @@ function newProfile() {
<input type="text" name="name" required <input type="text" name="name" required
class="${editableInputClass}"> class="${editableInputClass}">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700">Work Directory</label>
<input type="text" name="work_dir" readonly
class="${readonlyInputClass}">
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">LLM Model</label> <label class="block text-sm font-medium text-gray-700">LLM Model</label>
<select name="llm_model" <select name="llm_model"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
// Eliminar la función updateWorkDirDisplay ya que no se necesita más
// ...resto de funciones de utilidad...

View File

@ -1,161 +1,77 @@
// frontend/static/js/workdir_config.js // frontend/static/js/workdir_config.js
async function getWorkDirConfig() { async function editWorkDirConfig() {
if (!currentProfile?.work_dir) { if (!currentGroup?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo'); showError('No work directory configured');
return null;
}
try {
return await apiRequest(`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}`);
} catch (error) {
showError('Error al cargar la configuración del directorio de trabajo');
return null;
}
}
async function getGroupConfig(groupId) {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return null;
}
try {
return await apiRequest(
`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`
);
} catch (error) {
showError('Error al cargar la configuración del grupo');
return null;
}
}
async function updateGroupConfig(groupId, settings) {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return false;
}
try {
await apiRequest(
`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`,
{
method: 'PUT',
body: JSON.stringify(settings)
}
);
showSuccess('Group configuration updated successfully');
return true;
} catch (error) {
showError('Failed to update group configuration');
return false;
}
}
function showConfigEditor(config, schema) {
const modal = document.createElement('div');
modal.className = 'modal active';
const formContent = Object.entries(schema).map(([key, field]) => `
<div class="form-group">
<label class="block text-sm font-medium text-gray-700">${field.description || key}</label>
${getInputByType(key, field, config[key])}
</div>
`).join('');
modal.innerHTML = `
<div class="modal-content">
<h2 class="text-xl font-bold mb-4">Work Directory Configuration</h2>
<form id="configForm" class="space-y-4">
${formContent}
<div class="mt-4 flex justify-end space-x-3">
<button type="button" onclick="closeModal(this)"
class="${STYLES.buttonSecondary}">Cancel</button>
<button type="submit"
class="${STYLES.button}">Save</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
}
function getInputByType(key, field, value) {
switch (field.type) {
case 'select':
return `
<select name="${key}"
class="${STYLES.editableInput}">
${field.options.map(opt => `
<option value="${opt}" ${value === opt ? 'selected' : ''}>
${opt}
</option>
`).join('')}
</select>`;
case 'boolean':
return `
<select name="${key}"
class="${STYLES.editableInput}">
<option value="true" ${value ? 'selected' : ''}>Yes</option>
<option value="false" ${!value ? 'selected' : ''}>No</option>
</select>`;
case 'number':
return `
<input type="number" name="${key}"
value="${value || field.default || ''}"
class="${STYLES.editableInput}">`;
default:
return `
<input type="text" name="${key}"
value="${value || field.default || ''}"
class="${STYLES.editableInput}">`;
}
}
// static/js/workdir_config.js
async function showWorkDirConfig() {
if (!currentProfile?.work_dir) {
showError('No se ha seleccionado un directorio de trabajo');
return; return;
} }
try { try {
const config = await getWorkDirConfig(); // Load current configuration
const config = await apiRequest(`/workdir-config/${currentGroup.id}`);
// Load schema from script group
const schema = await apiRequest(`/script-groups/${currentGroup.id}/schema`);
const content = ` const content = `
<div class="space-y-4"> <form id="workDirConfigForm" class="space-y-4">
<div> ${Object.entries(schema.config_schema).map(([key, field]) => `
<h4 class="text-sm font-medium text-gray-900">Directory</h4> <div>
<p class="mt-1 text-sm text-gray-500">${currentProfile.work_dir}</p> <label class="block text-sm font-medium text-gray-700">
</div> ${field.description || key}
<div> </label>
<h4 class="text-sm font-medium text-gray-900">Version</h4> ${generateFormField(key, field, config[key])}
<p class="mt-1 text-sm text-gray-500">${config.version}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-900">Group Configurations</h4>
<div class="mt-2 space-y-3">
${Object.entries(config.group_settings || {}).map(([groupId, settings]) => `
<div class="rounded-md bg-gray-50 p-3">
<h5 class="text-sm font-medium text-gray-900">${groupId}</h5>
<pre class="mt-2 text-xs text-gray-500">${JSON.stringify(settings, null, 2)}</pre>
</div>
`).join('')}
</div> </div>
</div> `).join('')}
</div> </form>
`; `;
createModal('Work Directory Configuration', content); const modal = createModal('Edit Work Directory Configuration', content, true);
modal.querySelector('[onclick="saveModal(this)"]').onclick = () => saveWorkDirConfig(modal);
} catch (error) { } catch (error) {
showError('Error al cargar la configuración del directorio de trabajo'); showError('Error loading work directory configuration');
} }
} }
function closeModal(button) { async function saveWorkDirConfig(modal) {
const modal = button.closest('.modal'); if (!currentGroup?.work_dir) return;
if (modal) {
modal.remove(); const form = modal.querySelector('#workDirConfigForm');
const formData = new FormData(form);
const config = {};
formData.forEach((value, key) => {
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (!isNaN(value) && value !== '') value = Number(value);
config[key] = value;
});
try {
await apiRequest(`/workdir-config/${currentGroup.id}`, {
method: 'PUT',
body: JSON.stringify(config)
});
closeModal(modal);
showSuccess('Work directory configuration updated');
// Reload configuration display
const updatedConfig = await apiRequest(`/workdir-config/${currentGroup.id}`);
updateWorkDirConfig(updatedConfig);
} catch (error) {
showError('Error saving work directory configuration');
} }
} }
// Initialize configuration when the page loads
document.addEventListener('DOMContentLoaded', async () => {
if (currentGroup?.work_dir) {
try {
const config = await apiRequest(`/workdir-config/${currentGroup.id}`);
updateWorkDirConfig(config);
} catch (error) {
console.error('Error loading initial work directory config:', error);
}
}
});

View File

@ -9,5 +9,14 @@
</head> </head>
<body> <body>
{% block content %}{% endblock %} {% block content %}{% endblock %}
<!-- Scripts -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/modal.js') }}"></script>
<script src="{{ url_for('static', filename='js/profile.js') }}"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
<script src="{{ url_for('static', filename='js/workdir_config.js') }}"></script>
{% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -1,113 +1,281 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en" class="h-full bg-gray-50">
<head> {% block content %}
<meta charset="UTF-8"> <div class="min-h-full">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Navbar -->
<title>Local Scripts Web</title> <nav class="bg-white shadow-sm">
<!-- Tailwind y Alpine.js desde CDN --> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<script src="https://cdn.tailwindcss.com"></script> <div class="flex h-16 justify-between">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> <div class="flex">
<!-- HeroIcons --> <div class="flex flex-shrink-0 items-center">
<script src="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.0.18/solid/index.min.js"></script> <h1 class="text-xl font-semibold text-gray-900">Local Scripts Web</h1>
</head> </div>
<body class="h-full"> </div>
<div class="min-h-full"> <div class="flex items-center gap-4">
<!-- Navbar --> <select id="profileSelect"
<nav class="bg-white shadow-sm"> class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600"
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> onchange="changeProfile()">
<div class="flex h-16 justify-between"> <option value="">Select Profile</option>
<div class="flex"> </select>
<div class="flex flex-shrink-0 items-center"> <button onclick="editProfile()"
<h1 class="text-xl font-semibold text-gray-900">Local Scripts Web</h1> class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Edit Profile
</button>
<button onclick="newProfile()"
class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
New Profile
</button>
</div>
</div>
</div>
</nav>
<!-- Main content -->
<main>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<div class="space-y-6">
<!-- Profile Config Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Profile Configuration</h3>
<div id="profileConfig" class="mt-4">
<!-- Profile config will be loaded here -->
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> </div>
<select id="profileSelect"
class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600" <!-- Script Groups Section -->
onchange="changeProfile()"> <div class="bg-white shadow sm:rounded-lg">
<option value="">Select Profile</option> <div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center">
<h3 class="text-base font-semibold leading-6 text-gray-900">Script Groups</h3>
<button onclick="editGroupSchema()"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
Edit Schema
</button>
</div>
<div class="mt-4 space-y-4">
<select id="groupSelect"
class="w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<option value="">Select Script Group</option>
</select>
<!-- Group Configuration -->
<div id="groupConfig" class="mt-4">
<!-- Group config will be loaded here -->
</div>
<!-- Work Directory Configuration -->
<div id="workDirConfig" class="mt-4">
<!-- Work dir config will be loaded here -->
</div>
</div>
</div>
</div>
<!-- Scripts Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Scripts</h3>
<div id="scriptList" class="mt-4 space-y-4">
<!-- Scripts will be loaded here -->
</div>
</div>
</div>
<!-- Output Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center">
<h3 class="text-base font-semibold leading-6 text-gray-900">Output</h3>
<button onclick="clearOutput()"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
Clear
</button>
</div>
<div id="outputArea"
class="mt-4 h-64 overflow-y-auto p-4 font-mono text-sm bg-gray-50 rounded-md border border-gray-200">
</div>
</div>
</div>
</div>
</div>
</main>
</div>
{% endblock %}
{% block scripts %}
<!-- Load utilities first -->
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
<!-- Utility Functions -->
<script>
function generateFormField(key, field, value) {
switch (field.type) {
case 'string':
return `
<input type="text" name="${key}"
value="${value || field.default || ''}"
class="${STYLES.editableInput}">
`;
case 'number':
return `
<input type="number" name="${key}"
value="${value || field.default || 0}"
class="${STYLES.editableInput}">
`;
case 'boolean':
return `
<select name="${key}" class="${STYLES.editableInput}">
<option value="true" ${value ? 'selected' : ''}>Yes</option>
<option value="false" ${!value ? 'selected' : ''}>No</option>
</select>
`;
case 'select':
return `
<select name="${key}" class="${STYLES.editableInput}">
${field.options.map(opt => `
<option value="${opt}" ${value === opt ? 'selected' : ''}>
${opt}
</option>
`).join('')}
</select>
`;
case 'directory':
return `
<div class="flex gap-2">
<input type="text" name="${key}"
value="${value || field.default || ''}"
readonly
class="${STYLES.readonlyInput}">
<button type="button"
onclick="selectDirectory('${key}')"
class="${STYLES.buttonSecondary}">
Browse
</button>
</div>
`;
default:
return `
<input type="text" name="${key}"
value="${value || field.default || ''}"
class="${STYLES.editableInput}">
`;
}
}
function generateSchemaField(key = '', field = {}) {
const fieldId = `field_${Math.random().toString(36).substr(2, 9)}`;
return `
<div class="schema-field bg-gray-50 p-4 rounded-md relative" id="${fieldId}">
<button type="button"
onclick="removeSchemaField('${fieldId}')"
class="absolute top-2 right-2 text-red-600 hover:text-red-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Field Name</label>
<input type="text" name="field_name"
value="${key}"
class="${STYLES.editableInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Type</label>
<select name="field_type"
onchange="handleFieldTypeChange(this)"
class="${STYLES.editableInput}">
<option value="string" ${field.type === 'string' ? 'selected' : ''}>String</option>
<option value="number" ${field.type === 'number' ? 'selected' : ''}>Number</option>
<option value="boolean" ${field.type === 'boolean' ? 'selected' : ''}>Boolean</option>
<option value="select" ${field.type === 'select' ? 'selected' : ''}>Select</option>
<option value="directory" ${field.type === 'directory' ? 'selected' : ''}>Directory</option>
</select> </select>
<button onclick="editProfile()"
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Edit Profile
</button>
<button onclick="newProfile()"
class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">
New Profile
</button>
</div> </div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700">Description</label>
<input type="text" name="field_description"
value="${field.description || ''}"
class="${STYLES.editableInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Default Value</label>
<input type="text" name="field_default"
value="${field.default || ''}"
class="${STYLES.editableInput}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Required</label>
<select name="field_required" class="${STYLES.editableInput}">
<option value="true" ${field.required ? 'selected' : ''}>Yes</option>
<option value="false" ${!field.required ? 'selected' : ''}>No</option>
</select>
</div>
${field.type === 'select' ? `
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label>
<input type="text" name="field_options"
value="${(field.options || []).join(', ')}"
class="${STYLES.editableInput}">
</div>
` : ''}
</div> </div>
</div> </div>
</nav> `;
}
<!-- Main content --> function handleFieldTypeChange(select) {
<main> const fieldDiv = select.closest('.schema-field');
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> const optionsDiv = fieldDiv.querySelector('[name="field_options"]')?.closest('.col-span-2');
<div class="space-y-6">
<!-- Work Directory Section -->
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Work Directory</h3>
<div class="mt-4 flex gap-4">
<input type="text" id="workDirPath" readonly
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<button onclick="selectWorkDir()"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Browse
</button>
</div>
</div>
</div>
<!-- Scripts Section --> if (select.value === 'select' && !optionsDiv) {
<div class="bg-white shadow sm:rounded-lg"> const div = document.createElement('div');
<div class="px-4 py-5 sm:p-6"> div.className = 'col-span-2';
<h3 class="text-base font-semibold leading-6 text-gray-900">Scripts</h3> div.innerHTML = `
<div class="mt-4 space-y-4"> <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label>
<select id="groupSelect" <input type="text" name="field_options" class="${STYLES.editableInput}">
class="w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> `;
<option value="">Select Script Group</option> fieldDiv.querySelector('.grid').appendChild(div);
</select> } else if (select.value !== 'select' && optionsDiv) {
<div id="scriptList" class="hidden space-y-4"> optionsDiv.remove();
<!-- Scripts will be loaded here --> }
</div> }
</div>
</div>
</div>
<!-- Output Section --> function addSchemaField() {
<div class="bg-white shadow sm:rounded-lg"> const container = document.getElementById('schemaFields');
<div class="px-4 py-5 sm:p-6"> const field = document.createElement('div');
<div class="flex justify-between items-center"> field.innerHTML = generateSchemaField();
<h3 class="text-base font-semibold leading-6 text-gray-900">Output</h3> container.appendChild(field.firstElementChild);
<button onclick="clearOutput()" }
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
Clear
</button>
</div>
<div id="outputArea"
class="mt-4 h-64 overflow-y-auto p-4 font-mono text-sm bg-gray-50 rounded-md border border-gray-200">
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Scripts --> function removeSchemaField(fieldId) {
<script src="{{ url_for('static', filename='js/main.js') }}"></script> document.getElementById(fieldId).remove();
<script src="{{ url_for('static', filename='js/workdir_config.js') }}"></script> }
<script src="{{ url_for('static', filename='js/profile.js') }}"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script> async function selectDirectory(inputName) {
<script src="{{ url_for('static', filename='js/modal.js') }}"></script> try {
<!-- Al final del body --> const response = await apiRequest('/select-directory');
<script> if (response.path) {
// Initialización cuando la página carga const input = document.querySelector(`input[name="${inputName}"]`);
document.addEventListener('DOMContentLoaded', async () => { if (input) {
console.log('DOM loaded, initializing...'); input.value = response.path;
await initializeApp(); }
}); }
</script> } catch (error) {
</body> showError('Error selecting directory');
</html> }
}
</script>
<!-- Initialize app -->
<script>
document.addEventListener('DOMContentLoaded', async () => {
console.log('DOM loaded, initializing...');
await initializeApp();
});
</script>
{% endblock %}