Spaces:
Running
Running
File size: 8,693 Bytes
2235a2a 1bfeffc 2235a2a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | <!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>
|