Minerva-chat / index.html
AxoneSystem's picture
Upload 2 files
1bfeffc verified
Raw History Blame Contribute Delete
8.69 kB
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minerva Chat</title>
<style>
:root {
--fondo: #f7f5fb; --panel: #ffffff; --texto: #1f1a2e; --suave: #6b6480; --borde: #e3def0;
--yo: #5b3fc4; --yo-texto: #ffffff; --ella: #f0ecfa; --acento: #d4a017;
}
@media (prefers-color-scheme: dark) {
:root {
--fondo: #15121d; --panel: #1e1a29; --texto: #ece8f6; --suave: #a39cb8; --borde: #322c44;
--yo: #7b5ff0; --yo-texto: #ffffff; --ella: #2a2438; --acento: #e8b931;
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--fondo); color: var(--texto);
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
main { max-width: 760px; margin: 0 auto; padding: 16px; min-height: 100vh; display: flex; flex-direction: column; gap: 12px; }
header h1 { margin: 0; font-size: 1.5rem; }
header p { margin: 4px 0 0; color: var(--suave); font-size: .92rem; }
header a { color: var(--yo); }
#estado { background: var(--panel); border: 1px solid var(--borde); border-radius: 12px; padding: 12px; font-size: .92rem; }
#barra { height: 6px; background: var(--borde); border-radius: 3px; margin-top: 8px; overflow: hidden; }
#barra div { height: 100%; width: 0; background: var(--yo); transition: width .2s; }
#chat { flex: 1; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; }
.msg { max-width: 85%; padding: 10px 14px; border-radius: 16px; white-space: pre-wrap; word-wrap: break-word; }
.yo { align-self: flex-end; background: var(--yo); color: var(--yo-texto); border-bottom-right-radius: 4px; }
.ella { align-self: flex-start; background: var(--ella); border-bottom-left-radius: 4px; }
.ella::before { content: "🦉 Minerva"; display: block; font-size: .75rem; font-weight: 600; color: var(--suave); margin-bottom: 2px; }
#ejemplos { display: flex; flex-wrap: wrap; gap: 8px; }
#ejemplos button { background: var(--panel); color: var(--texto); border: 1px solid var(--borde); border-radius: 999px;
padding: 6px 12px; cursor: pointer; font: inherit; font-size: .88rem; }
form { display: flex; gap: 8px; }
input[type=text] { flex: 1; min-width: 0; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--borde);
background: var(--panel); color: var(--texto); font: inherit; }
button.principal { padding: 12px 18px; border: 0; border-radius: 12px; background: var(--yo); color: var(--yo-texto);
font: inherit; font-weight: 600; cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
.ajustes { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; color: var(--suave); font-size: .88rem; }
.ajustes button { background: none; border: 0; color: var(--yo); cursor: pointer; font: inherit; padding: 0; }
footer { color: var(--suave); font-size: .8rem; text-align: center; }
</style>
</head>
<body>
<main>
<header>
<h1>🦉 Minerva v0.1</h1>
<p>Un modelo de lenguaje en español de 30 millones de parámetros, entrenado desde cero en un portátil.
Habla bien, pero <strong>se inventa casi todo</strong>: es un modelo de juguete.
<a href="https://huggingface.co/AxoneSystem/Minerva-v0.1" target="_blank" rel="noopener">Ver el modelo</a></p>
</header>
<div id="estado">Preparando…<div id="barra"><div></div></div></div>
<div id="chat"></div>
<div id="ejemplos">
<button>¿Quién eres?</button>
<button>¿Por qué te llamas Minerva?</button>
<button>¿Qué es la paella?</button>
<button>Cuéntame un cuento corto.</button>
</div>
<form id="formulario">
<input id="entrada" type="text" placeholder="Escribe a Minerva…" autocomplete="off" disabled>
<button class="principal" id="enviar" disabled>Enviar</button>
</form>
<div class="ajustes">
<label>Temperatura <input id="temp" type="range" min="0.1" max="1.5" step="0.1" value="0.7">
<span id="tempValor">0.7</span> <small>(más alta = más loca)</small></label>
<button id="nueva" type="button">Nueva conversación</button>
</div>
<footer>Funciona entero en tu navegador (llama.cpp en WebAssembly): la primera vez descarga ~60 MB.</footer>
</main>
<script type="module">
import { Wllama } from 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.6.1/esm/index.js';
const USUARIO = '<|usuario|>', ASISTENTE = '<|asistente|>', FIN = '<|fin|>';
const MEMORIA = 1200; // caracteres de historial que recuerda (su contexto es de solo 512 tokens)
const $ = (id) => document.getElementById(id);
const estado = $('estado'), barra = document.querySelector('#barra div');
const chat = $('chat'), entrada = $('entrada'), enviar = $('enviar');
let historial = []; // [{yo, ella}]
let ocupada = true;
$('temp').addEventListener('input', (e) => { $('tempValor').textContent = e.target.value; });
$('nueva').addEventListener('click', () => { historial = []; chat.innerHTML = ''; entrada.focus(); });
document.querySelectorAll('#ejemplos button').forEach((b) =>
b.addEventListener('click', () => { if (!ocupada) preguntar(b.textContent); }));
$('formulario').addEventListener('submit', (e) => {
e.preventDefault();
const q = entrada.value.trim();
if (q && !ocupada) { entrada.value = ''; preguntar(q); }
});
function burbuja(clase, texto) {
const d = document.createElement('div');
d.className = 'msg ' + clase;
d.textContent = texto;
chat.appendChild(d);
d.scrollIntoView({ block: 'end' });
return d;
}
function prompt(pregunta) {
// Mismo formato con el que se entrenó: <|usuario|>…<|asistente|>…<|fin|>
let previo = '';
for (let i = historial.length - 1; i >= 0; i--) {
const turno = USUARIO + historial[i].yo + ASISTENTE + historial[i].ella + FIN;
if (previo.length + turno.length > MEMORIA) break;
previo = turno + previo;
}
return previo + USUARIO + pregunta + ASISTENTE;
}
function bloquear(si) {
ocupada = si;
entrada.disabled = enviar.disabled = si;
if (!si) entrada.focus();
}
const URL_MODELO = 'https://huggingface.co/AxoneSystem/Minerva-v0.1/resolve/main/Minerva-v0.1-F16.gguf';
const PARAMS = { n_ctx: 512, n_gpu_layers: 0 };
// Con almacenamiento del navegador (OPFS), wllama guarda el modelo y la próxima visita es instantánea.
// Sin él (algunos navegadores o vistas previas), lo descargamos nosotros y lo cargamos desde memoria.
const hayOPFS = !!(navigator.storage && navigator.storage.getDirectory);
const wllama = new Wllama(
{ default: 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.6.1/esm/wasm/wllama.wasm' },
hayOPFS ? {} : { cacheManager: {}, modelManager: {} },
);
function progreso(loaded, total) {
const pct = total ? Math.round((loaded / total) * 100) : 0;
barra.style.width = pct + '%';
estado.firstChild.textContent = `Descargando a Minerva… ${pct}%`;
}
async function descargarEnMemoria() {
const r = await fetch(URL_MODELO);
if (!r.ok) throw new Error('HTTP ' + r.status);
const total = Number(r.headers.get('content-length')) || 0;
const lector = r.body.getReader();
const trozos = [];
let cargado = 0;
for (;;) {
const { done, value } = await lector.read();
if (done) break;
trozos.push(value);
cargado += value.length;
progreso(cargado, total);
}
return new Blob(trozos);
}
async function preguntar(q) {
bloquear(true);
burbuja('yo', q);
const b = burbuja('ella', '…');
let texto = '';
try {
await wllama.createCompletion({
prompt: prompt(q),
max_tokens: 150,
temperature: parseFloat($('temp').value),
top_k: 30,
stop: [FIN, USUARIO],
stream: true,
onData: (trozo) => {
texto += trozo.choices?.[0]?.text ?? '';
b.textContent = texto.trim() || '…';
b.scrollIntoView({ block: 'end' });
},
});
texto = texto.trim() || '(se ha quedado sin palabras)';
b.textContent = texto;
historial.push({ yo: q, ella: texto });
} catch (err) {
b.textContent = 'Error: ' + (err?.message || err);
}
bloquear(false);
}
try {
estado.firstChild.textContent = 'Descargando a Minerva (~60 MB)…';
if (hayOPFS) {
await wllama.loadModelFromHF(
{ repo: 'AxoneSystem/Minerva-v0.1', file: 'Minerva-v0.1-F16.gguf' },
{ ...PARAMS, progressCallback: ({ loaded, total }) => progreso(loaded, total) },
);
} else {
await wllama.loadModel([await descargarEnMemoria()], PARAMS);
}
estado.style.display = 'none';
bloquear(false);
} catch (err) {
estado.firstChild.textContent = 'No se pudo cargar el modelo: ' + (err?.message || err);
}
</script>
</body>
</html>