Se añadió un nuevo registro en `launcher_history.json` para el script `main.py`, incluyendo detalles de ejecución como el estado y el PID. Además, se actualizaron los metadatos de los scripts en `launcher_script_metadata.json` y se incorporó un nuevo script "IPChangerNG" en `launcher_scripts.json`. Se realizó una limpieza en `log.txt`, eliminando entradas obsoletas y optimizando el formato del log.

This commit is contained in:
Miguel 2025-08-08 10:33:33 +02:00
parent 37b8de5dd2
commit 784f7c59e2
7 changed files with 1459 additions and 28363 deletions

View File

@ -0,0 +1,160 @@
# 🦙 Ollama Model Manager
Script web para gestionar modelos de Ollama de forma sencilla e intuitiva.
## 🚀 Características
- **📋 Listar modelos**: Ve todos los modelos instalados con información detallada
- **📥 Descargar modelos**: Descarga nuevos modelos desde la interfaz web
- **🗑️ Eliminar modelos**: Elimina modelos que ya no necesites
- ** Información detallada**: Ve información completa de cada modelo
- **💾 Monitoreo de espacio**: Controla cuánto espacio ocupan tus modelos
- **🔄 Estado en tiempo real**: Verifica el estado de conexión con Ollama
- **🔴 Cierre elegante**: Botón para cerrar la aplicación y la página web
## 📋 Requisitos
1. **Ollama instalado y ejecutándose**
```bash
# Instalar Ollama (si no está instalado)
curl -fsSL https://ollama.ai/install.sh | sh
# Iniciar Ollama
ollama serve
```
2. **Python 3.7+** con las siguientes librerías:
```bash
pip install flask requests
```
## 🏃‍♂️ Uso Rápido
1. **Ejecutar el script**:
```bash
python manager.py
```
2. **Abrir navegador**: El script abrirá automáticamente tu navegador en `http://127.0.0.1:PUERTO`
3. **¡Listo!** Ya puedes gestionar tus modelos de Ollama
## ⚙️ Configuración
El script puede usar un archivo `script_config.json` opcional para personalizar su comportamiento:
```json
{
"ollama_host": "http://localhost:11434",
"auto_open_browser": true,
"level1": {
"default_models": ["llama3.2", "mistral", "codellama"],
"recommended_models": {
"llama3.2": "Modelo general de Meta",
"mistral": "Modelo rápido y eficiente",
"codellama": "Especializado en programación"
}
}
}
```
### Parámetros de configuración:
- `ollama_host`: URL del servidor Ollama (por defecto: `http://localhost:11434`)
- `auto_open_browser`: Abrir navegador automáticamente (por defecto: `true`)
- `recommended_models`: Lista de modelos recomendados con descripciones
## 🔧 Integración con el Sistema Principal
Este script está diseñado para integrarse con el sistema ParamManagerScripts:
1. **Colocar en directorio de proyectos**: El script se puede colocar en cualquier proyecto del launcher
2. **Ejecutar como script web**: Usar el endpoint `execute-python-web-script` del frontend principal
3. **Configuración automática**: Si está en el sistema principal, carga configuración automáticamente
## 📖 API Endpoints
El script expone los siguientes endpoints:
- `GET /` - Interfaz web principal
- `GET /api/status` - Estado de conexión con Ollama
- `GET /api/models` - Lista de modelos instalados
- `GET /api/models/<name>/info` - Información detallada de un modelo
- `POST /api/models/pull` - Descargar un modelo
- `DELETE /api/models/<name>` - Eliminar un modelo
- `POST /_shutdown` - Cerrar la aplicación (uso interno)
## 🎨 Interfaz Web
La interfaz incluye:
- **Panel de estado**: Muestra si Ollama está conectado y estadísticas generales
- **Botón de cierre**: Permite cerrar la aplicación de forma elegante
- **Barra de acciones**: Campo para descargar nuevos modelos y botón de actualización
- **Grid de modelos**: Tarjetas con información de cada modelo instalado
- **Modal de información**: Detalles completos del modelo (licencia, parámetros, template)
- **Mensajes**: Notificaciones de éxito y error
## 🔍 Modelos Recomendados
### Para uso general:
- **llama3.2** - Modelo versátil de Meta, excelente para conversaciones
- **mistral** - Rápido y eficiente, bueno para tareas generales
- **gemma2** - De Google, bueno para análisis y razonamiento
### Para programación:
- **codellama** - Especializado en código y programación
- **deepseek-coder** - Excelente para tareas de desarrollo
### Para recursos limitados:
- **phi3** - Modelo compacto de Microsoft
- **tinyllama** - Muy pequeño, ideal para pruebas
### Multilingüe:
- **qwen2.5** - Excelente soporte para español y otros idiomas
## 🐛 Solución de Problemas
### Ollama no se conecta
```bash
# Verificar que Ollama esté ejecutándose
ollama serve
# Verificar modelos instalados
ollama list
```
### Error de puerto ocupado
El script encuentra automáticamente un puerto libre. Si hay problemas, reinicia el script.
### Descarga lenta de modelos
Las descargas pueden tardar varios minutos dependiendo del tamaño del modelo y la velocidad de internet. Los modelos grandes (>7B parámetros) pueden ocupar varios GB.
## 📝 Logs y Debug
Para activar logs detallados, modifica `script_config.json`:
```json
{
"level3": {
"debug_mode": true,
"log_api_calls": true
}
}
```
## 🤝 Contribuir
Este script es parte del sistema ParamManagerScripts. Para mejoras o reportar problemas:
1. Crea un issue describiendo el problema o mejora
2. Si es un bug, incluye logs y pasos para reproducir
3. Para nuevas características, explica el caso de uso
## 📄 Licencia
Este script es parte del proyecto ParamManagerScripts y sigue la misma licencia del proyecto principal.
---
**¡Disfruta gestionando tus modelos de Ollama! 🦙✨**

