
Chismes del Lince 16: ¿Qué iba a hacer?, una memoria de bolsillo por voz
Noche cerrada en la cueva del Lince. El Lince entra a toda prisa, se para en seco y se queda mirando la pared.
—Gata, ¿a qué venía yo?
—A la cueva, Lince. Has venido a la cueva.
—Eso ya lo sé. Venía a otra cosa.
La Gata suspira con paciencia de sidekick joven: —Ayer te dije que apuntaras lo del coche y no lo apuntaste. Hoy no sabes dónde has dejado las llaves. Así que ahora vas a hacer lo que yo te diga: hablarme, y yo me acuerdo por ti.
—¿Y si te oigo mal?
—Entonces te lo repito antes de apuntarlo. Yo mando, pero no soy tonta.
Introducción
El móvil ya había escuchado, hablado, contado pasos, medido la voz y la mano, usado la pantalla como tambor y como papel, y recordado una pastilla. Ahora le pedimos algo que parece sencillo y no lo es: entender una frase normal y quedarse con lo importante.
El Chisme 16 es una memoria de bolsillo. La persona le dice a la Gata lo que no quiere olvidar y, cuando se le olvida, se lo pregunta:
- «Gata, acuérdame de llamar a mi hija esta tarde.»
- «Gata, mañana tengo médico.»
- «¿Qué tenía mañana?»
- «He dejado las llaves en el cajón de la entrada.»
- «¿Dónde dejé las llaves?»
No hay menús ni teclado. Solo un botón enorme con un micrófono.
Se parece al Chisme 15, y por eso conviene decir en qué se diferencia:
| Chisme 15 (la caja de las pastillas) | Chisme 16 (¿Qué iba a hacer?) | |
|---|---|---|
| Quién escribe | un familiar, una vez | la persona, hablando, cuando se le ocurre |
| Qué guarda | horarios fijos y repetidos | cosas sueltas y variadas |
| Cómo funciona | el aviso llama a la persona | la persona le pregunta a la Gata |
| Aviso en el móvil bloqueado | sí, con el calendario | no |
Lo interesante de este chisme no es la IA, porque no usa ninguna. Lo interesante es cómo se entiende una frase con reglas, y que la persona no tenga que escribir ni elegir en un menú.
Reglas de diseño, las de siempre: botones enormes, nada de teclear, letra grande y todo lo que dice la Gata, escrito.
⚠️ Esto es una ayuda, no una garantía
No avisa. Solo contesta cuando le preguntas. Si algo es importante de verdad, no puede depender solo de esta aplicación.
No usa IA ni Worker. Lo que se apunta vive en el móvil, en el navegador. Pero el reconocimiento de voz de Chrome envía el audio a Google para entenderte: no hay forma de hablarle a una web sin eso.
Para las pruebas usamos solo frases inventadas. «Mañana tengo médico» ya es, en el fondo, un dato de salud.
Pruébala aquí (en su propia dirección, en el móvil, con Chrome): https://montesteam.org/app/chisme16recordatorio.html

