Build with AI · Santa Cruz

Construye
un panel de ayuda
contextual
con IA

QR Slides
// Slides
QR Proyecto
// Proyecto
// gemini-nano.local
// view-transitions
// anchor-positioning
// Build with AI · Santa Cruz

Software Engineer

Vanessa Aristizabal
GDE · Web
GitHub Star
// IDENTITY
Vanessa Aristizabal
  • [01]Ingeniera de Sistemas & Magíster en Ingeniería.
  • [02]Google Developer Expert — Web Technologies & Angular.
  • [03]GitHub Star.
  • [04]Creadora de contenido.
// Build with AI · Santa Cruz

El chatbot no siempre es la respuesta

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

[!] CHATBOT FATIGUE
  • Context switching — abrir el widget, perder de vista el componente.
  • El usuario redacta la pregunta — la app ya sabe qué está mirando.
  • Round-trip de red + tokens — latencia y costo por cada duda.
// Build with AI · Santa Cruz

La idea: un panel contextual, no otro chatbot

// el camino largo

Chatbot tradicional

  • Widget separado del componente.
  • El usuario redacta y espera.
  • Modelo en la nube, tokens, latencia.
  • JS pesado, librería de tooltips, z-index war.
Cloud LLM WebSocket JS framework
→
shift
// el camino ligero

Panel contextual nativo

  • Anclado al componente que el usuario mira.
  • Se abre con la transición, no con un click extra.
  • Modelo local, sin red, sin tokens.
  • CSS moderno + Web APIs del navegador.
anchor-name popover view-transition Summarizer LanguageModel
// Build with AI · Santa Cruz

Roadmap del taller

// LAYER_A — MODERN UI

Capa de UI moderna

01
CSS Anchor Positioning
Acoplamiento geométrico declarativo.
02
View Transitions API
Snapshots del DOM, fluidez nativa.
03
Popover & Dialog
Top Layer nativo · a11y de serie.
// LAYER_B — BUILT-IN AI

Capa de inteligencia

04
Summarizer API
Destila el DOM en TL;DR & key-points.
05
Prompt API (LanguageModel)
Razonamiento conversacional sobre la UI.
// Build with AI · Santa Cruz

CSS Anchor Positioning

Acopla un elemento flotante a otro de forma declarativa. Tres capacidades nucleares (MDN):

01 · ASOCIAR
anchor-name en el ancla · position-anchor en el flotante.
02 · POSICIONAR
position-area con keywords (top, bottom span-inline…) o la función anchor().
03 · SOBREVIVIR
position-try-fallbacks para reubicar si se sale del viewport.

Párrafo activo — ancla del popover.

// CSS only · zero JS
// help panel
Anclado a #anchor-src.
position-area: right span-block;
⚠ Anchor positioning aún no está disponible en este navegador.
anchor.css · mdn.io/anchor-positioning
/* 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);
}
// Build with AI · Santa Cruz

View Transitions API

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).

01 · SPA — SINGLE PAGE APP
document.startViewTransition(cb) envuelve una mutación del DOM. Es lo que usa el panel.
02 · MPA — MULTI-PAGE APP
At-rule @view-transition activa transiciones entre páginas distintas. No aplica a nuestro panel.
03 · CUSTOM
view-transition-name aísla elementos. Pseudo-elementos ::view-transition-old / new orquestan el detalle.
// HELP-PANEL · EMPTY
// startViewTransition
snapshot & interpolate
view-transitions · mdn.io/view-transition-api
// 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 */
// Build with AI · Santa Cruz

UI Nativa: Popover & Dialog

// TOP LAYER

Adiós a las peleas con z-index

El navegador gestiona un "Top Layer" propio para popovers y diálogos modales. Siempre por encima — sin hacks.

// ACCESSIBILITY

A11y out-of-the-box

Light Dismiss con Escape o click fuera. Focus Trapping semántico, sin polyfills.

// Top Layer (managed by browser)
<dialog> · <div popover>
SIEMPRE ARRIBA
↑
Por encima del DOM stacking context
.dropdownz-index: 99999 !important
.tooltipz-index: 99999
.toastz-index: 9999
.modal-oldz-index: 999
// document flow
// native <div popover>
<!-- markup -->
<button popovertarget="menu">Abrir</button>
<div id="menu" popover="auto">…</div>

// Soy un popover nativo

Vivo en el Top Layer del navegador.

Pulsa Esc o haz click fuera para cerrar.

Focus trapping automático · cero JS.

// Build with AI · Santa Cruz

Summarizer API

Resume cualquier texto del DOM usando el modelo interno del navegador. Promise-based (MDN).

01 · STATIC METHODS
Summarizer.availability(opts) y Summarizer.create(opts). Estados: available, downloading, unavailable.
02 · OPCIONES
type: tldr · key-points · teaser · headline. length, format, sharedContext, expectedInputLanguages, outputLanguage.
03 · INSTANCE METHODS
summarize(text) → string · summarizeStreaming(text) → ReadableStream · measureInputUsage() · destroy().
! HTTPS + transient user activation (click reciente).
! monitor() reporta downloadprogress mientras el modelo se descarga.
! Cancela con ctrl.abort() al cerrar el panel; libera con destroy().
summarizer · mdn.io/summarizer-api
// 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())
);
// Build with AI · Santa Cruz

Prompt API

Razonamiento local sobre el DOM con sesiones conversacionales. Multimodal: texto, imagen y audio (Chrome docs).