View File

@ -0,0 +1,325 @@
#!/usr/bin/env python3
"""
Ollama Model Manager - Script Web para gestionar modelos de Ollama
Permite listar, descargar y eliminar modelos de Ollama con interfaz web.
"""
import os
import sys
import socket
import threading
import webbrowser
import time
import requests
from datetime import datetime
from typing import Dict, Any
from flask import Flask, render_template, jsonify, request
# Configuración del path para importar utilidades del proyecto principal
script_root = os.path.dirname(os.path.dirname(__file__))
sys.path.append(script_root)
# Importar utilidades del proyecto (opcional, para configuración)
try:
from ParamManagerScripts.backend.script_utils import load_configuration
HAS_CONFIG = True
except ImportError:
HAS_CONFIG = False
msg = (
"⚠️ No se pudo importar load_configuration, " "usando configuración por defecto"
)
print(msg)
class OllamaManager:
"""Clase para gestionar modelos de Ollama"""
def __init__(self, base_url: str = "http://localhost:11434"):
self.base_url = base_url.rstrip("/")
self.api_url = f"{self.base_url}/api"
def is_ollama_running(self) -> bool:
"""Verificar si Ollama está ejecutándose"""
try:
response = requests.get(f"{self.base_url}/api/tags", timeout=5)
return response.status_code == 200
except requests.exceptions.RequestException:
return False
def list_models(self) -> Dict[str, Any]:
"""Listar todos los modelos instalados"""
try:
response = requests.get(f"{self.api_url}/tags", timeout=10)
if response.status_code == 200:
data = response.json()
models = data.get("models", [])
# Calcular tamaño total
total_size = sum(model.get("size", 0) for model in models)
return {
"status": "success",
"models": models,
"total_models": len(models),
"total_size": total_size,
"total_size_human": self._format_bytes(total_size),
}
else:
error_msg = f"Error HTTP: {response.status_code}"
return {"status": "error", "message": error_msg}
except requests.exceptions.RequestException as e:
error_msg = f"Error de conexión: {str(e)}"
return {"status": "error", "message": error_msg}
def pull_model(self, model_name: str) -> Dict[str, Any]:
"""Descargar un modelo"""
try:
# Iniciar descarga
response = requests.post(
f"{self.api_url}/pull",
json={"name": model_name},
stream=True,
timeout=300, # 5 minutos timeout
)
if response.status_code == 200:
msg = f"Descarga de '{model_name}' iniciada"
return {"status": "success", "message": msg}
else:
error_msg = f"Error al descargar: {response.status_code}"
return {"status": "error", "message": error_msg}
except requests.exceptions.RequestException as e:
error_msg = f"Error de conexión: {str(e)}"
return {"status": "error", "message": error_msg}
def delete_model(self, model_name: str) -> Dict[str, Any]:
"""Eliminar un modelo"""
try:
response = requests.delete(
f"{self.api_url}/delete", json={"name": model_name}, timeout=30
)
if response.status_code == 200:
msg = f"Modelo '{model_name}' eliminado correctamente"
return {"status": "success", "message": msg}
else:
error_msg = f"Error al eliminar: {response.status_code}"
return {"status": "error", "message": error_msg}
except requests.exceptions.RequestException as e:
error_msg = f"Error de conexión: {str(e)}"
return {"status": "error", "message": error_msg}
def get_model_info(self, model_name: str) -> Dict[str, Any]:
"""Obtener información detallada de un modelo"""
try:
response = requests.post(
f"{self.api_url}/show", json={"name": model_name}, timeout=10
)
if response.status_code == 200:
return {"status": "success", "info": response.json()}
else:
error_msg = f"Error al obtener info: {response.status_code}"
return {"status": "error", "message": error_msg}
except requests.exceptions.RequestException as e:
error_msg = f"Error de conexión: {str(e)}"
return {"status": "error", "message": error_msg}
def _format_bytes(self, bytes_size: int) -> str:
"""Formatear bytes a formato legible"""
if bytes_size == 0:
return "0 B"
for unit in ["B", "KB", "MB", "GB", "TB"]:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def find_free_port() -> int:
"""Encontrar un puerto libre"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
return s.getsockname()[1]
def create_app() -> Flask:
"""Crear la aplicación Flask"""
app = Flask(__name__)
# Configurar directorio de templates
template_dir = os.path.join(os.path.dirname(__file__), "templates")
if not os.path.exists(template_dir):
os.makedirs(template_dir)
app.template_folder = template_dir
# Configurar directorio de archivos estáticos
static_dir = os.path.join(os.path.dirname(__file__), "static")
if not os.path.exists(static_dir):
os.makedirs(static_dir)
app.static_folder = static_dir
# Inicializar gestor de Ollama
ollama_manager = OllamaManager()
@app.route("/")
def index():
"""Página principal"""
return render_template("index.html")
@app.route("/api/status")
def api_status():
"""Verificar estado de Ollama"""
is_running = ollama_manager.is_ollama_running()
return jsonify(
{
"status": "success",
"ollama_running": is_running,
"timestamp": datetime.now().isoformat(),
}
)
@app.route("/api/models")
def api_models():
"""Listar modelos"""
return jsonify(ollama_manager.list_models())
@app.route("/api/models/<model_name>/info")
def api_model_info(model_name):
"""Obtener información de un modelo"""
return jsonify(ollama_manager.get_model_info(model_name))
@app.route("/api/models/pull", methods=["POST"])
def api_pull_model():
"""Descargar un modelo"""
data = request.get_json()
if not data or "name" not in data:
error_msg = "Nombre del modelo requerido"
return jsonify({"status": "error", "message": error_msg}), 400
model_name = data["name"].strip()
if not model_name:
error_msg = "Nombre del modelo no puede estar vacío"
return jsonify({"status": "error", "message": error_msg}), 400
result = ollama_manager.pull_model(model_name)
return jsonify(result)
@app.route("/api/models/<model_name>", methods=["DELETE"])
def api_delete_model(model_name):
"""Eliminar un modelo"""
result = ollama_manager.delete_model(model_name)
return jsonify(result)
@app.errorhandler(404)
def not_found(error):
error_msg = "Endpoint no encontrado"
return jsonify({"status": "error", "message": error_msg}), 404
@app.errorhandler(500)
def internal_error(error):
error_msg = "Error interno del servidor"
return jsonify({"status": "error", "message": error_msg}), 500
@app.route("/_shutdown", methods=["POST"])
def shutdown_route():
"""Endpoint interno para cerrar la aplicación"""
print("🛑 Solicitud de cierre recibida desde la interfaz web")
def shutdown_server():
time.sleep(0.1) # Pequeña pausa para permitir respuesta HTTP
try:
# Intentar cerrar el servidor Flask de manera elegante
import os
import signal
os.kill(os.getpid(), signal.SIGINT)
except Exception as e:
print(f"Error al cerrar servidor: {e}")
# Fallback: forzar salida
os._exit(0)
# Ejecutar cierre en hilo separado
shutdown_thread = threading.Thread(target=shutdown_server, daemon=True)
shutdown_thread.start()
return (
jsonify(
{"status": "success", "message": "Cerrando Ollama Model Manager..."}
),
200,
)
return app
def main():
"""Función principal"""
print("🦙 Ollama Model Manager - Iniciando...")
# Cargar configuración si está disponible
configs = {}
if HAS_CONFIG:
try:
configs = load_configuration()
print("✅ Configuración cargada correctamente")
except Exception as e:
print(f"⚠️ Error al cargar configuración: {e}")
# Obtener configuración
ollama_host = configs.get("ollama_host", "http://localhost:11434")
auto_open_browser = configs.get("auto_open_browser", True)
# Crear aplicación Flask
app = create_app()
# Encontrar puerto libre
port = find_free_port()
# URL de la aplicación
app_url = f"http://127.0.0.1:{port}"
print(f"🌐 Servidor iniciado en: {app_url}")
print(f"🦙 Ollama Host: {ollama_host}")
print("📋 Funcionalidades disponibles:")
print(" - Listar modelos instalados")
print(" - Ver información detallada de modelos")
print(" - Descargar nuevos modelos")
print(" - Eliminar modelos existentes")
print(" - Monitoreo de espacio ocupado")
print()
print("⏹️ Presiona Ctrl+C para cerrar el servidor")
# Abrir navegador automáticamente
if auto_open_browser:
def open_browser():
time.sleep(1.0) # Esperar a que Flask inicie
try:
webbrowser.open(app_url)
print(f"🌐 Navegador abierto en: {app_url}")
except Exception as e:
print(f"⚠️ No se pudo abrir el navegador automáticamente: {e}")
print(f"🌐 Abrir manualmente: {app_url}")
timer = threading.Timer(1.0, open_browser)
timer.start()
try:
# Iniciar servidor Flask
app.run(host="127.0.0.1", port=port, debug=False, use_reloader=False)
except KeyboardInterrupt:
print("\n🛑 Cerrando Ollama Model Manager...")
except Exception as e:
print(f"❌ Error al iniciar el servidor: {e}")
finally:
print("👋 Ollama Model Manager cerrado")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,900 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🦙 Ollama Model Manager</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.header h1 {
color: #4a5568;
font-size: 2.5em;
margin-bottom: 10px;
text-align: center;
}
.header p {
color: #718096;
text-align: center;
font-size: 1.1em;
}
.status-bar {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.online {
background: #48bb78;
}
.status-dot.offline {
background: #f56565;
animation: none;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.stats {
display: flex;
gap: 20px;
color: #4a5568;
font-size: 0.9em;
}
.shutdown-section {
margin-left: auto;
}
.main-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
flex-wrap: wrap;
gap: 15px;
}
.download-section {
display: flex;
gap: 10px;
align-items: center;
}
.input-group {
position: relative;
}
input[type="text"] {
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1em;
width: 300px;
transition: border-color 0.3s ease;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
color: white;
}
.btn-info {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
color: white;
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.models-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.model-card {
background: #f7fafc;
border-radius: 12px;
padding: 20px;
border: 2px solid #e2e8f0;
transition: all 0.3s ease;
}
.model-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
border-color: #667eea;
}
.model-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.model-name {
font-size: 1.2em;
font-weight: 700;
color: #2d3748;
word-break: break-word;
}
.model-tag {
background: #667eea;
color: white;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.8em;
font-weight: 600;
}
.model-info {
margin-bottom: 15px;
color: #4a5568;
}
.model-info div {
margin-bottom: 5px;
font-size: 0.9em;
}
.model-size {
font-weight: 600;
color: #2d3748;
}
.model-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.loading {
text-align: center;
padding: 40px;
color: #718096;
}
.spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-radius: 50%;
border-top-color: #667eea;
animation: spin 1s ease-in-out infinite;
margin-bottom: 15px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error {
background: #fed7d7;
color: #c53030;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #f56565;
}
.success {
background: #c6f6d5;
color: #2f855a;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #48bb78;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #718096;
}
.empty-state h3 {
font-size: 1.5em;
margin-bottom: 15px;
color: #4a5568;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e2e8f0;
}
.modal-header h2 {
color: #2d3748;
margin: 0;
}
.close {
color: #a0aec0;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
}
.close:hover {
color: #4a5568;
}
.info-item {
margin-bottom: 15px;
padding: 10px;
background: #f7fafc;
border-radius: 8px;
}
.info-label {
font-weight: 600;
color: #4a5568;
margin-bottom: 5px;
}
.info-value {
color: #2d3748;
word-break: break-word;
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.models-grid {
grid-template-columns: 1fr;
}
input[type="text"] {
width: 100%;
}
.actions-bar {
flex-direction: column;
align-items: stretch;
}
.download-section {
flex-direction: column;
}
.status-bar {
flex-direction: column;
gap: 15px;
text-align: center;
}
.shutdown-section {
margin-left: 0;
align-self: center;
}
.stats {
justify-content: center;
text-align: center;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>🦙 Ollama Model Manager</h1>
<p>Gestiona tus modelos de Ollama de forma sencilla</p>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="status-dot"></div>
<span id="status-text">Verificando conexión...</span>
</div>
<div class="stats" id="stats">
<span>📊 Cargando estadísticas...</span>
</div>
<div class="shutdown-section">
<button class="btn btn-danger" id="shutdown-btn" onclick="shutdownApplication()"
title="Cerrar aplicación">
🔴 Cerrar
</button>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Actions Bar -->
<div class="actions-bar">
<h2>Modelos Instalados</h2>
<div class="download-section">
<div class="input-group">
<input type="text" id="model-name-input" placeholder="Nombre del modelo (ej: llama3.2, mistral)"
autocomplete="off">
</div>
<button class="btn btn-primary" id="download-btn" onclick="downloadModel()">
📥 Descargar
</button>
<button class="btn btn-secondary" onclick="refreshModels()">
🔄 Actualizar
</button>
</div>
</div>
<!-- Messages -->
<div id="messages"></div>
<!-- Models Grid -->
<div id="models-container">
<div class="loading">
<div class="spinner"></div>
<div>Cargando modelos...</div>
</div>
</div>
</div>
</div>
<!-- Modal para información del modelo -->
<div id="info-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>📋 Información del Modelo</h2>
<span class="close" onclick="closeInfoModal()">&times;</span>
</div>
<div id="info-modal-body">
<div class="loading">
<div class="spinner"></div>
<div>Cargando información...</div>
</div>
</div>
</div>
</div>
<script>
// Estado global de la aplicación
let currentModels = [];
let isLoading = false;
// Inicializar aplicación
document.addEventListener('DOMContentLoaded', function () {
checkOllamaStatus();
loadModels();
// Configurar entrada de texto para descargar modelos
document.getElementById('model-name-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
downloadModel();
}
});
});
// Verificar estado de Ollama
async function checkOllamaStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
if (data.ollama_running) {
statusDot.className = 'status-dot online';
statusText.textContent = '🟢 Ollama conectado';
} else {
statusDot.className = 'status-dot offline';
statusText.textContent = '🔴 Ollama desconectado';
}
} catch (error) {
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
statusDot.className = 'status-dot offline';
statusText.textContent = '❌ Error de conexión';
}
}
// Cargar modelos
async function loadModels() {
if (isLoading) return;
isLoading = true;
const container = document.getElementById('models-container');
// Mostrar spinner de carga
container.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<div>Cargando modelos...</div>
</div>
`;
try {
const response = await fetch('/api/models');
const data = await response.json();
if (data.status === 'success') {
currentModels = data.models || [];
updateStats(data);
renderModels(currentModels);
} else {
showError(`Error al cargar modelos: ${data.message}`);
container.innerHTML = `
<div class="error">
❌ Error al cargar modelos: ${data.message}
</div>
`;
}
} catch (error) {
showError(`Error de conexión: ${error.message}`);
container.innerHTML = `
<div class="error">
❌ Error de conexión: ${error.message}
</div>
`;
} finally {
isLoading = false;
}
}
// Actualizar estadísticas
function updateStats(data) {
const statsElement = document.getElementById('stats');
statsElement.innerHTML = `
<span>📦 ${data.total_models} modelos</span>
<span>💾 ${data.total_size_human}</span>
`;
}
// Renderizar modelos
function renderModels(models) {
const container = document.getElementById('models-container');
if (models.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>🦙 No hay modelos instalados</h3>
<p>Descarga tu primer modelo usando el campo de arriba</p>
<p><strong>Modelos populares:</strong> llama3.2, mistral, codellama, phi3</p>
</div>
`;
return;
}
const modelsHTML = models.map(model => {
const sizeFormatted = formatBytes(model.size || 0);
const modifiedDate = model.modified_at ?
new Date(model.modified_at).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'short',
day: 'numeric'
}) : 'N/A';
return `
<div class="model-card">
<div class="model-header">
<div class="model-name">${model.name}</div>
<div class="model-tag">LLM</div>
</div>
<div class="model-info">
<div class="model-size">📦 Tamaño: ${sizeFormatted}</div>
<div>📅 Modificado: ${modifiedDate}</div>
<div>🏷️ Digest: ${(model.digest || 'N/A').substring(0, 12)}...</div>
</div>
<div class="model-actions">
<button class="btn btn-info" onclick="showModelInfo('${model.name}')">
Info
</button>
<button class="btn btn-danger" onclick="deleteModel('${model.name}')">
🗑️ Eliminar
</button>
</div>
</div>
`;
}).join('');
container.innerHTML = `<div class="models-grid">${modelsHTML}</div>`;
}
// Descargar modelo
async function downloadModel() {
const input = document.getElementById('model-name-input');
const modelName = input.value.trim();
if (!modelName) {
showError('Por favor, introduce el nombre del modelo');
return;
}
const downloadBtn = document.getElementById('download-btn');
downloadBtn.disabled = true;
downloadBtn.innerHTML = '⏳ Descargando...';
try {
const response = await fetch('/api/models/pull', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: modelName })
});
const data = await response.json();
if (data.status === 'success') {
showSuccess(`✅ Descarga de "${modelName}" iniciada. Puede tardar varios minutos...`);
input.value = '';
// Recargar modelos después de unos segundos
setTimeout(() => {
loadModels();
}, 3000);
} else {
showError(`❌ Error al descargar: ${data.message}`);
}
} catch (error) {
showError(`❌ Error de conexión: ${error.message}`);
} finally {
downloadBtn.disabled = false;
downloadBtn.innerHTML = '📥 Descargar';
}
}
// Eliminar modelo
async function deleteModel(modelName) {
if (!confirm(`¿Estás seguro de que quieres eliminar el modelo "${modelName}"?`)) {
return;
}
try {
const response = await fetch(`/api/models/${encodeURIComponent(modelName)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.status === 'success') {
showSuccess(`✅ Modelo "${modelName}" eliminado correctamente`);
loadModels();
} else {
showError(`❌ Error al eliminar: ${data.message}`);
}
} catch (error) {
showError(`❌ Error de conexión: ${error.message}`);
}
}
// Mostrar información del modelo
async function showModelInfo(modelName) {
const modal = document.getElementById('info-modal');
const modalBody = document.getElementById('info-modal-body');
modal.style.display = 'block';
modalBody.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<div>Cargando información...</div>
</div>
`;
try {
const response = await fetch(`/api/models/${encodeURIComponent(modelName)}/info`);
const data = await response.json();
if (data.status === 'success') {
const info = data.info;
modalBody.innerHTML = `
<div class="info-item">
<div class="info-label">Nombre del Modelo</div>
<div class="info-value">${modelName}</div>
</div>
${info.license ? `
<div class="info-item">
<div class="info-label">Licencia</div>
<div class="info-value">${info.license}</div>
</div>
` : ''}
${info.modelfile ? `
<div class="info-item">
<div class="info-label">Modelfile</div>
<div class="info-value"><pre style="white-space: pre-wrap; font-size: 0.9em;">${info.modelfile}</pre></div>
</div>
` : ''}
${info.parameters ? `
<div class="info-item">
<div class="info-label">Parámetros</div>
<div class="info-value"><pre style="white-space: pre-wrap; font-size: 0.9em;">${info.parameters}</pre></div>
</div>
` : ''}
${info.template ? `
<div class="info-item">
<div class="info-label">Template</div>
<div class="info-value"><pre style="white-space: pre-wrap; font-size: 0.9em;">${info.template}</pre></div>
</div>
` : ''}
`;
} else {
modalBody.innerHTML = `
<div class="error">
❌ Error al cargar información: ${data.message}
</div>
`;
}
} catch (error) {
modalBody.innerHTML = `
<div class="error">
❌ Error de conexión: ${error.message}
</div>
`;
}
}
// Cerrar modal de información
function closeInfoModal() {
document.getElementById('info-modal').style.display = 'none';
}
// Cerrar modal al hacer clic fuera de él
window.onclick = function (event) {
const modal = document.getElementById('info-modal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
// Actualizar modelos
function refreshModels() {
checkOllamaStatus();
loadModels();
}
// Mostrar mensaje de error
function showError(message) {
showMessage(message, 'error');
}
// Mostrar mensaje de éxito
function showSuccess(message) {
showMessage(message, 'success');
}
// Mostrar mensaje genérico
function showMessage(message, type) {
const messagesContainer = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = type;
messageDiv.textContent = message;
messagesContainer.appendChild(messageDiv);
// Remover mensaje después de 5 segundos
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 5000);
}
// Formatear bytes
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// Cerrar aplicación
async function shutdownApplication() {
if (!confirm('¿Estás seguro de que quieres cerrar la aplicación?')) {
return;
}
const shutdownBtn = document.getElementById('shutdown-btn');
shutdownBtn.disabled = true;
shutdownBtn.innerHTML = '⏳ Cerrando...';
try {
// Mostrar mensaje de cierre
showSuccess('🛑 Cerrando Ollama Model Manager...');
// Esperar un momento para que se vea el mensaje
await new Promise(resolve => setTimeout(resolve, 1000));
// Enviar solicitud de cierre al servidor
const response = await fetch('/_shutdown', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
// Intentar cerrar la pestaña/ventana del navegador
try {
window.close();
} catch (e) {
// Si no se puede cerrar, mostrar mensaje
document.body.innerHTML = `
<div style="
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-family: 'Segoe UI', sans-serif;
text-align: center;
padding: 20px;
">
<h1 style="font-size: 3em; margin-bottom: 20px;">🦙</h1>
<h2 style="margin-bottom: 20px;">Ollama Model Manager Cerrado</h2>
<p style="font-size: 1.2em; margin-bottom: 30px;">
La aplicación se ha cerrado correctamente.
</p>
<p style="opacity: 0.8;">
Puedes cerrar esta pestaña manualmente.
</p>
</div>
`;
}
} else {
throw new Error('Error en la respuesta del servidor');
}
} catch (error) {
console.error('Error cerrando aplicación:', error);
showError('❌ Error al cerrar la aplicación');
// Restaurar botón
shutdownBtn.disabled = false;
shutdownBtn.innerHTML = '🔴 Cerrar';
}
}
</script>
</body>
</html>

View File

@ -1,5 +1,18 @@
{ {
"history": [ "history": [
{
"id": "23248897",
"group_id": "2",
"script_name": "main.py",
"executed_date": "2025-08-07T16:29:51.631911Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 22124,
"execution_time": null
},
{ {
"id": "e8aa982b", "id": "e8aa982b",
"group_id": "2", "group_id": "2",

File diff suppressed because one or more lines are too long

View File

@ -46,6 +46,19 @@
"tags": [], "tags": [],
"created_date": "2025-06-11T22:55:41.635081Z", "created_date": "2025-06-11T22:55:41.635081Z",
"updated_date": "2025-06-11T22:55:41.635081Z" "updated_date": "2025-06-11T22:55:41.635081Z"
},
{
"name": "IPChangerNG",
"description": "",
"category": "Utilidades",
"version": "1.0",
"python_env": "general",
"directory": "D:/Proyectos/Scripts/IPchangeNG",
"id": "f48628b2",
"author": "",
"tags": [],
"created_date": "2025-08-03T10:31:24.531950Z",
"updated_date": "2025-08-03T10:31:24.531950Z"
} }
], ],
"categories": { "categories": {

28388
data/log.txt

File diff suppressed because it is too large Load Diff