intentando hacer funcionar los 3 niveles
This commit is contained in:
parent
7e267f3509
commit
37abdf8cfd
166
backend/app.py
166
backend/app.py
|
@ -4,11 +4,11 @@ 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
|
||||
backend_dir = Path(__file__).parent.parent
|
||||
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 flask import Flask, render_template, jsonify, request
|
||||
from core.directory_handler import select_directory
|
||||
from core.script_manager import ScriptManager
|
||||
from core.profile_manager import ProfileManager
|
||||
|
@ -46,9 +46,8 @@ def get_profile(profile_id):
|
|||
@app.route('/api/profiles', methods=['POST'])
|
||||
def create_profile():
|
||||
"""Create new profile"""
|
||||
profile_data = request.json
|
||||
try:
|
||||
profile = profile_manager.create_profile(profile_data)
|
||||
profile = profile_manager.create_profile(request.json)
|
||||
return jsonify(profile)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
@ -57,13 +56,9 @@ def create_profile():
|
|||
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
|
||||
profile = profile_manager.update_profile(profile_id, request.json)
|
||||
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'])
|
||||
|
@ -75,6 +70,7 @@ def delete_profile(profile_id):
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
# Script group endpoints
|
||||
@app.route('/api/script-groups', methods=['GET'])
|
||||
def get_script_groups():
|
||||
"""Get all available script groups"""
|
||||
|
@ -84,110 +80,94 @@ def get_script_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"""
|
||||
@app.route('/api/script-groups/<group_id>/config', methods=['GET'])
|
||||
def get_group_config(group_id):
|
||||
"""Get script group configuration"""
|
||||
try:
|
||||
groups = script_manager.discover_groups()
|
||||
return jsonify(groups)
|
||||
config = script_manager.get_group_data(group_id)
|
||||
return jsonify(config)
|
||||
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
|
||||
|
||||
@app.route('/api/script-groups/<group_id>/config', methods=['PUT'])
|
||||
def update_group_config(group_id):
|
||||
"""Update script group configuration"""
|
||||
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"})
|
||||
config = script_manager.update_group_data(group_id, request.json)
|
||||
return jsonify(config)
|
||||
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"""
|
||||
@app.route('/api/script-groups/<group_id>/schema', methods=['GET'])
|
||||
def get_group_schema(group_id):
|
||||
"""Get script group schema"""
|
||||
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
|
||||
schema = script_manager.get_global_schema()
|
||||
return jsonify(schema)
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, port=5000)
|
||||
app.run(debug=True, port=5000)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -1,4 +1,3 @@
|
|||
# backend/core/profile_manager.py
|
||||
from pathlib import Path
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
@ -6,44 +5,55 @@ from datetime import datetime
|
|||
|
||||
|
||||
class ProfileManager:
|
||||
"""Manages configuration profiles"""
|
||||
"""Manages application 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.schema_file = data_dir / "profile_schema.json"
|
||||
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]:
|
||||
"""Load profiles from file"""
|
||||
if self.profiles_file.exists():
|
||||
try:
|
||||
"""Load all profiles and ensure default profile exists"""
|
||||
profiles = {}
|
||||
|
||||
# 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:
|
||||
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()}
|
||||
loaded_profiles = json.load(f)
|
||||
profiles.update(loaded_profiles)
|
||||
except Exception as e:
|
||||
print(f"Error loading profiles: {e}")
|
||||
|
||||
# Asegurar que existe el perfil por defecto
|
||||
if "default" not in profiles:
|
||||
profiles["default"] = default_profile
|
||||
self._save_profiles(profiles)
|
||||
return profiles
|
||||
|
||||
return profiles
|
||||
|
||||
def _create_default_profile(self) -> Dict[str, Any]:
|
||||
"""Create default profile with timestamp"""
|
||||
"""Create default profile"""
|
||||
profile = self.DEFAULT_PROFILE.copy()
|
||||
now = datetime.now().isoformat()
|
||||
profile["created_at"] = now
|
||||
|
@ -51,25 +61,87 @@ class ProfileManager:
|
|||
return profile
|
||||
|
||||
def _save_profiles(self, profiles: Optional[Dict] = None):
|
||||
"""Save profiles to file"""
|
||||
"""Save all profiles"""
|
||||
if profiles is None:
|
||||
profiles = self.profiles
|
||||
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:
|
||||
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
|
||||
print(f"Error saving profiles: {e}")
|
||||
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]]:
|
||||
"""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)
|
||||
"""Get specific profile with fallback to default"""
|
||||
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]:
|
||||
"""Create new profile"""
|
||||
|
@ -85,41 +157,36 @@ class ProfileManager:
|
|||
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]
|
||||
# Validate profile data
|
||||
validated_data = self._validate_profile(profile_data)
|
||||
|
||||
self.profiles[profile_id] = profile_data
|
||||
# Save profile
|
||||
self.profiles[profile_id] = validated_data
|
||||
self._save_profiles()
|
||||
return profile_data
|
||||
return validated_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 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")
|
||||
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 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
|
||||
# Validate profile data
|
||||
validated_data = self._validate_profile(profile_data)
|
||||
|
||||
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
|
||||
# Update profile
|
||||
current_profile = self.profiles[profile_id].copy()
|
||||
current_profile.update(validated_data)
|
||||
self.profiles[profile_id] = current_profile
|
||||
self._save_profiles()
|
||||
return current_profile
|
||||
|
||||
def delete_profile(self, profile_id: str):
|
||||
"""Delete profile"""
|
||||
|
@ -131,3 +198,18 @@ class ProfileManager:
|
|||
|
||||
del self.profiles[profile_id]
|
||||
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
|
||||
|
|
|
@ -1,131 +1,163 @@
|
|||
# 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
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
class ScriptManager:
|
||||
"""Manages script groups and their execution"""
|
||||
|
||||
def __init__(self, script_groups_dir: Path):
|
||||
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]:
|
||||
"""Get settings for a script group"""
|
||||
return self.group_settings.get_group_settings(group_id)
|
||||
def _load_global_schema(self) -> Dict[str, Any]:
|
||||
"""Load global 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 global schema: {e}")
|
||||
return {"config_schema": {}}
|
||||
|
||||
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 _load_group_data(self, group_id: str) -> Dict[str, Any]:
|
||||
"""Load group data"""
|
||||
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]:
|
||||
"""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
|
||||
def _save_group_data(self, group_id: str, data: Dict[str, Any]):
|
||||
"""Save group data"""
|
||||
data_file = self.script_groups_dir / group_id / "data.json"
|
||||
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():
|
||||
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
|
||||
def _validate_group_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Validate group data against schema"""
|
||||
schema = self._global_schema.get("config_schema", {})
|
||||
validated = {}
|
||||
|
||||
# Retornar un schema vacío si no existe el archivo
|
||||
return {"group_name": group_id, "description": "", "config_schema": {}}
|
||||
for key, field_schema in schema.items():
|
||||
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]]:
|
||||
"""Get list of available script groups"""
|
||||
"""Get all 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(
|
||||
{
|
||||
group_data = self._load_group_data(group_dir.name)
|
||||
if group_data:
|
||||
groups.append({
|
||||
"id": group_dir.name,
|
||||
"name": group_dir.name.replace("_", " ").title(),
|
||||
"path": str(group_dir),
|
||||
}
|
||||
)
|
||||
|
||||
"name": group_data.get("name", group_dir.name),
|
||||
"description": group_data.get("description", ""),
|
||||
"work_dir": group_data.get("work_dir", ""),
|
||||
"enabled": group_data.get("enabled", True)
|
||||
})
|
||||
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]]:
|
||||
"""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
|
||||
# Importar módulo del script
|
||||
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
|
||||
# Encontrar la clase del script
|
||||
script_class = None
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (
|
||||
inspect.isclass(obj)
|
||||
and obj.__module__ == module.__name__
|
||||
and hasattr(obj, "run")
|
||||
):
|
||||
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
|
||||
# Extraer nombre y descripción del docstring
|
||||
docstring = inspect.getdoc(script_class)
|
||||
if docstring:
|
||||
name, *description = docstring.split("\n", 1)
|
||||
|
@ -138,44 +170,38 @@ class ScriptManager:
|
|||
"id": script_file.stem,
|
||||
"name": name.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:
|
||||
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"""
|
||||
# Get group settings first
|
||||
group_settings = self.group_settings.get_group_settings(group_id)
|
||||
work_dir = group_settings.get("work_dir")
|
||||
# Obtener datos del grupo
|
||||
group_data = self._load_group_data(group_id)
|
||||
work_dir = group_data.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
|
||||
# Importar módulo del script
|
||||
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
|
||||
# Encontrar e instanciar la clase del script
|
||||
script_class = None
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (
|
||||
inspect.isclass(obj)
|
||||
and obj.__module__ == module.__name__
|
||||
and hasattr(obj, "run")
|
||||
):
|
||||
if (inspect.isclass(obj) and
|
||||
obj.__module__ == module.__name__ and
|
||||
hasattr(obj, "run")):
|
||||
script_class = obj
|
||||
break
|
||||
|
||||
|
@ -187,3 +213,7 @@ class ScriptManager:
|
|||
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def get_global_schema(self) -> Dict[str, Any]:
|
||||
"""Get global configuration schema"""
|
||||
return self._global_schema
|
|
@ -2,6 +2,7 @@
|
|||
from typing import Dict, Any
|
||||
from pathlib import Path
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
class BaseScript:
|
||||
"""Base class for all scripts"""
|
||||
|
@ -19,8 +20,8 @@ class BaseScript:
|
|||
"""
|
||||
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"""
|
||||
def get_work_config(self, work_dir: str, group_id: str) -> Dict[str, Any]:
|
||||
"""Get configuration from work directory"""
|
||||
config_file = Path(work_dir) / "script_config.json"
|
||||
|
||||
if config_file.exists():
|
||||
|
@ -29,33 +30,49 @@ class BaseScript:
|
|||
config = json.load(f)
|
||||
return config.get("group_settings", {}).get(group_id, {})
|
||||
except Exception as e:
|
||||
print(f"Error loading config: {e}")
|
||||
print(f"Error loading work directory config: {e}")
|
||||
|
||||
return {}
|
||||
|
||||
def save_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]):
|
||||
"""Save group configuration to work directory"""
|
||||
def save_work_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]):
|
||||
"""Save configuration to work directory"""
|
||||
config_file = Path(work_dir) / "script_config.json"
|
||||
|
||||
try:
|
||||
# Load existing config or create new
|
||||
# Cargar configuración existente o crear nueva
|
||||
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": {}
|
||||
"group_settings": {},
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Update settings
|
||||
# Actualizar configuración
|
||||
if "group_settings" not in config:
|
||||
config["group_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:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
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 {}
|
|
@ -2,29 +2,46 @@
|
|||
"description": "Configuration schema for script groups",
|
||||
"config_schema": {
|
||||
"work_dir": {
|
||||
"type": "string",
|
||||
"type": "directory",
|
||||
"description": "Working directory for this script group",
|
||||
"required": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name for the script group",
|
||||
"required": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of this script group",
|
||||
"description": "Detailed description of the 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": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable automatic backups",
|
||||
"default": false
|
||||
"description": "Whether this script group is enabled",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
# backend/__init__.py
|
|
@ -1 +0,0 @@
|
|||
# backend/core/__init__.py
|
|
@ -1 +0,0 @@
|
|||
# backend/script_groups/__init__.py
|
|
@ -1 +0,0 @@
|
|||
# backend/script_groups/example_group/__init__.py
|
193
claude/app.py
193
claude/app.py
|
@ -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)
|
|
@ -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>
|
|
@ -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}")
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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)
|
|
@ -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>
|
264
claude/main.js
264
claude/main.js
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)
|
108
claude/x1.py
108
claude/x1.py
|
@ -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)
|
||||
}
|
104
claude/x2.py
104
claude/x2.py
|
@ -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)
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,25 +2,23 @@
|
|||
"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"
|
||||
"created_at": "2025-02-08T12:00:00.000Z",
|
||||
"updated_at": "2025-02-08T12:00:00.000Z"
|
||||
},
|
||||
"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"
|
||||
"created_at": "2025-02-08T13:00:43.541932",
|
||||
"updated_at": "2025-02-08T23:34:43.039269"
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
/* 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 {
|
||||
white-space: pre-wrap;
|
||||
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 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -21,11 +21,6 @@
|
|||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
// frontend/static/js/main.js
|
||||
|
||||
// Global state
|
||||
// Estado global
|
||||
let currentProfile = null;
|
||||
let currentGroup = null;
|
||||
|
||||
// Definir clases comunes para inputs
|
||||
// Estilos comunes
|
||||
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",
|
||||
|
@ -11,9 +12,10 @@ const STYLES = {
|
|||
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() {
|
||||
try {
|
||||
console.log('Inicializando aplicación...');
|
||||
console.log('Initializing application...');
|
||||
|
||||
// Cargar perfiles
|
||||
const profiles = await apiRequest('/profiles');
|
||||
|
@ -33,66 +35,16 @@ async function initializeApp() {
|
|||
await selectProfile(selectedProfile.id);
|
||||
}
|
||||
|
||||
// Cargar grupos de scripts y restaurar la última selección
|
||||
await restoreScriptGroup();
|
||||
|
||||
// Actualizar la interfaz
|
||||
updateWorkDirDisplay();
|
||||
// Restaurar último estado
|
||||
await restoreLastState();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error al inicializar la aplicación:', error);
|
||||
showError('Error al inicializar la aplicación');
|
||||
console.error('Error initializing application:', error);
|
||||
showError('Error initializing application');
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreScriptGroup() {
|
||||
try {
|
||||
// Primero cargar los grupos disponibles
|
||||
await loadScriptGroups();
|
||||
|
||||
// Luego intentar restaurar el último grupo seleccionado
|
||||
const lastGroupId = localStorage.getItem('lastGroupId');
|
||||
if (lastGroupId) {
|
||||
console.log('Restoring last group:', lastGroupId);
|
||||
const groupSelect = document.getElementById('groupSelect');
|
||||
if (groupSelect) {
|
||||
groupSelect.value = lastGroupId;
|
||||
if (groupSelect.value) { // Verifica que el valor se haya establecido correctamente
|
||||
await loadGroupScripts(lastGroupId);
|
||||
} else {
|
||||
console.log('Selected group no longer exists:', lastGroupId);
|
||||
localStorage.removeItem('lastGroupId');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restoring script group:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Función para restaurar el último estado
|
||||
async function restoreLastState() {
|
||||
const lastProfileId = localStorage.getItem('lastProfileId');
|
||||
const lastGroupId = localStorage.getItem('lastGroupId');
|
||||
|
||||
console.log('Restoring last state:', { lastProfileId, lastGroupId });
|
||||
|
||||
if (lastProfileId) {
|
||||
const profileSelect = document.getElementById('profileSelect');
|
||||
profileSelect.value = lastProfileId;
|
||||
await selectProfile(lastProfileId);
|
||||
}
|
||||
|
||||
if (lastGroupId) {
|
||||
const groupSelect = document.getElementById('groupSelect');
|
||||
if (groupSelect) {
|
||||
groupSelect.value = lastGroupId;
|
||||
await loadGroupScripts(lastGroupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API functions
|
||||
// Funciones de API
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
try {
|
||||
const response = await fetch(`/api${endpoint}`, {
|
||||
|
@ -105,127 +57,110 @@ async function apiRequest(endpoint, options = {}) {
|
|||
|
||||
if (!response.ok) {
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error('Error API:', error);
|
||||
console.error('API Error:', 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');
|
||||
}
|
||||
}
|
||||
|
||||
// Funciones de gestión de perfiles
|
||||
async function selectProfile(profileId) {
|
||||
try {
|
||||
console.log('Seleccionando perfil:', profileId);
|
||||
console.log('Selecting profile:', profileId);
|
||||
|
||||
// Cargar perfil
|
||||
currentProfile = await apiRequest(`/profiles/${profileId}`);
|
||||
|
||||
// Guardar en localStorage
|
||||
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
|
||||
const select = document.getElementById('profileSelect');
|
||||
if (select) {
|
||||
select.value = profileId;
|
||||
console.log('Updated profileSelect value to:', profileId);
|
||||
}
|
||||
// Actualizar UI
|
||||
updateProfileSelector([currentProfile]);
|
||||
updateProfileDisplay();
|
||||
|
||||
updateWorkDirDisplay();
|
||||
|
||||
// Recargar scripts con el último grupo seleccionado
|
||||
await restoreScriptGroup();
|
||||
return currentProfile;
|
||||
} catch (error) {
|
||||
console.error('Error al seleccionar perfil:', error);
|
||||
showError('Error al cargar el perfil');
|
||||
console.error('Error selecting profile:', error);
|
||||
showError('Error loading profile');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||
|
||||
function updateProfileSelector(profiles) {
|
||||
const select = document.getElementById('profileSelect');
|
||||
if (!select) return;
|
||||
|
||||
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
|
||||
}
|
||||
function updateProfileDisplay() {
|
||||
const container = document.getElementById('profileConfig');
|
||||
if (!container || !currentProfile) return;
|
||||
|
||||
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
|
||||
function updateWorkDirDisplay() {
|
||||
const input = document.getElementById('workDirPath');
|
||||
if (input && currentProfile) {
|
||||
input.value = currentProfile.work_dir || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function selectWorkDir() {
|
||||
// Funciones de estado
|
||||
async function restoreLastState() {
|
||||
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');
|
||||
// Restaurar último grupo seleccionado
|
||||
const lastGroupId = localStorage.getItem('lastGroupId');
|
||||
if (lastGroupId) {
|
||||
const groupSelect = document.getElementById('groupSelect');
|
||||
if (groupSelect) {
|
||||
groupSelect.value = lastGroupId;
|
||||
await handleGroupChange({ target: { value: lastGroupId } });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al seleccionar directorio:', error); // Debug
|
||||
showError('Error al actualizar el directorio de trabajo');
|
||||
console.error('Error restoring state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Output functions
|
||||
// Funciones de utilidad UI
|
||||
function showError(message) {
|
||||
const output = document.getElementById('outputArea');
|
||||
if (!output) return;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
output.innerHTML += `\n[${timestamp}] ERROR: ${message}`;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
|
@ -233,6 +168,8 @@ function showError(message) {
|
|||
|
||||
function showSuccess(message) {
|
||||
const output = document.getElementById('outputArea');
|
||||
if (!output) return;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
|
@ -240,10 +177,32 @@ function showSuccess(message) {
|
|||
|
||||
function clearOutput() {
|
||||
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) {
|
||||
const modal = button.closest('.modal');
|
||||
if (modal) {
|
||||
|
@ -251,14 +210,17 @@ function closeModal(button) {
|
|||
}
|
||||
}
|
||||
|
||||
// Global error handler
|
||||
// Error Handler Global
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
showError('An unexpected error occurred');
|
||||
});
|
||||
|
||||
// Export functions for use in other modules
|
||||
// Exportar funciones globales
|
||||
window.showError = showError;
|
||||
window.showSuccess = showSuccess;
|
||||
window.closeModal = closeModal;
|
||||
window.currentProfile = currentProfile;
|
||||
window.STYLES = STYLES;
|
||||
|
||||
// Inicializar cuando la página carga
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
|
@ -5,66 +5,125 @@ let editingProfile = null;
|
|||
async function loadProfiles() {
|
||||
try {
|
||||
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');
|
||||
|
||||
// Actualizar el selector
|
||||
select.innerHTML = profiles.map(profile => `
|
||||
<option value="${profile.id}">
|
||||
${profile.name}
|
||||
<option value="${profile.id}" ${profile.id === selectedProfileId ? 'selected' : ''}>
|
||||
${profile.name || profile.id}
|
||||
</option>
|
||||
`).join('');
|
||||
|
||||
// Establecer el valor seleccionado después de actualizar las opciones
|
||||
if (response[selectedProfileId]) {
|
||||
// Intentar seleccionar el perfil guardado o el predeterminado
|
||||
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;
|
||||
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 });
|
||||
localStorage.setItem('selectedProfileId', selectedProfileId);
|
||||
|
||||
} 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) {
|
||||
try {
|
||||
currentProfile = await apiRequest(`/profiles/${profileId}`);
|
||||
updateWorkDirDisplay();
|
||||
const response = await apiRequest(`/profiles/${profileId}`);
|
||||
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) {
|
||||
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() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
// Eliminar la función updateWorkDirDisplay y selectWorkDir
|
||||
|
||||
// Profile editor modal
|
||||
|
||||
|
@ -95,12 +154,6 @@ function showProfileEditor(profile = null) {
|
|||
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"
|
||||
|
@ -143,7 +196,6 @@ async function saveProfile(event) {
|
|||
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'),
|
||||
|
@ -192,11 +244,6 @@ async function editProfile() {
|
|||
<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"
|
||||
|
@ -232,7 +279,6 @@ async function saveProfile(modal) {
|
|||
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'),
|
||||
|
@ -278,11 +324,6 @@ function newProfile() {
|
|||
<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"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
// Eliminar la función updateWorkDirDisplay ya que no se necesita más
|
||||
|
||||
// ...resto de funciones de utilidad...
|
|
@ -1,161 +1,77 @@
|
|||
// 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');
|
||||
async function editWorkDirConfig() {
|
||||
if (!currentGroup?.work_dir) {
|
||||
showError('No work directory configured');
|
||||
return;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<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('')}
|
||||
<form id="workDirConfigForm" class="space-y-4">
|
||||
${Object.entries(schema.config_schema).map(([key, field]) => `
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
${field.description || key}
|
||||
</label>
|
||||
${generateFormField(key, field, config[key])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</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) {
|
||||
showError('Error al cargar la configuración del directorio de trabajo');
|
||||
showError('Error loading work directory configuration');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(button) {
|
||||
const modal = button.closest('.modal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
async function saveWorkDirConfig(modal) {
|
||||
if (!currentGroup?.work_dir) return;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -9,5 +9,14 @@
|
|||
</head>
|
||||
<body>
|
||||
{% 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>
|
||||
</html>
|
|
@ -1,113 +1,281 @@
|
|||
<!DOCTYPE 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>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<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">
|
||||
<!-- 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 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>
|
||||
</div>
|
||||
|
||||
<!-- Script Groups 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">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>
|
||||
<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 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>
|
||||
</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>
|
||||
function handleFieldTypeChange(select) {
|
||||
const fieldDiv = select.closest('.schema-field');
|
||||
const optionsDiv = fieldDiv.querySelector('[name="field_options"]')?.closest('.col-span-2');
|
||||
|
||||
if (select.value === 'select' && !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="field_options" class="${STYLES.editableInput}">
|
||||
`;
|
||||
fieldDiv.querySelector('.grid').appendChild(div);
|
||||
} else if (select.value !== 'select' && optionsDiv) {
|
||||
optionsDiv.remove();
|
||||
}
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
function addSchemaField() {
|
||||
const container = document.getElementById('schemaFields');
|
||||
const field = document.createElement('div');
|
||||
field.innerHTML = generateSchemaField();
|
||||
container.appendChild(field.firstElementChild);
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
function removeSchemaField(fieldId) {
|
||||
document.getElementById(fieldId).remove();
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
async function selectDirectory(inputName) {
|
||||
try {
|
||||
const response = await apiRequest('/select-directory');
|
||||
if (response.path) {
|
||||
const input = document.querySelector(`input[name="${inputName}"]`);
|
||||
if (input) {
|
||||
input.value = response.path;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Error selecting directory');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Initialize app -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('DOM loaded, initializing...');
|
||||
await initializeApp();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue