Compare commits
3 Commits
b67851e615
...
784f7c59e2
Author | SHA1 | Date |
---|---|---|
|
784f7c59e2 | |
|
37b8de5dd2 | |
|
64ef308a65 |
|
@ -30,6 +30,7 @@ MANIFEST
|
||||||
*.txt
|
*.txt
|
||||||
*.json
|
*.json
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# Usually these files are written by a python script from a template
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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! 🦙✨**
|
|
@ -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()
|
|
@ -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()">×</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>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -15,5 +15,5 @@
|
||||||
"xref_source_subdir": "source"
|
"xref_source_subdir": "source"
|
||||||
},
|
},
|
||||||
"level3": {},
|
"level3": {},
|
||||||
"working_directory": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia"
|
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport"
|
||||||
}
|
}
|
|
@ -58,5 +58,11 @@
|
||||||
"short_description": "x8_manual_gui.py - Interfaz Manual con GUI para XML→SCL",
|
"short_description": "x8_manual_gui.py - Interfaz Manual con GUI para XML→SCL",
|
||||||
"long_description": "",
|
"long_description": "",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
|
},
|
||||||
|
"test_array_fix.py": {
|
||||||
|
"display_name": "test_array_fix",
|
||||||
|
"short_description": "Sin descripción corta.",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"path": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia",
|
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
|
||||||
"history": [
|
"history": [
|
||||||
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
|
||||||
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia"
|
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue