900 lines
28 KiB
HTML
900 lines
28 KiB
HTML
<!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> |