Cómo funciona por dentro
Botón de micro → reconocimiento de Chrome → texto → tipo de frase → fechas → tarjeta «¿Lo apunto?» → VALE → localStorage → respuesta de la Gata
- La persona toca el botón y habla. Chrome devuelve la frase escrita.
- El programa decide de qué tipo es: ¿pregunta dónde está algo?, ¿guarda un objeto?, ¿borra algo?, ¿pregunta qué tiene apuntado?, ¿recuerda o apunta algo?
- Un «entendedor» de fechas busca en la frase el día y la hora («mañana», «el viernes», «a las cinco y media», «dentro de dos horas») y los va quitando. Lo que sobra es lo que hay que recordar.
- La Gata repite lo que ha entendido, con la fecha completa, y salen dos botones: VALE y NO.
- Con VALE se guarda en el navegador. Con NO no se guarda nada.
- Cuando la persona pregunta («¿qué tenía mañana?»), el programa filtra lo guardado y la Gata lo lee en voz alta y lo enseña escrito.
Los tres tipos de frase
| Tipo | Ejemplo | Qué hace |
|---|---|---|
| Recordar o apuntar | «Acuérdame el viernes a las 5 de comprar pan» | Guarda «Comprar pan», el viernes, 17:00 |
| Preguntar | «¿Qué tenía mañana?», «¿Qué iba a hacer?», «¿Qué tengo esta semana?» | Lee lo apuntado para esa fecha. Sin fecha, lee lo de hoy, lo atrasado y lo que no tiene fecha |
| Objetos | «He dejado las llaves en el cajón» / «¿Dónde dejé las llaves?» | Guarda el objeto y el lugar; luego lo busca |
Y hay una cuarta cosa pequeña: borrar. «Borra lo de comprar pan» o «ya llamé a mi hija» buscan el recordatorio que más se parezca, lo enseñan y piden confirmación.
Seis decisiones de ingeniería
1. Que conteste, no que avise. El título lo dice: el momento clave es «¿qué iba a hacer?», cuando la persona pregunta. Eso no necesita notificaciones. Además, una web cerrada no puede avisar (el mismo muro del Chisme 15). Preferimos no prometer lo que no podemos cumplir.
2. Entender fechas con reglas, no con IA. «Mañana», «el viernes», «dentro de dos horas»… se reconocen con expresiones regulares: patrones de texto. Tiene ventajas: funciona sin Internet para esta parte, no manda frases personales a ningún servidor y se puede leer y cambiar en clase. A cambio, solo entiende lo que alguien previó.
3. Dos copias del texto. Para buscar usamos una copia sin tildes y en minúsculas (así «mañana» y «manana» valen igual). Para mostrar usamos la original. Las dos se recortan a la vez y con la misma longitud, de modo que lo que sobra de la frase conserva sus tildes y mayúsculas. Es un truco pequeño y muy útil.
4. Confirmar siempre. El mayor riesgo no es la fecha, es que el reconocimiento oiga mal. La Gata repite la frase con el día completo («mañana, viernes 2 de octubre») y no guarda nada hasta el VALE. Es la decisión que más importa en todo el proyecto.
5. Una hora dudosa, a la vista. «A las 5» puede ser de la mañana o de la tarde. El programa decide con una regla simple (de la 1 a las 7 sin más datos, por la tarde) y la deja escrita en la tarjeta para que se pueda corregir. Con «esta tarde» no inventa una hora: guarda «por la tarde».
6. Los objetos se pisan. Si dices «dejé las llaves en el cajón» y luego «dejé las llaves en la mesa», gana la última. Es lo que haría una persona.
Los límites: qué da de sí esta memoria
- No avisa. Contesta cuando le preguntas. No suena con el móvil bloqueado ni sabe si has hecho lo que apuntaste.
- Chrome oye lo que oye. Corta cuando haces una pausa, y con voz baja o pastosa puede fallar más. Eso importa mucho si la usa alguien con la voz débil.
- Solo entiende lo previsto. Frases raras, fechas complicadas o dos recordatorios en una frase se le escapan. Por eso se confirma.
- Algunas horas son dudosas. «El viernes», dicho en viernes, se apunta para el viernes siguiente. «A las doce de la noche» no se resuelve bien.
- El registro vive en el navegador. Si se borran los datos, se pierde. No se ve desde otro móvil, y un móvil compartido comparte la lista.
- No sincroniza ni avisa a nadie. Es decisión nuestra: así no hay datos en ningún servidor.
- El audio va a Google. Es el precio del reconocimiento de voz de Chrome.
Lo que todavía hay que comprobar en el móvil real
Esto no lo hemos probado todavía, y lo decimos tal cual:
- ¿Escribe Chrome las horas en cifras («a las 5») o en letras («a las cinco»)? El código admite las dos.
- ¿Cuántas veces hay que repetir una frase hablando bajito?
- ¿Se pisan la voz de la Gata y el micrófono?
- ¿Se entienden bien las frases de la lista de pruebas, una por una?
Para el aula
- Busca el «entendedor». Localiza la función
fechasen el código. ¿En qué orden busca las cosas? ¿Qué pasaría si buscara «mañana» antes que «por la mañana»? - Rompe el parser. Inventa cinco frases que se le escapen. ¿Qué regla habría que añadir para cada una?
- Una expresión regular, al detalle. Coge la de «a las N y media» y explica cada trozo. Después añade «y veinte».
- Dos copias del texto. Explica con tus palabras por qué
normno puede cambiar la longitud de la frase. ¿Qué se rompería si la cambiara? - Añade una franja. Añade «de madrugada» o «a primera hora». ¿Qué más hay que tocar?
- Compara con la IA. ¿Qué entendería un modelo de lenguaje que no entiende este programa? ¿Y qué riesgos nuevos traería (privacidad, errores que no se ven)?
- Mira el almacenamiento. En las herramientas de desarrollo de un ordenador, busca
qih.items. ¿Qué datos hay? ¿Quién podría leerlos en un móvil compartido? - Piensa en la ética. Si la lista dice «atrasado», ¿puede alguien sentirse mal? ¿Quién debería ver este registro? ¿Qué diferencia hay entre ayudar a recordar y controlar?
Del aula a la calle: Aprendizaje-Servicio
La ruta de siempre: aprender → escuchar → construir y probar con acompañamiento → mejorar → devolver. Queremos ofrecérsela a Parkinson Sol Estepona y preguntar, sobre todo, a profesionales y a familias si les sirve de algo. Puede que no, y también aprenderemos. Hay muchas aplicaciones de notas y recordatorios: queremos saber qué aportaría esta que no tengan las demás, y qué sobra.
Lo que queremos aprender de las personas
- ¿Entiende bien la voz de cada persona? ¿Cuántas veces hay que repetir?
- ¿Es cómodo el botón del micrófono? ¿Se ve bien la letra? ¿Se oye bien a la Gata?
- ¿Se entiende la pregunta de confirmación? ¿Molesta tener que decir VALE?
- ¿Qué frases dice la gente de verdad? ¿Cuáles no entiende la aplicación?
- ¿Sirve la memoria de objetos («¿dónde dejé las llaves?») o sobra?
- ¿Genera sensación de ayuda o de examen ver cosas «atrasadas»?
- ¿Qué echarías de menos? ¿Y qué te sobra?
Si alguien se siente incómodo, se para.
Paso 1: prepara la página
Este chisme no necesita Worker ni clave de API. Copia el código de la sección HTML y guárdalo como que-iba-a-hacer.html en tu carpeta app. Lo que quizá quieras tocar está al principio del <script>:
const FO={manana:"09:00",mediodia:"13:00",tarde:"17:00",noche:"21:00"}; // horas para ordenar las franjas
const FR={manana:"por la mañana",mediodia:"a mediodía",tarde:"por la tarde",noche:"por la noche"}; // cómo se dicen
Y dentro de entender(), las expresiones regulares que reconocen cada tipo de frase.
Paso 2: súbela y ábrela
Ábrela directamente en su dirección (no dentro de una entrada de WordPress), desde el móvil, con Chrome y con https. Sin https el navegador no deja usar el micrófono.
Paso 3: pruébala
- Pulsa «Hablar con la Gata» y acepta el permiso del micrófono.
- Di: «Acuérdame de llamar a mi hija esta tarde.» Comprueba la tarjeta y pulsa VALE.
- Di: «Mañana tengo médico.» y confirma.
- Di: «He dejado las llaves en el cajón de la entrada.» y confirma.
- Pregunta: «¿Qué tenía mañana?», «¿Qué iba a hacer?» y «¿Dónde dejé las llaves?»
- Di: «Borra lo de llamar a mi hija.» y confirma.
- Pulsa «Copiar la lista» y pégala en una nota para ver el resultado.
Si algo falla
- No pasa nada al tocar el micro: mira que sea Chrome, que la página sea
httpsy que el permiso del micrófono esté concedido. - Dice «Sin Internet»: Chrome necesita conexión para entenderte.
- «No te he oído»: acércate al móvil y habla un poco más fuerte. Chrome corta cuando pasa un momento sin oír nada.
- Entiende mal la fecha: mira la tarjeta antes de pulsar VALE. Si está mal, pulsa NO y repítelo con una frase más corta.
- La Gata no habla: revisa el volumen y la casilla «La Gata habla en voz alta».
- La lista desaparece: vive en el navegador. Si se borran los datos o se usa el modo incógnito, se pierde.
- No te entiende ninguna frase: usa el cuadro «Escribir en vez de hablar» para ver si el fallo es del micrófono o del programa.
Conclusión
Con un botón, el reconocimiento de voz de Chrome, unas cuantas expresiones regulares y localStorage hemos hecho una memoria de bolsillo que no usa IA: voz → texto → tipo de frase → fechas → confirmación → lista → respuesta. Lo interesante, otra vez, son las decisiones: que conteste en vez de avisar, entender fechas con reglas que se pueden leer, confirmar siempre antes de guardar, dejar a la vista las horas dudosas y no prometer más de lo que sabe hacer.
El Lince por fin ha recordado a qué venía: a pedirle a la Gata que se acuerde por él. La Gata, que ya lo había apuntado, está pensando en el chisme 17.
⚠️ ANTES DE PUBLICAR NADA, LEE ESTO
No es un producto sanitario ni una ayuda médica. Es una prueba técnica de un centro educativo. No la presentes como herramienta para mejorar la memoria, tratar ninguna enfermedad ni sustituir un recordatorio importante, ni prometas beneficios que no hemos comprobado.
El audio va a Google. El reconocimiento de voz de Chrome envía lo que se dice a los servidores de Google. Cuéntalo siempre a quien la pruebe, antes de que hable.
Lo que se apunta puede ser un dato de salud («mañana tengo médico»). Para las pruebas, usa solo frases inventadas. Si alguien quiere usarla de verdad, pide consentimiento a la persona (y a su familia) y explícale dónde se guardan los datos y cómo se borran.
Un móvil compartido comparte la lista. Usa «Borrar todo» al terminar una sesión de prueba.
Puede entender mal. Por eso pide confirmación. No des por bueno lo que no se ha confirmado.
No avisa. No debe ser el único recurso de nadie. Si un olvido puede tener consecuencias, hacen falta otras medidas.
Si lo usas en un centro educativo, revisa las indicaciones de la persona responsable de protección de datos.
La tecnología es la parte fácil. Decidir cómo, con quién y para qué la usamos es el verdadero chisme.
HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>¿Qué iba a hacer?</title>
<style>
:root{--ink:#1d1715;--bg:#f8f6f3;--bermejo:#a3261a;--ok:#17703a;--line:#ddd5cd;--mute:#5f524b}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:20px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
main{max-width:560px;margin:0 auto;padding:18px 16px 40px}
h1{font-size:30px;line-height:1.15;margin:.3em 0 .4em;color:var(--bermejo);font-weight:800}
h2{font-size:22px;margin:1.2em 0 .4em}
h3{font-size:18px;margin:.8em 0 .2em;color:var(--mute)}
button,input{font:inherit;color:inherit}
:focus-visible{outline:4px solid #1b5fd1;outline-offset:2px}
.btn{display:block;width:100%;min-height:64px;margin:12px 0;border:0;border-radius:14px;background:var(--bermejo);color:#fff;font-weight:700;cursor:pointer}
.btn.sec{background:#fff;color:var(--bermejo);border:2px solid var(--bermejo)}
.btn.ok{background:var(--ok);min-height:110px;font-size:34px;border-radius:22px}
.btn.mic{min-height:130px;font-size:28px;border-radius:24px}
.btn.mic.on{background:var(--ok)}
.box{background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:14px 0}
.box.err{border-color:var(--bermejo);color:var(--bermejo);font-weight:600}
.line{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--line)}
.line:last-child{border-bottom:0}
.mini{flex:0 0 64px;height:64px;border:2px solid var(--ok);border-radius:12px;background:#fff;color:var(--ok);font-size:28px;font-weight:700;cursor:pointer}
.note{font-size:16px;color:var(--mute)}
input[type=text]{width:100%;padding:12px;border:2px solid var(--line);border-radius:8px;background:#fff;margin:8px 0}
summary{cursor:pointer;font-weight:600;padding:8px 0}
.row{display:flex;gap:12px;align-items:center;margin:10px 0}
.row input{width:28px;height:28px}
</style>
</head>
<body>
<main id="app"></main>
<script>
const $=s=>document.querySelector(s),pad=n=>String(n).padStart(2,"0");
const ymd=d=>d.getFullYear()+"-"+pad(d.getMonth()+1)+"-"+pad(d.getDate());
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
const ls={get(k,d){try{const v=JSON.parse(localStorage.getItem(k));return v==null?d:v}catch(e){return d}},set(k,v){try{localStorage.setItem(k,JSON.stringify(v))}catch(e){}}};
let items=ls.get("qih.items",[]),habla=ls.get("qih.voz",true),on=false,rc;
const S={heard:"",say:"",pend:null,msg:""};
const dOnly=d=>new Date(d.getFullYear(),d.getMonth(),d.getDate());
const H=()=>dOnly(new Date());
const add=(d,n)=>new Date(d.getFullYear(),d.getMonth(),d.getDate()+n);
// Quita tildes y ñ sin cambiar la longitud, para poder cortar el texto original con los mismos índices
const norm=s=>s.toLowerCase().replace(/[áàä]/g,"a").replace(/[éèë]/g,"e").replace(/[íìï]/g,"i").replace(/[óòö]/g,"o").replace(/[úùü]/g,"u").replace(/ñ/g,"n");
const NW={un:1,una:1,dos:2,tres:3,cuatro:4,cinco:5,seis:6,siete:7,ocho:8,nueve:9,diez:10,once:11,doce:12};
const NUM="\\d{1,2}|una?|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez|once|doce";
const num=s=>/^\d/.test(s)?+s:NW[s];
const DIAS=["domingo","lunes","martes","miercoles","jueves","viernes","sabado"];
const MESES=["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"];
const FO={manana:"09:00",mediodia:"13:00",tarde:"17:00",noche:"21:00"};
const FR={manana:"por la mañana",mediodia:"a mediodía",tarde:"por la tarde",noche:"por la noche"};
const STOP=new Set("el la los las un una unos unas de del al a mi mis tu que y en se me lo con por para he ha has hemos habia esta estan estaba estaban deje dejaste dejado puse puesto guarde guardado meti metido donde ya hice hecho borra esa ese eso".split(" "));
const ws=s=>norm(s).split(/[^a-z0-9]+/).filter(w=>w.length>1&&!STOP.has(w));
const sc=(a,b)=>a.filter(x=>b.some(y=>x===y||(x.length>3&&y.length>3&&x.slice(0,4)===y.slice(0,4)))).length;
const mejor=(w,arr)=>{let b=null,bs=0;arr.forEach(i=>{const s=sc(w,ws(i.txt));if(s>bs){bs=s;b=i}});return b};
const fl=f=>new Date(f+"T12:00").toLocaleDateString("es-ES",{weekday:"long",day:"numeric",month:"long"});
const rel=f=>{const d=Math.round((new Date(f+"T12:00")-new Date(ymd(H())+"T12:00"))/864e5);return d===0?"hoy":d===1?"mañana":d===2?"pasado mañana":d===-1?"ayer":fl(f)};
const cuando=i=>(i.fecha?(rel(i.fecha)!==fl(i.fecha)?rel(i.fecha)+", ":"")+fl(i.fecha):"sin fecha")+(i.hora?", a las "+i.hora:i.franja?", "+FR[i.franja]:"");
// Texto con dos copias alineadas: n (normalizada, para buscar) y o (original, para mostrar)
function Txt(o){this.o=o;this.n=norm(o)}
Txt.prototype.take=function(re){const m=re.exec(this.n);if(!m)return null;const i=m.index,l=m[0].length;this.n=this.n.slice(0,i)+" "+this.n.slice(i+l);this.o=this.o.slice(0,i)+" "+this.o.slice(i+l);return m};
// El «entendedor» de fechas: reglas, sin IA. Va quitando del texto lo que reconoce.
function fechas(T){
const hoy=H(),now=new Date();let f=null,h=null,fr=null,sem=false,m;
if(T.take(/\besta semana\b|\bproximos dias\b/))sem=true;
if(m=T.take(new RegExp("\\ba las? ("+NUM+")(?:[:.](\\d\\d)| y (media|cuarto|\\d{1,2})| menos (cuarto|\\d{1,2}))?(?: en punto)?(?: (?:de|por|en) la (manana|tarde|noche|madrugada))?\\b"))){
let hh=num(m[1]),mi=0;
if(m[2])mi=+m[2];else if(m[3])mi=m[3]==="media"?30:m[3]==="cuarto"?15:+m[3];else if(m[4]){hh-=1;mi=m[4]==="cuarto"?45:60-m[4]}
const p=m[5];
if((p==="tarde"||p==="noche")&&hh<12)hh+=12;
else if(!p&&hh>=1&&hh<=7&&!/^0/.test(m[1]))hh+=12;
h=pad(hh%24)+":"+pad(mi);
}
if(T.take(/\bpasado manana\b/))f=add(hoy,2);
if(m=T.take(/\b(esta|por la|de la|en la|a la|al|a) (manana|tarde|noche|mediodia)\b/)){fr=m[2];if(m[1]==="esta")f=hoy}
if(T.take(/\bmanana\b/))f=add(hoy,1);
if(T.take(/\bhoy\b/))f=hoy;
if(T.take(/\bdentro de media hora\b/)){const d=new Date(+now+18e5);f=dOnly(d);h=pad(d.getHours())+":"+pad(d.getMinutes())}
if(m=T.take(new RegExp("\\b(?:dentro de|en) ("+NUM+") (minutos?|horas?|dias?|semanas?)\\b"))){
const n=num(m[1]),u=m[2][0];
if(u==="m"||u==="h"){const d=new Date(+now+n*(u==="m"?60:3600)*1000);f=dOnly(d);h=pad(d.getHours())+":"+pad(d.getMinutes())}
else f=add(hoy,n*(u==="s"?7:1));
}
if(m=T.take(/\b(?:el proximo |este |proximo |el )?(lunes|martes|miercoles|jueves|viernes|sabado|domingo)\b(?: que viene| proximo)?/)){
let k=(DIAS.indexOf(m[1])-hoy.getDay()+7)%7;if(!k)k=7;f=add(hoy,k);
}
if(m=T.take(new RegExp("\\b(?:el )?(?:dia )?(\\d{1,2}) de ("+MESES.join("|")+"|setiembre)\\b"))){
let mo=MESES.indexOf(m[2]);if(mo<0)mo=8;let d=new Date(hoy.getFullYear(),mo,+m[1]);if(d<hoy)d=new Date(hoy.getFullYear()+1,mo,+m[1]);f=d;
}else if(m=T.take(/\bel (?:dia )?(\d{1,2})\b/)){
let d=new Date(hoy.getFullYear(),hoy.getMonth(),+m[1]);if(d<hoy)d=new Date(hoy.getFullYear(),hoy.getMonth()+1,+m[1]);f=d;
}
if(!f&&(h||fr)){f=hoy;if(h&&!fr){const[a,b]=h.split(":");if(new Date(hoy.getFullYear(),hoy.getMonth(),hoy.getDate(),+a,+b)<now)f=add(hoy,1)}}
return{f:f?ymd(f):null,h,fr,sem};
}
function limpia(s){
s=s.replace(/[¿?¡!.,;]+/g," ").replace(/\s+/g," ").trim();let p;
do{p=s;s=s.replace(/^(?:para|de|que|a|el|la|y|en|por|pues)\s+/i,"").replace(/\s+(?:para|de|que|a|el|la|y|en|por|pues|dia)$/i,"")}while(s!==p);
return s?s[0].toUpperCase()+s.slice(1):"";
}
function entender(raw){
S.heard=raw;S.msg="";S.say="";S.pend=null;
const T=new Txt(raw);T.take(/^\W*(?:hola\W+)?(?:gata|gato|cata)\b\W*/);
const n0=T.n.trim();let m;
if(n0.length<3||/^(ya esta|vale|si|no|hola|gracias|ok|adios)\W*$/.test(n0))return resp("Dime algo para apuntar, o pregúntame qué tienes apuntado.");
// 1. ¿Dónde dejé…?
if(m=/^\W*(?:y\s+)?donde\b(.*)$/.exec(T.n)){
const o=mejor(ws(m[1]),items.filter(i=>i.tipo==="objeto"));
return resp(o?"Apuntaste: "+o.txt+", "+o.lugar+".":"No tengo apuntado dónde está eso.");
}
// 2. Guardar un objeto: «he dejado las llaves en el cajón»
if(m=/\b(?:he |hemos )?(dejado|puesto|guardado|metido|escondido|deje|puse|guarde|meti|escondi)\s+(.+?)\s+((?:en|encima|debajo|dentro|sobre|junto|al lado|detras|delante|bajo|a la)\b.*)$/d.exec(T.n)){
return pide({tipo:"objeto",txt:T.o.slice(...m.indices[2]).trim(),lugar:T.o.slice(...m.indices[3]).trim().replace(/[.?!]+$/,"")});
}
// 3. Borrar: «borra lo del médico», «ya llamé a mi hija»
if(m=/^\W*(?:borra(?:me)?|borrar|elimina|quita|olvida|ya (?:hice|he hecho|termine|acabe|llame a|he llamado a|fui a|he ido a|esta lo de|esta))\s+(.+)$/.exec(T.n)){
const it=mejor(ws(m[1]),items);
return it?pide({tipo:"borrar",id:it.id},it):resp("No encuentro nada apuntado que se parezca a eso.");
}
// 4. Preguntar: «¿qué tenía mañana?», «¿qué iba a hacer?»
if(/^\W*(?:y\s+)?(?:(?:que|cuales|cuantas?)\b|dime\b|leeme\b|lee\b|tengo algo\b|tenia algo\b|tenemos algo\b|hay algo\b|me dijiste\b)/.test(T.n))return consulta(fechas(T));
// 5. Recordar o apuntar
let tipo="apuntar";
if(T.take(/^\W*(?:no me dejes olvidar|acuerdame|recuerdame|avisame|apuntame|apunta|anotame|anota|acuerdate)\b(?:\s+de que|\s+de|\s+que)?\s*/))tipo="recordar";
const F=fechas(T);
if(/^\s*(?:tengo que|tenemos que|hay que|debo|necesito)\b/.test(T.n))tipo="recordar";
T.take(/^\s*(?:tengo que|tenemos que|hay que|tengo|tenemos|hay|me toca|toca)\s+/);
const txt=limpia(T.o);
if(txt.length<2)return resp("No te he entendido bien. Prueba otra vez.");
return pide({tipo,txt,fecha:F.f,hora:F.h,franja:F.fr});
}
function consulta(F){
const hoy=ymd(H()),key=i=>(i.fecha||"")+(i.hora||FO[i.franja]||"99"),pr=items.filter(i=>i.tipo!=="objeto");
let l,ini,vacio,dia=false;
if(F.sem){const fin=ymd(add(H(),6));l=pr.filter(i=>i.fecha&&i.fecha>=hoy&&i.fecha<=fin);ini="En los próximos siete días tienes: ";vacio="No tengo nada apuntado en los próximos siete días.";dia=true}
else if(F.f){l=pr.filter(i=>i.fecha===F.f);ini="Para "+rel(F.f)+" tienes: ";vacio="No tengo nada apuntado para "+rel(F.f)+"."}
else{l=pr.filter(i=>!i.fecha||i.fecha<=hoy);ini="Tienes pendiente: ";vacio="No tengo nada pendiente.";dia=true}
if(!l.length)return resp(vacio);
l.sort((a,b)=>key(a).localeCompare(key(b)));
return resp(ini+l.map(i=>i.txt+(dia&&i.fecha?" ("+rel(i.fecha)+")":"")+(i.hora?" a las "+i.hora:i.franja?" "+FR[i.franja]:"")).join("; ")+".");
}
function pide(it,x){
it.id=it.id||Date.now().toString(36)+Math.random().toString(36).slice(2,5);S.pend=it;
S.say=it.tipo==="objeto"?"¿Apunto que "+it.txt+" está "+it.lugar+"?":it.tipo==="borrar"?"¿Borro «"+x.txt+"»?":"¿Apunto «"+it.txt+"», "+cuando(it)+"?";
speak(S.say);render();
}
function ok(){
const p=S.pend;if(!p)return;S.pend=null;
if(p.tipo==="borrar")items=items.filter(i=>i.id!==p.id);
else{if(p.tipo==="objeto"){const k=ws(p.txt).join(" ");items=items.filter(i=>!(i.tipo==="objeto"&&ws(i.txt).join(" ")===k))}items.push(p)}
ls.set("qih.items",items);resp(p.tipo==="borrar"?"Borrado.":"Apuntado.");
}
function no(){S.pend=null;resp("Vale, no lo apunto. Díselo otra vez.")}
function hecho(id){items=items.filter(i=>i.id!==id);ls.set("qih.items",items);resp("Hecho. Lo he quitado de la lista.")}
function resp(t){S.say=t;speak(t);render()}
function speak(t){if(!habla||!window.speechSynthesis)return;speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(t);u.lang="es-ES";u.rate=.9;speechSynthesis.speak(u)}
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
function mic(){
if(!SR){S.msg="Este navegador no sabe escuchar. Usa Chrome, o escribe abajo.";render();return}
if(on){rc.stop();return}
if(window.speechSynthesis)speechSynthesis.cancel();
S.msg="";rc=new SR();rc.lang="es-ES";rc.interimResults=false;rc.maxAlternatives=1;
rc.onstart=()=>{on=true;render()};
rc.onend=()=>{on=false;render()};
rc.onerror=e=>{S.msg=/not-allowed|service/.test(e.error)?"Falta el permiso del micrófono. Actívalo en los ajustes del navegador.":e.error==="no-speech"?"No te he oído. Acércate al móvil y prueba otra vez.":e.error==="network"?"Sin Internet: Chrome lo necesita para entenderte.":e.error==="audio-capture"?"No encuentro el micrófono.":"No he podido escucharte ("+e.error+").";render()};
rc.onresult=e=>entender(e.results[0][0].transcript);
try{rc.start()}catch(e){}
}
function escribir(){const v=$("#tx").value.trim();if(v)entender(v)}
function copiar(){
if(!items.length){S.msg="No hay nada que copiar.";render();return}
const t="¿Qué iba a hacer? — copia del "+new Date().toLocaleString("es-ES")+"\n"+items.map(i=>"- ["+i.tipo+"] "+i.txt+" — "+(i.tipo==="objeto"?i.lugar:cuando(i))).join("\n");
const fin=b=>{S.msg=b?"":"No se ha podido copiar.";S.say=b?"Copiado. Ya puedes pegarlo donde quieras.":"";render()};
const viejo=()=>{const a=document.createElement("textarea");a.value=t;document.body.appendChild(a);a.select();let b=false;try{b=document.execCommand("copy")}catch(e){}a.remove();fin(b)};
if(navigator.clipboard)navigator.clipboard.writeText(t).then(()=>fin(true),viejo);else viejo();
}
function borrarTodo(){if(confirm("¿Borrar todo lo apuntado en este móvil?")){items=[];ls.set("qih.items",items);S.pend=null;resp("Todo borrado.")}}
function render(){
const hoy=ymd(H()),ma=ymd(add(H(),1)),key=i=>(i.fecha||"")+(i.hora||FO[i.franja]||"99");
const vis=items.filter(i=>i.tipo!=="objeto").sort((a,b)=>key(a).localeCompare(key(b)));
const G=[["Atrasado",vis.filter(i=>i.fecha&&i.fecha<hoy)],["Hoy",vis.filter(i=>i.fecha===hoy)],["Mañana",vis.filter(i=>i.fecha===ma)],["Más adelante",vis.filter(i=>i.fecha&&i.fecha>ma)],["Sin fecha",vis.filter(i=>!i.fecha)],["Dónde dejé las cosas",items.filter(i=>i.tipo==="objeto")]];
const ic={recordar:"🔔",apuntar:"📅",objeto:"🔑"};
const lista=G.filter(g=>g[1].length).map(g=>"<h3>"+g[0]+"</h3>"+g[1].map(i=>'<div class="line"><div><b>'+ic[i.tipo]+" "+esc(i.txt)+'</b><br><span class="note">'+esc(i.tipo==="objeto"?i.lugar:cuando(i))+'</span></div><button class="mini" onclick="hecho(\''+i.id+'\')" aria-label="Hecho: '+esc(i.txt)+'">✓</button></div>').join("")).join("");
$("#app").innerHTML='<h1>¿Qué iba a hacer?</h1>'+
'<p class="note">Dile a la Gata lo que no quieres olvidar, y pregúntale cuando se te haya olvidado.</p>'+
'<button class="btn mic'+(on?" on":"")+'" onclick="mic()">'+(on?"Te escucho… (toca para parar)":"🎤 Hablar con la Gata")+'</button>'+
(S.msg?'<div class="box err" role="alert">'+esc(S.msg)+'</div>':"")+
(S.heard?'<p class="note">Has dicho: «'+esc(S.heard)+'»</p>':"")+
(S.say?'<div class="box" aria-live="polite"><b>Gata:</b> '+esc(S.say)+'</div>':"")+
(S.pend?'<button class="btn ok" onclick="ok()">VALE</button><button class="btn sec" onclick="no()">NO</button>':"")+
'<h2>Lo que tengo apuntado</h2>'+(lista?'<div class="box">'+lista+'</div>':'<div class="box">Todavía no hay nada. Prueba a decir: «Gata, mañana tengo médico».</div>')+
'<button class="btn sec" onclick="copiar()">Copiar la lista</button>'+
'<label class="row"><input type="checkbox" '+(habla?"checked":"")+' onchange="habla=this.checked;ls.set(\'qih.voz\',habla)"><span>La Gata habla en voz alta</span></label>'+
'<details><summary>Escribir en vez de hablar</summary><input type="text" id="tx" placeholder="Ejemplo: mañana tengo médico"><button class="btn sec" onclick="escribir()">Enviar</button></details>'+
'<details><summary>Frases de prueba (inventadas)</summary><p class="note">Acuérdame de llamar a mi hija esta tarde.<br>Mañana tengo médico.<br>Acuérdame el viernes a las 5 de comprar pan.<br>He dejado las llaves en el cajón de la entrada.<br>¿Qué tenía mañana? · ¿Qué iba a hacer? · ¿Qué tengo esta semana?<br>¿Dónde dejé las llaves?<br>Borra lo de comprar pan.</p></details>'+
'<details><summary>Borrar todo</summary><button class="btn sec" onclick="borrarTodo()">Borrar todo lo apuntado</button></details>'+
'<p class="note">Es una ayuda, no una garantía: no avisa, solo contesta cuando le preguntas. Chrome envía el audio a Google para entenderte. Lo apuntado se guarda solo en este móvil y se pierde si se borran los datos del navegador. En las pruebas usa frases inventadas.</p>';
}
render();
</script>
</body>
</html>
Etiquetas: accesibilidad, reconocimiento de voz, expresiones regulares, localStorage, recordatorios, privacidad, Chismes del Lince



