ParamManagerScripts/backend/script_groups/OllamaTools/templates/index.html

900 lines
28 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>