Funcionando
This commit is contained in:
parent
3353b45424
commit
dbf4d9d685
|
@ -1,21 +1,35 @@
|
||||||
# backend/app.py
|
# backend/app.py
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the backend directory to Python path
|
||||||
|
backend_dir = Path(__file__).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, send_from_directory
|
||||||
from core.directory_handler import select_directory
|
from core.directory_handler import select_directory
|
||||||
from pathlib import Path
|
from core.script_manager import ScriptManager
|
||||||
from core.profile_manager import ProfileManager
|
from core.profile_manager import ProfileManager
|
||||||
|
|
||||||
app = Flask(__name__,
|
app = Flask(__name__,
|
||||||
template_folder='../frontend/templates',
|
template_folder='../frontend/templates',
|
||||||
static_folder='../frontend/static')
|
static_folder='../frontend/static')
|
||||||
|
|
||||||
# Initialize profile manager
|
# Initialize managers
|
||||||
profile_manager = ProfileManager(Path('../data'))
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Render main page"""
|
"""Render main page"""
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
# Profile endpoints
|
||||||
@app.route('/api/profiles', methods=['GET'])
|
@app.route('/api/profiles', methods=['GET'])
|
||||||
def get_profiles():
|
def get_profiles():
|
||||||
"""Get all profiles"""
|
"""Get all profiles"""
|
||||||
|
@ -58,6 +72,7 @@ def delete_profile(profile_id):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 400
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
# Directory handling endpoints
|
||||||
@app.route('/api/select-directory', methods=['GET'])
|
@app.route('/api/select-directory', methods=['GET'])
|
||||||
def handle_select_directory():
|
def handle_select_directory():
|
||||||
"""Handle directory selection"""
|
"""Handle directory selection"""
|
||||||
|
@ -66,5 +81,61 @@ def handle_select_directory():
|
||||||
return jsonify(result), 400
|
return jsonify(result), 400
|
||||||
return jsonify(result)
|
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
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,112 @@
|
||||||
|
# backend/core/script_manager.py
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
import json
|
||||||
|
|
||||||
|
class ScriptManager:
|
||||||
|
"""Manages script discovery and execution"""
|
||||||
|
|
||||||
|
def __init__(self, script_groups_dir: Path):
|
||||||
|
self.script_groups_dir = script_groups_dir
|
||||||
|
|
||||||
|
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:
|
||||||
|
return {
|
||||||
|
"id": script_file.stem,
|
||||||
|
"name": script_class.__doc__.split('\n')[0].strip() if script_class.__doc__ else script_file.stem,
|
||||||
|
"description": inspect.getdoc(script_class),
|
||||||
|
"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, work_dir: str,
|
||||||
|
profile: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Execute a specific script"""
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
# 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}")
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,49 @@
|
||||||
|
# backend/script_groups/example_group/x1.py
|
||||||
|
from ..base_script import BaseScript
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class FileCounter(BaseScript):
|
||||||
|
"""
|
||||||
|
Count Files in Directory
|
||||||
|
Lists and counts files in the working directory by extension
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self, work_dir: str, profile: dict) -> dict:
|
||||||
|
try:
|
||||||
|
# Get configuration if any
|
||||||
|
config = self.get_config(work_dir, "example_group")
|
||||||
|
exclude_dirs = config.get("exclude_dirs", [])
|
||||||
|
|
||||||
|
# Initialize counters
|
||||||
|
extension_counts = {}
|
||||||
|
total_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:
|
||||||
|
total_files += 1
|
||||||
|
ext = Path(file).suffix.lower() or 'no extension'
|
||||||
|
extension_counts[ext] = extension_counts.get(ext, 0) + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"total_files": total_files,
|
||||||
|
"extension_counts": extension_counts
|
||||||
|
},
|
||||||
|
"output": f"Found {total_files} files\n" + "\n".join(
|
||||||
|
f"{ext}: {count} files"
|
||||||
|
for ext, count in sorted(extension_counts.items())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
# backend/script_groups/example_group/x2.py
|
||||||
|
from ..base_script import BaseScript
|
||||||
|
import psutil
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class SystemInfo(BaseScript):
|
||||||
|
"""
|
||||||
|
System Information
|
||||||
|
Collects and displays basic system information
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self, work_dir: str, profile: dict) -> dict:
|
||||||
|
try:
|
||||||
|
# Collect system information
|
||||||
|
info = {
|
||||||
|
"cpu": {
|
||||||
|
"cores": psutil.cpu_count(),
|
||||||
|
"usage": psutil.cpu_percent(interval=1),
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"total": psutil.virtual_memory().total,
|
||||||
|
"available": psutil.virtual_memory().available,
|
||||||
|
"percent": psutil.virtual_memory().percent,
|
||||||
|
},
|
||||||
|
"disk": {
|
||||||
|
"total": psutil.disk_usage(work_dir).total,
|
||||||
|
"free": psutil.disk_usage(work_dir).free,
|
||||||
|
"percent": psutil.disk_usage(work_dir).percent,
|
||||||
|
},
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save to work directory if configured
|
||||||
|
config = self.get_config(work_dir, "example_group")
|
||||||
|
if config.get("save_system_info", False):
|
||||||
|
output_file = Path(work_dir) / "system_info.json"
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
json.dump(info, f, indent=2)
|
||||||
|
|
||||||
|
# Format output
|
||||||
|
output = f"""System Information:
|
||||||
|
CPU: {info['cpu']['cores']} cores ({info['cpu']['usage']}% usage)
|
||||||
|
Memory: {info['memory']['percent']}% used
|
||||||
|
Disk: {info['disk']['percent']}% used"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": info,
|
||||||
|
"output": output
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"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": "D:/Proyectos/AutoCAD",
|
||||||
|
"llm_settings": {
|
||||||
|
"api_key": "333333333333",
|
||||||
|
"model": "gpt-4",
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
"created_at": "2025-02-07T13:00:43.541932",
|
||||||
|
"updated_at": "2025-02-07T13:01:40.473406"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,113 +1,163 @@
|
||||||
# frontend/static/css/style.css
|
/* frontend/static/css/style.css - Add these styles */
|
||||||
:root {
|
|
||||||
--primary-color: #2c3e50;
|
|
||||||
--secondary-color: #34495e;
|
|
||||||
--accent-color: #3498db;
|
|
||||||
--text-color: #333;
|
|
||||||
--bg-color: #f5f6fa;
|
|
||||||
--border-color: #dcdde1;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
.script-group {
|
||||||
margin: 0;
|
background: white;
|
||||||
padding: 0;
|
border-radius: 8px;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
.script-group-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 1200px;
|
margin-bottom: 1rem;
|
||||||
margin: 0 auto;
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-brand {
|
.script-group-header h3 {
|
||||||
font-size: 1.5rem;
|
margin: 0;
|
||||||
font-weight: bold;
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-selector {
|
.script-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.script-item {
|
||||||
max-width: 1200px;
|
display: flex;
|
||||||
margin: 2rem auto;
|
justify-content: space-between;
|
||||||
padding: 0 1rem;
|
align-items: start;
|
||||||
}
|
|
||||||
|
|
||||||
select, input, button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-dir-section {
|
|
||||||
background: white;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 8px;
|
background: var(--bg-color);
|
||||||
margin-bottom: 2rem;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-dir-controls {
|
.script-info {
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-dir-controls input {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scripts-section {
|
.script-info h4 {
|
||||||
background: white;
|
margin: 0 0 0.5rem 0;
|
||||||
padding: 1rem;
|
color: var(--secondary-color);
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-groups {
|
.script-info p {
|
||||||
display: grid;
|
margin: 0;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
font-size: 0.9rem;
|
||||||
gap: 1rem;
|
color: #666;
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-group {
|
.script-actions {
|
||||||
background: var(--bg-color);
|
display: flex;
|
||||||
padding: 1rem;
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn:hover {
|
||||||
|
background-color: #219a52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-editor {
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.script
|
.output-area {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scripts {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add to your existing style.css */
|
||||||
|
|
||||||
|
.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: 8px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-info {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-configs {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-config {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-config pre {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
|
@ -1,11 +1,18 @@
|
||||||
# frontend/static/js/main.js
|
// frontend/static/js/main.js
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
let currentProfile = null;
|
let currentProfile = null;
|
||||||
|
|
||||||
// Initialize when page loads
|
// Initialize when page loads
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
try {
|
||||||
await loadProfiles();
|
await loadProfiles();
|
||||||
|
await loadScriptGroups();
|
||||||
updateWorkDirDisplay();
|
updateWorkDirDisplay();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Initialization error:', error);
|
||||||
|
showError('Failed to initialize application');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// API functions
|
// API functions
|
||||||
|
@ -70,13 +77,18 @@ async function selectProfile(profileId) {
|
||||||
|
|
||||||
async function changeProfile() {
|
async function changeProfile() {
|
||||||
const select = document.getElementById('profileSelect');
|
const select = document.getElementById('profileSelect');
|
||||||
|
if (select.value) {
|
||||||
await selectProfile(select.value);
|
await selectProfile(select.value);
|
||||||
|
await loadScriptGroups(); // Reload scripts when profile changes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work directory functions
|
// Work directory functions
|
||||||
function updateWorkDirDisplay() {
|
function updateWorkDirDisplay() {
|
||||||
const input = document.getElementById('workDirPath');
|
const input = document.getElementById('workDirPath');
|
||||||
input.value = currentProfile?.work_dir || '';
|
if (input && currentProfile) {
|
||||||
|
input.value = currentProfile.work_dir || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectWorkDir() {
|
async function selectWorkDir() {
|
||||||
|
@ -101,13 +113,15 @@ async function selectWorkDir() {
|
||||||
// Output functions
|
// Output functions
|
||||||
function showError(message) {
|
function showError(message) {
|
||||||
const output = document.getElementById('outputArea');
|
const output = document.getElementById('outputArea');
|
||||||
output.innerHTML += `\nError: ${message}`;
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
output.innerHTML += `\n[${timestamp}] ERROR: ${message}`;
|
||||||
output.scrollTop = output.scrollHeight;
|
output.scrollTop = output.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSuccess(message) {
|
function showSuccess(message) {
|
||||||
const output = document.getElementById('outputArea');
|
const output = document.getElementById('outputArea');
|
||||||
output.innerHTML += `\nSuccess: ${message}`;
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`;
|
||||||
output.scrollTop = output.scrollHeight;
|
output.scrollTop = output.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,3 +129,23 @@ function clearOutput() {
|
||||||
const output = document.getElementById('outputArea');
|
const output = document.getElementById('outputArea');
|
||||||
output.innerHTML = '';
|
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,4 +1,65 @@
|
||||||
# frontend/static/js/profile.js
|
// frontend/static/js/profile.js
|
||||||
|
|
||||||
|
// Profile functions
|
||||||
|
async function loadProfiles() {
|
||||||
|
try {
|
||||||
|
const profiles = await apiRequest('/profiles');
|
||||||
|
updateProfileSelector(profiles);
|
||||||
|
|
||||||
|
// Select first profile if none selected
|
||||||
|
if (!currentProfile) {
|
||||||
|
const defaultProfile = profiles.find(p => p.id === 'default') || profiles[0];
|
||||||
|
if (defaultProfile) {
|
||||||
|
await selectProfile(defaultProfile.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('Failed to load profiles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProfileSelector(profiles) {
|
||||||
|
const select = document.getElementById('profileSelect');
|
||||||
|
select.innerHTML = profiles.map(profile => `
|
||||||
|
<option value="${profile.id}" ${profile.id === currentProfile?.id ? 'selected' : ''}>
|
||||||
|
${profile.name}
|
||||||
|
</option>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Profile editor modal
|
||||||
let editingProfile = null;
|
let editingProfile = null;
|
||||||
|
|
||||||
|
@ -27,7 +88,7 @@ function showProfileEditor(profile = null) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="workDir">Work Directory</label>
|
<label for="workDir">Work Directory</label>
|
||||||
<input type="text" id="workDir" name="work_dir"
|
<input type="text" id="workDir" name="work_dir"
|
||||||
value="${profile?.work_dir || ''}" required>
|
value="${profile?.work_dir || ''}" readonly>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="llmModel">LLM Model</label>
|
<label for="llmModel">LLM Model</label>
|
||||||
|
@ -48,7 +109,7 @@ function showProfileEditor(profile = null) {
|
||||||
min="0" max="2" step="0.1">
|
min="0" max="2" step="0.1">
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button type="button" onclick="closeProfileEditor()">Cancel</button>
|
<button type="button" onclick="closeModal(this)">Cancel</button>
|
||||||
<button type="submit">Save</button>
|
<button type="submit">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -58,14 +119,6 @@ function showProfileEditor(profile = null) {
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeProfileEditor() {
|
|
||||||
const modal = document.querySelector('.modal');
|
|
||||||
if (modal) {
|
|
||||||
modal.remove();
|
|
||||||
}
|
|
||||||
editingProfile = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveProfile(event) {
|
async function saveProfile(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = event.target;
|
const form = event.target;
|
||||||
|
@ -96,7 +149,7 @@ async function saveProfile(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadProfiles();
|
await loadProfiles();
|
||||||
closeProfileEditor();
|
closeModal(event.target);
|
||||||
showSuccess(`Profile ${editingProfile ? 'updated' : 'created'} successfully`);
|
showSuccess(`Profile ${editingProfile ? 'updated' : 'created'} successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(`Failed to ${editingProfile ? 'update' : 'create'} profile`);
|
showError(`Failed to ${editingProfile ? 'update' : 'create'} profile`);
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
// 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 from API
|
||||||
|
async function loadScriptGroups() {
|
||||||
|
try {
|
||||||
|
scriptGroups = await apiRequest('/scripts');
|
||||||
|
updateScriptGroupsDisplay();
|
||||||
|
} catch (error) {
|
||||||
|
showError('Failed to load script groups');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save group configuration
|
||||||
|
async function saveGroupConfig(event, groupId) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configText = document.getElementById('configData').value;
|
||||||
|
const config = JSON.parse(configText);
|
||||||
|
|
||||||
|
await updateGroupConfig(groupId, config);
|
||||||
|
closeModal(event.target);
|
||||||
|
showSuccess('Group configuration updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to save configuration: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
# frontend/static/js/workdir_config.js
|
// frontend/static/js/workdir_config.js
|
||||||
|
|
||||||
async function getWorkDirConfig() {
|
async function getWorkDirConfig() {
|
||||||
if (!currentProfile?.work_dir) {
|
if (!currentProfile?.work_dir) {
|
||||||
showError('No work directory selected');
|
showError('No work directory selected');
|
||||||
|
@ -51,13 +52,13 @@ async function updateGroupConfig(groupId, settings) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWorkDirConfig() {
|
async function showWorkDirConfig() {
|
||||||
if (!currentProfile?.work_dir) {
|
if (!currentProfile?.work_dir) {
|
||||||
showError('No work directory selected');
|
showError('No work directory selected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWorkDirConfig().then(config => {
|
const config = await getWorkDirConfig();
|
||||||
if (config) {
|
if (config) {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.className = 'modal active';
|
modal.className = 'modal active';
|
||||||
|
@ -88,7 +89,6 @@ function showWorkDirConfig() {
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal(button) {
|
function closeModal(button) {
|
||||||
|
|
|
@ -1,16 +1,33 @@
|
||||||
# frontend/templates/index.html
|
<!DOCTYPE html>
|
||||||
{% extends "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 - Home</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-brand">Local Scripts Web</div>
|
||||||
|
<div class="profile-selector">
|
||||||
|
<select id="profileSelect" onchange="changeProfile()">
|
||||||
|
<option value="">Loading profiles...</option>
|
||||||
|
</select>
|
||||||
|
<button onclick="editProfile()">Edit Profile</button>
|
||||||
|
<button onclick="newProfile()">New Profile</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
{% block title %}Local Scripts Web - Home{% endblock %}
|
<main>
|
||||||
|
<div class="container">
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="work-dir-section">
|
<div class="work-dir-section">
|
||||||
<h2>Work Directory</h2>
|
<h2>Work Directory</h2>
|
||||||
<div class="work-dir-controls">
|
<div class="work-dir-controls">
|
||||||
<input type="text" id="workDirPath" readonly>
|
<input type="text" id="workDirPath" readonly>
|
||||||
<button onclick="selectWorkDir()">Browse</button>
|
<button onclick="selectWorkDir()">Browse</button>
|
||||||
<button onclick="editWorkDirConfig()">Config</button>
|
<button onclick="showWorkDirConfig()">Config</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -23,13 +40,20 @@
|
||||||
|
|
||||||
<div class="output-section">
|
<div class="output-section">
|
||||||
<h2>Output</h2>
|
<h2>Output</h2>
|
||||||
|
<div class="output-controls">
|
||||||
|
<button onclick="clearOutput()">Clear Output</button>
|
||||||
|
</div>
|
||||||
<div id="outputArea" class="output-area">
|
<div id="outputArea" class="output-area">
|
||||||
<!-- Script output will appear here -->
|
<!-- Script output will appear here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
</main>
|
||||||
|
|
||||||
{% block extra_js %}
|
<!-- Scripts -->
|
||||||
<script src="{{ url_for('static', filename='js/profile.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
{% endblock %}
|
<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>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue