
Chismes del Lince 03: Gata Montesa se hace mayor
Introducción
En el Chisme 02, la Gata Montesa dio sus primeros pasos con una cuenta compartida y sin tener que tocar ni una línea de configuración. Nos sirvió para probar la idea, pero teníamos poco control: no elegíamos el modelo exacto, y cada persona que abría la página necesitaba su propia cuenta de Puter.
En este chisme la Gata Montesa se hace mayor. Vamos a montarle su propio backend: una piecita de código que vive en la nube, guarda nuestra clave de la IA a buen recaudo y decide exactamente qué modelo responde. Es un paso más técnico que los anteriores, con cuenta en Cloudflare, clave de API y algo de código, pero lo hemos escrito para que lo pueda seguir cualquiera, paso a paso, aunque sea la primera vez que oyes la palabra “backend”. Al final tendrás tu propia Gata Montesa, con tu propia clave de Gemini, sin que nadie más pueda usarla ni verla.
Paso 1: consigue tu clave de la API de Gemini
Entra en aistudio.google.com, inicia sesión y busca “Get API key” u “Obtener clave de API”. Genera una clave y guárdala en un sitio seguro. Empieza por AIza.... Nunca la pegues en el HTML.
Paso 2: crea tu cuenta de Cloudflare
Entra en dash.cloudflare.com y regístrate gratis. No hace falta mover tu dominio a Cloudflare; el Worker vive en su propia dirección (algo.workers.dev).
Paso 3: crea el Worker desde el panel
Workers y Pages → Crear → Crear Worker. Ponle un nombre, por ejemplo gata-montesa, y pulsa Desplegar para tener la plantilla inicial. Todo se edita en el navegador, sin instalar nada.
Paso 4: guarda tu clave como secreto
Configuración → Variables y secretos → Añadir. Nombre exacto: GEMINI_API_KEY. Pega tu clave, marca Cifrar/Secret y guarda.
Paso 5: sustituye TODO el código del Worker por este
Borra lo que trae el editor de ejemplo y pega el archivo completo (te lo dejo también descargable como worker-gata-montesa.js para copiar y pegar sin errores):
javascript
const MODELO = 'gemini-3.8-flash';
const SISTEMA = 'Eres la Gata Montesa, la asistente de inteligencia artificial del Lince ' +
'de la Noche, superhéroe de Sierra Bermeja (Estepona, Málaga), del proyecto educativo ' +
'MonteSTeam. Hablas en español, con humor amable y un toque andaluz, en 2 o 3 frases ' +
'como máximo. Si no sabes algo, lo dices. No pidas ni guardes datos personales.';
// ⚠️ CAMBIA ESTO por tu propio dominio antes de publicarlo
const ORIGEN_PERMITIDO = 'https://montesteam.org';
export default {
async fetch(request, env) {
const cabeceras = {
'Access-Control-Allow-Origin': ORIGEN_PERMITIDO,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (request.method === 'OPTIONS') return new Response(null, {headers: cabeceras});
if (request.method !== 'POST') return new Response('Método no permitido', {status: 405, headers: cabeceras});
let cuerpo;
try { cuerpo = await request.json(); } catch { return new Response('JSON inválido', {status: 400, headers: cabeceras}); }
const historial = Array.isArray(cuerpo.historial) ? cuerpo.historial.slice(-8) : [];
if (!historial.length) return new Response('Falta el mensaje', {status: 400, headers: cabeceras});
const contents = historial.map(m => ({
role: m.role === 'assistant' ? 'model' : 'user',
parts: [{text: String(m.content).slice(0, 1000)}],
}));
const url = `https://generativelanguage.googleapis.com/v1beta/models/${MODELO}:generateContent`;
const r = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'x-goog-api-key': env.GEMINI_API_KEY},
body: JSON.stringify({system_instruction: {parts: [{text: SISTEMA}]}, contents}),
});
if (!r.ok) return new Response('Error al llamar a Gemini', {status: 502, headers: cabeceras});
const datos = await r.json();
const texto = datos?.candidates?.[0]?.content?.parts?.map(p => p.text).join('') || '';
return new Response(JSON.stringify({texto}), {headers: {...cabeceras, 'Content-Type': 'application/json'}});
},
};
Pulsa Desplegar. Copia la dirección que aparece arriba del editor, tipo https://gata-montesa.tu-usuario.workers.dev. La necesitas ahora mismo.
Paso 6: prueba el Worker solo, antes de tocar la webapp
Abre la consola del navegador (F12 → Console) y pega esto, cambiando la URL por la tuya:
javascript
fetch('https://gata-montesa.tu-usuario.workers.dev', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({historial: [{role: 'user', content: '¿Qué comen los linces?'}]})
}).then(r => r.json()).then(console.log);
Si sale un objeto con texto, funciona. Si da error de CORS, revisa que ORIGEN_PERMITIDO del Worker coincide con la página desde la que pruebas (para pruebas rápidas puedes poner '*' un momento y volver a dejarlo en https://montesteam.org antes de publicar).
Paso 7: la única línea que hay que cambiar en el archivo
Ya tienes el archivo completo, lincecbgemini.html, listo para publicar. Solo hay una línea que tienes que editar tú, marcada bien visible cerca del principio del <script>:
javascript
const WORKER = 'https://gata-montesa.tu-usuario.workers.dev';
Cambia esa dirección por la tuya del paso 5, y ya está. El resto del archivo no hay que tocarlo.
Paso 8: sube y prueba desde el móvil
Sube lincecbgemini.html a tu carpeta app, ábrelo desde el móvil y pregúntale algo. Si no responde, mira la consola: casi siempre es el nombre del secreto (GEMINI_API_KEY, exacto) o el ORIGEN_PERMITIDO del Worker.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#7a2e1d">
<title>Chisme 03 · Gata Montesa, asistente IA del Lince de la Noche</title>
<style>
:root{--bg:#f1e8e0;--card:#fbf7f3;--ink:#2e1d17;--mute:#7a6558;--line:#dccbbd;--pine:#7a2e1d;--sea:#a2422a;--ocre:#b9770e;--bad:#8a1c1c}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;display:flex;flex-direction:column;padding:env(safe-area-inset-top) 0 env(safe-area-inset-bottom)}
header{background:var(--pine);color:#fff5ee;padding:18px 18px 16px;flex:none}
header h1{margin:0;font-size:1.4rem;letter-spacing:-.01em}
header p{margin:4px 0 0;color:#f0cdbf;font-size:.9rem}
#chat{flex:1;overflow-y:auto;padding:14px;max-width:640px;width:100%;margin:0 auto}
.b{max-width:88%;padding:10px 13px;border-radius:12px;margin:0 0 10px;white-space:pre-wrap;overflow-wrap:anywhere}
.lince{background:var(--card);border:1px solid var(--line)}
.yo{background:var(--sea);color:#fff;margin-left:auto}
.err{background:#f6e3df;border:1px solid #dfb1a8;color:var(--bad)}
.pens{color:var(--mute);font-style:italic}
#sug{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 10px}
#sug button{background:transparent;color:var(--pine);border:1.5px solid var(--pine);font-weight:500;font-size:.9rem;padding:8px 12px;min-height:40px}
#f{flex:none;background:var(--card);border-top:1px solid var(--line);padding:10px 14px}
#f div{max-width:640px;margin:0 auto;display:flex;gap:8px}
input{flex:1;min-width:0;font:inherit;padding:12px;border:1.5px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}
button{font:inherit;font-weight:600;border:0;border-radius:8px;padding:12px 16px;background:var(--pine);color:#fff;min-height:46px;cursor:pointer}
button:disabled{opacity:.5}
button:focus-visible,input:focus-visible{outline:3px solid var(--ocre);outline-offset:2px}
small{display:block;max-width:640px;margin:6px auto 0;color:var(--mute);font-size:.78rem}
</style>
</head>
<body>
<header>
<h1>Gata Montesa</h1>
<p>Asistente IA de El Lince de la Noche</p>
</header>
<div id="chat" aria-live="polite">
<div class="b lince">¡Ea! Soy la Gata Montesa, la asistente IA del Lince de la Noche. Pregúntame lo que quieras del monte, de Estepona o de ciencia, y si te apetece, hasta te cuento un chisme.</div>
<div id="sug">
<button type="button">¿Qué come un lince ibérico?</button>
<button type="button">¿Por qué es importante proteger el cielo de noche?</button>
<button type="button">Cuéntame un chisme del monte</button>
</div>
</div>
<form id="f">
<div>
<input id="q" autocomplete="off" placeholder="Escríbele a la Gata Montesa…" aria-label="Tu mensaje" maxlength="400">
<button id="env" type="submit">Enviar</button>
</div>
<small>Responde una IA y puede equivocarse. No escribas datos personales.</small>
</form>
<script>
/* ============================================================
CAMBIA ESTO por la dirección de tu propio Worker de Cloudflare
(la que copiaste al final del Paso 5 del tutorial)
============================================================ */
const WORKER = 'https://black-flower-85bb.jluserotrabajo.workers.dev';
const $ = id => document.getElementById(id);
const chat = $('chat'), q = $('q'), env = $('env');
let historial = [];
function burbuja(txt, clase) {
const d = document.createElement('div');
d.className = 'b ' + clase; d.textContent = txt;
chat.appendChild(d); chat.scrollTop = chat.scrollHeight;
return d;
}
async function enviar(msg) {
msg = msg.trim();
if (!msg) return;
$('sug').hidden = true;
burbuja(msg, 'yo');
historial.push({role: 'user', content: msg});
historial = historial.slice(-8);
q.value = ''; env.disabled = true;
const p = burbuja('La Gata Montesa está pensando…', 'lince pens');
try {
const r = await fetch(WORKER, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({historial}),
});
if (!r.ok) throw new Error('fallo del worker');
const {texto} = await r.json();
const t = (texto || '').trim() || 'Me he quedado sin palabras. Prueba otra vez.';
p.className = 'b lince'; p.textContent = t;
historial.push({role: 'assistant', content: t});
} catch (e) {
p.className = 'b err';
p.textContent = 'No he podido responder. Prueba otra vez en un momento.';
historial.pop();
}
env.disabled = false; q.focus(); chat.scrollTop = chat.scrollHeight;
}
$('f').onsubmit = e => { e.preventDefault(); enviar(q.value); };
document.querySelectorAll('#sug button').forEach(b => b.onclick = () => enviar(b.textContent));
</script>
</body>
</html>
Abrir la Gata Montesa a pantalla completa
Conclusión
⚠️ ANTES DE PUBLICAR NADA, LEE ESTO
UNA IA PUEDE EQUIVOCARSE
La Gata Montesa puede decir cosas falsas con total seguridad, inventarse datos, fechas o nombres que no existen, y no darse cuenta de que se ha equivocado. Nunca la uses como única fuente. Contrasta siempre con tu profesorado, con libros o con fuentes fiables.
UNA IA NO ENTIENDE LO QUE DICE
No “sabe” cosas como las sabe una persona: predice palabras probables a partir de lo que ha leído antes. Puede sonar muy segura y estar completamente equivocada al mismo tiempo. Eso no es un fallo raro: es como funciona por dentro.
NO ESCRIBAS DATOS PERSONALES
Ni el tuyo ni el de nadie: nombre completo, dirección, teléfono, centro educativo, fotos que identifiquen a alguien. Lo que escribas en el chat sale de tu móvil y viaja hasta los servidores de Google. Si publicas tu propia versión, dejas este aviso bien visible, tan grande como lo tenemos aquí.
TU CLAVE ES TUYA, Y SOLO TUYA
No la compartas, no la subas a un repositorio público, no la pegues en el HTML. Si alguna vez sospechas que se ha filtrado, entra en Google AI Studio y genera una nueva inmediatamente.
ESTO ES UN EXPERIMENTO EDUCATIVO
No es un producto terminado ni una IA “oficial” de nada. Lo hemos montado para aprender cómo funciona por dentro un chatbot: qué es un modelo, qué es una clave de API, por qué hace falta un backend. El objetivo no es que la Gata Montesa lo sepa todo, sino que entendamos cómo se construye.
LAS IAS TIENEN SESGOS Y LÍMITES
Los modelos de IA aprenden de textos escritos por personas, así que heredan sus errores, sus huecos y a veces sus prejuicios. No des por buena una respuesta solo porque suene razonable o esté bien escrita.
REVISA LAS CONDICIONES ANTES DE USARLO CON MENORES
Tanto Google (Gemini) como Cloudflare tienen sus propias condiciones de uso, incluida una edad mínima para crear cuenta. Si vas a poner esto en manos de alumnado menor de edad, la cuenta y la clave las gestiona una persona adulta, nunca el alumnado.
NADA DE ESTO ES INFALIBLE
Ni el Worker, ni Gemini, ni Cloudflare, ni nosotros. Puede fallar, puede caerse, puede dar una respuesta rara. Fórmate el hábito de desconfiar un poco de cualquier respuesta de una IA, la nuestra incluida.


