Hoy cada empresa monta un chatbot sobre su propio modelo: soporte, FAQ, dudas de producto. Pero el usuario sigue cargando con todo el trabajo.
Sale del flujo, abre el widget, redacta la pregunta, espera la respuesta y vuelve a ubicarse en la pantalla. Eso también es carga cognitiva.
> UX: USER_DOES_THE_WORK — MODEL_LIVES_FAR_AWAY
Acopla un elemento flotante a otro de forma declarativa. Tres capacidades nucleares (MDN):
anchor-name en el ancla ·
position-anchor en el flotante.
position-area con keywords (top,
bottom span-inline…) o la función
anchor().
position-try-fallbacks para reubicar si se sale
del viewport.
Párrafo activo — ancla del popover.
// CSS only · zero JS#anchor-src./* 1. Declarar el ancla */ #active { anchor-name: --active; } /* 2. Asociar y posicionar */ #help-panel { position: fixed; position-anchor: --active; position-area: right span-block; margin-inline-start: 16px; /* 3. Reubicar si overflowea */ position-try-fallbacks: flip-block, flip-inline; } /* O con la función anchor() */ .tooltip { top: anchor(--active bottom); width: anchor-size(--active width); }
El navegador toma snapshots automáticos del DOM antes y después de la mutación, y los interpola. Funciona en SPA y MPA (MDN).
document.startViewTransition(cb) envuelve una
mutación del DOM.
Es lo que usa el panel.
@view-transition activa transiciones
entre páginas distintas. No aplica a nuestro panel.
view-transition-name aísla elementos.
Pseudo-elementos
::view-transition-old / new orquestan el
detalle.
// SPA — panel en la misma página function openHelpPanel(summary) { // Fallback si no hay soporte if (!document.startViewTransition) { render(summary); return; } document.startViewTransition( () => render(summary) ); } /* Aislar el panel del resto */ #help-panel { view-transition-name: help; } ::view-transition-old(help), ::view-transition-new(help) { animation-duration: 280ms; } /* A11y: prefers-reduced-motion → none */
El navegador gestiona un "Top Layer" propio para popovers y diálogos modales. Siempre por encima — sin hacks.
Light Dismiss con Escape o click fuera. Focus Trapping semántico, sin polyfills.
<!-- markup --> <button popovertarget="menu">Abrir</button> <div id="menu" popover="auto">…</div>
Vivo en el Top Layer del navegador.
Pulsa Esc o haz click fuera para cerrar.
Focus trapping automático · cero JS.
Resume cualquier texto del DOM usando el modelo interno del navegador. Promise-based (MDN).
Summarizer.availability(opts) y
Summarizer.create(opts). Estados:
available, downloading,
unavailable.
type: tldr ·
key-points · teaser ·
headline. length,
format, sharedContext,
expectedInputLanguages,
outputLanguage.
summarize(text) → string ·
summarizeStreaming(text) →
ReadableStream · measureInputUsage() ·
destroy().
downloadprogress mientras el modelo se descarga.
ctrl.abort() al cerrar el
panel; libera con destroy().
// 1. Disponibilidad del modelo if (!('Summarizer' in self)) return; const av = await Summarizer.availability(); if (av === 'unavailable') return; // 2. Crear (monitor + AbortController) const ctrl = new AbortController(); const sum = await Summarizer.create({ type: 'key-points', length: 'short', format: 'markdown', sharedContext: 'Asistente UX.', expectedInputLanguages: ['es'], signal: ctrl.signal, monitor: m => m.addEventListener( 'downloadprogress', e => setProgress(e.loaded) ), }); // 3. Streaming directo al panel for await (const c of sum.summarizeStreaming(text)) { panel.textContent += c; } // 4. Cancelar + liberar al cerrar panel.addEventListener('toggle', e => e.newState === 'closed' && (ctrl.abort(), sum.destroy()) );
Razonamiento local sobre el DOM con sesiones conversacionales. Multimodal: texto, imagen y audio (Chrome docs).
LanguageModel.availability(opts) y
LanguageModel.create(opts) — comparten las
mismas opciones.
initialPrompts:
system/user/assistant.
expectedInputs:
text/image/audio.
monitor, signal,
responseConstraint (JSON Schema).
prompt(text) ·
promptStreaming(text) → ReadableStream ·
append() · clone() ·
measureInputUsage() · destroy().
session.contextUsage /
session.contextWindow. Escucha
'contextoverflow'.
allow="language-model". No funciona en Web
Workers.
// 1. Disponibilidad (mismas opts que create) const av = await LanguageModel.availability(); if (av === 'unavailable') return; // 2. Sesión con system prompt + monitor const ctrl = new AbortController(); const session = await LanguageModel.create({ expectedInputs: [{ type: 'text', languages: ['es'] }], initialPrompts: [ { role: 'system', content: 'Asistente UX. Responde en 2 frases.' }, { role: 'user', content: `Contexto: ${active.innerText}` }, ], signal: ctrl.signal, monitor: m => m.addEventListener( 'downloadprogress', e => setProgress(e.loaded) ), }); // 3. Streaming directo al panel for await (const c of session.promptStreaming(q)) { help.textContent += c; } // 4. Cleanup al cerrar el panel panel.addEventListener('toggle', e => e.newState === 'closed' && (ctrl.abort(), session.destroy()) );
En esta charla solo usamos Summarizer y Prompt para el panel. Pero el navegador trae más APIs de IA que podés combinar:
Detecta el idioma de cualquier texto del DOM. Ideal antes de traducir.
LanguageDetector.detect(text)
Traduce offline entre idiomas con language packs locales.
Translator.create({source, target})
Genera texto nuevo: emails, reseñas, borradores. Tono y longitud configurables.
writer.write(prompt, { context })
Refactoriza tono y longitud de texto existente: más formal, más casual, más corto.
rewriter.rewrite(text, { tone })
Corrige gramática, ortografía y puntuación. Devuelve correcciones con etiquetas.
proofreader.proofread(text)
Multimodal Prompt (audio + imagen), structured output con JSON Schema, fine-tuning con LoRA…
developer.chrome.com/docs/ai
'Summarizer' in self
→
Summarizer.availability().
monitor
emite
downloadprogress.
summarizeStreaming()
→
ReadableStream.
startViewTransition(). Cero red.
'toggle'
→
ctrl.abort()
+
destroy().
<!-- Resultado de laboratorio con valor fuera de rango --> <article id="lab-tsh" class="abnormal"> <h3>TSH (Tirotropina)</h3> <p class="value">4.5 mIU/L</p> <p class="range">Rango: 0.4 – 4.0</p> </article> <!-- Trigger nativo del panel --> <button popovertarget="explain"> Explicámelo en palabras simples </button> <!-- Top Layer del navegador --> <div id="explain" popover="auto"> <output id="ai-out"></output> </div>
/* 1. El resultado anormal expone un ancla */ #lab-tsh { anchor-name: --lab; } /* 2. El popover se acopla al ancla */ #explain { position: fixed; position-anchor: --lab; position-area: right span-block; margin-inline-start: 16px; view-transition-name: explain; } /* 3. Animación nativa entrada/salida */ ::view-transition-old(explain), ::view-transition-new(explain) { animation-duration: 260ms; }
// 1. Feature detection estricto if (!('Summarizer' in self)) return; const status = await Summarizer.availability(); if (status !== 'available') return; // 2. Sesión con contexto clínico const sum = await Summarizer.create({ type: 'key-points', length: 'short', sharedContext: 'Explica resultados de laboratorio en lenguaje claro. No emite diagnósticos.', }); // 3. Abrir el panel + stream del resumen document.startViewTransition(() => explain.showPopover()); for await (const c of sum.summarizeStreaming(labResult.innerText)) { out.textContent += c; }
// Sesión conversacional · follow-ups del paciente const session = await LanguageModel.create({ initialPrompts: [ { role: 'system', content: 'Asistente médico. Lenguaje simple. Nunca diagnostiques.' }, { role: 'user', content: `Contexto: ${labResult.innerText}` }, ], }); form.addEventListener('submit', async (e) => { e.preventDefault(); document.startViewTransition(() => out.textContent = ''); for await (const c of session.promptStreaming(input.value)) { out.textContent += c; } }); // Privacidad: datos médicos jamás salen del navegador explain.addEventListener('toggle', (e) => { if (e.newState === 'closed') session.destroy(); });
Perfil tiroideo del 24/05/2026. Algunos valores están fuera del rango de referencia.
El resultado tiene jerga clínica. El asistente local lo traduce a lenguaje claro — sin enviar datos médicos a ningún servidor.
// done in 612ms · 0 network · 0 datos enviados
window.ai antes de renderizar la UI del
asistente. Sin modelo, sin botón.
localStorage.
Sesión efímera, memoria limpia al cierre.