01 · STATIC METHODS
LanguageModel.availability(opts) y LanguageModel.create(opts) — comparten las mismas opciones.
02 · SESSION SETUP
initialPrompts: system/user/assistant. expectedInputs: text/image/audio. monitor, signal, responseConstraint (JSON Schema).
03 · SESSION INSTANCE
prompt(text) · promptStreaming(text) → ReadableStream · append() · clone() · measureInputUsage() · destroy().
! Inputs: texto, imagen, audio. Output: solo texto.
! Monitorea: session.contextUsage / session.contextWindow. Escucha 'contextoverflow'.
! En iframes cross-origin: allow="language-model". No funciona en Web Workers.
prompt · developer.chrome.com/docs/ai/prompt-api
// 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())
);
// Build with AI · Santa Cruz

Hay más: el ecosistema built-in AI

En esta charla solo usamos Summarizer y Prompt para el panel. Pero el navegador trae más APIs de IA que podés combinar:

// 01 — DETECTION

Language Detector

Detecta el idioma de cualquier texto del DOM. Ideal antes de traducir.

LanguageDetector.detect(text)
// 02 — TRANSLATION

Translator

Traduce offline entre idiomas con language packs locales.

Translator.create({source, target})
// 03 — GENERATION

Writer

Genera texto nuevo: emails, reseñas, borradores. Tono y longitud configurables.

writer.write(prompt, { context })
// 04 — REVISION

Rewriter

Refactoriza tono y longitud de texto existente: más formal, más casual, más corto.

rewriter.rewrite(text, { tone })
// 05 — PROOFREAD

Proofreader

Corrige gramática, ortografía y puntuación. Devuelve correcciones con etiquetas.

proofreader.proofread(text)
// 06 — Y MÁS

Y aún más en camino

Multimodal Prompt (audio + imagen), structured output con JSON Schema, fine-tuning con LoRA…

developer.chrome.com/docs/ai
// Build with AI · Santa Cruz

Arquitectura: in-browser inference

01
// UI EVENT
El usuario activa el panel contextual
Click, hover o foco sobre un bloque del DOM.
DOM event
02
// FEATURE DETECT
El browser valida la API local
'Summarizer' in self → Summarizer.availability().
globals
03
// MODEL READY
Gemini Nano on-device
Pesos cacheados (~1.5 GB). Si falta: monitor emite downloadprogress.
WebGPU
04
// INFERENCE
El DOM se tokeniza en local
summarizeStreaming() → ReadableStream.
streaming
05
// DOM MUTATION
La respuesta se inyecta en el panel
Envuelta en startViewTransition(). Cero red.
0 network
06
// CLEANUP
Liberar al cerrar el panel
Listener 'toggle' → ctrl.abort() + destroy().
no leaks
// 01 — Latency
Latencia Cero
Inferencia instantánea, sin saltos de red.
// 02 — Cost
Costo Cero
Cómputo delegado al hardware del cliente.
// 03 — Privacy
Privacidad 100%
Los datos nunca viajan a ningún servidor.
// MANOS A LA OBRA · PASO 1 DE 2

Construyámoslo: HTML + CSS del menú

menu.html
<!-- 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>
menu.css
/* 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;
}
popover=auto anchor-name position-area view-transition-name // Cero JS para posicionar, cero z-index, cero listeners.
TSH · 4.5 mIU/L · fuera de rango // anchor + popover + view-transition
// Resumen · key-points
Resultado anormal explicado:
  • TSH = hormona que regula la tiroides.
  • Valor 0.5 puntos por encima del rango normal.
  • No es un diagnóstico — consúltalo con tu médico.
// anchored to #applied-anchor · 0 network
// MANOS A LA OBRA · PASO 2 DE 2

Construyámoslo: IA + streaming al panel

summarize-on-open.ts
// 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;
}
follow-up-prompt.ts
// 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();
});
Summarizer.create LanguageModel.create startViewTransition // Resumen automático + follow-up. Datos del paciente nunca dejan el navegador.
// MANOS A LA OBRA · EN VIVO

Simulador de la demo en vivo

https://miportal-salud.local/laboratorio

Resultados de laboratorio · Vanessa A.

Perfil tiroideo del 24/05/2026. Algunos valores están fuera del rango de referencia.

[ ⚠ FUERA DE RANGO ]
TSH (Tirotropina): 4.5 mIU/L · Rango normal: 0.4 – 4.0 mIU/L

El resultado tiene jerga clínica. El asistente local lo traduce a lenguaje claro — sin enviar datos médicos a ningún servidor.

Gemini Nano // Local
streaming
>_ Esperando trigger del paciente.
Pulsa "Explícamelo" para iniciar la inferencia local.
type: key-points length: short local
Qué significa tu TSH
  • TSH es la hormona que controla la tiroides.
  • Tu valor (4.5) está 0.5 puntos por encima del rango normal.
  • Puede indicar hipotiroidismo leve — consúltalo con tu médico.
  • No es un diagnóstico. El asistente nunca reemplaza al profesional.

// done in 612ms · 0 network · 0 datos enviados

// Build with AI · Santa Cruz

Checklist: privacidad & producción

01
Feature Detection estricto
Valida window.ai antes de renderizar la UI del asistente. Sin modelo, sin botón.
02
Model Weights Management
Notifica visualmente la carga de pesos del modelo local (~1.5 GB). Estado, progreso, retry.
03
Sanitización obligatoria
Filtra el output antes de inyectarlo en el DOM. Mitiga Prompt Injection en cada inserción.
04
Zero Local Persistence
No almacenes prompts sensibles en localStorage. Sesión efímera, memoria limpia al cierre.
// Build with AI · Santa Cruz

Explora la demo

El menú contextual completo — markup, CSS y wiring con Summarizer + Prompt API + View Transitions — está disponible en abierto.
// 02 — SLIDES
Esta presentación
// END OF TRANSMISSION
¿Preguntas?
¡Muchas gracias por asistir a este taller práctico!
Instagram
@vanessamarelycode
YouTube
vanessamarely
GitHub
vanessamarely