¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

Mostrar la respuesta de un chatbot en flujo

Después de esta lección: Puedo mostrar en una página web una respuesta en flujo según va llegando.

Lección de 60 min9 comandosLaboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

Mostrar la respuesta según llega

Una página de chat en flujo muestra la respuesta de un chatbot unas pocas palabras cada vez. El servidor las envía según las tiene. Importa porque una respuesta larga puede tardar muchos segundos, y una página en blanco hace pensar que la web falla.

El servidor envía trozos pequeños. La página añade cada trozo a la pantalla. El terminal del laboratorio no tiene navegador, así que aquí construyes la página y el flujo como texto. En tu propio equipo la abrirías en un navegador.

Al final de esta lección

Nota

Puedo mostrar en una página web una respuesta en flujo según va llegando.

Palabras clave

PalabraQué significa
Flujouna respuesta enviada en trozos pequeños
Eventos del servidoruna forma de que un servidor empuje líneas a una página
Manejadorla función que responde a una petición
Escaparcambiar caracteres para que se vean como texto
Trozouna parte pequeña de una respuesta en flujo

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Un servidor puede enviar una respuesta en trozos

Con eventos del servidor, el servidor mantiene la conexión abierta y escribe líneas. Cada evento empieza por data: , lleva un trozo en JSON y acaba con una línea en blanco. El tipo de la respuesta es text/event-stream.

Un último evento, llamado done, lleva una línea data: end y avisa a la página de que la respuesta ha terminado.

Ejemplo resuelto: las líneas de un flujo

Línea enviadaLo que hace la página
data: {"piece": "Hola"} y una línea en blancoAñade Hola a la pantalla
data: {"piece": " y bienvenido"} y una línea en blancoAñade las palabras siguientes detrás
event: done, luego data: end y una línea en blancoCierra la conexión

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

La página añade y puede parar

La página lleva un pequeño script. Abre el flujo y, por cada evento, añade el trozo a un párrafo. Un botón de parar cierra el flujo, así que ya no llegan más trozos.

Añadir un trozo con append y una cadena añade texto simple. Nunca añade marcado.

Escapa el texto del modelo

Un modelo puede repetir lo que se le dio, incluido <script>. Si ese texto entra en una página como HTML, el navegador puede ejecutarlo.

Escapar convierte < en &lt; y los demás caracteres especiales en códigos seguros. Escapa todo texto de modelo que vaya a HTML. Haz & primero, o escaparás tus propios códigos.

Un error frecuente

Advertencia

Mucha gente cree que el texto del modelo es seguro porque tú escribiste las reglas. No lo es. El modelo puede repetir las palabras de un usuario o de una fuente. Trata cada respuesta como no fiable: añádela como texto, o escápala antes.

Escribir la ayuda del modelo, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"estado \" + res.status);' '    return (await res.json()).choices[0].message.content;' > model.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la ayuda del modelo, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  } catch (error) {' '    console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' 'module.exports = { ask };' >> model.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la página y el flujo, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'function quoted(text) { return JSON.stringify(text); }' 'function escapeHtml(text) {' '  return text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")' '    .replace(/\"/g, \"&quot;\").replace(/\x27/g, \"&#39;\");' '}' > ui4.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la página y el flujo, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function pageHtml() {' '  const script = [' '    \"const out = document.getElementById(\" + quoted(\"answer\") + \");\",' '    \"const source = new EventSource(\" + quoted(\"/stream\") + \");\",' '    \"source.onmessage = function (e) { out.append(JSON.parse(e.data).piece); };\",' '    \"source.addEventListener(\" + quoted(\"done\") + \", function () { source.close(); });\",' '    \"document.getElementById(\" + quoted(\"stop\") + \").onclick = function () { source.close(); };\",' '  ].join(\"\n\");' '  return \"<html><body><p id=answer></p><button id=stop>Parar</button><script>\" + script + \"</script></body></html>\";' '}' >> ui4.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la página y el flujo, parte 3

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function sseLine(piece) { return \"data: \" + JSON.stringify({ piece: piece }) + \"\n\n\"; }' 'function streamBody(pieces, stopAfter) {' '  let body = \"\";' '  pieces.slice(0, stopAfter).forEach(function (piece) { body = body + sseLine(piece); });' '  return stopAfter >= pieces.length ? body + \"event: done\ndata: end\n\n\" : body;' '}' >> ui4.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la página y el flujo, parte 4

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function handle(request, pieces) {' '  if (request.url === \"/\") return { status: 200, type: \"text/html\", body: pageHtml() };' '  if (request.url === \"/stream\") {' '    return { status: 200, type: \"text/event-stream\", body: streamBody(pieces, request.stopAfter || 99) };' '  }' '  return { status: 404, type: \"text/plain\", body: \"No encontrado\" };' '}' >> ui4.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la página y el flujo, parte 5

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function show(pieces) {' '  const home = handle({ url: \"/\" }, pieces);' '  console.log(\"GET / ->\", home.status, home.type, home.body.length + \" caracteres\");' '  const live = handle({ url: \"/stream\" }, pieces);' '  console.log(\"GET /stream ->\", live.status, live.type);' '  console.log(live.body.split(\"\n\n\").join(\" | \"));' '  const stopped = handle({ url: \"/stream\", stopAfter: 2 }, pieces);' '  console.log(\"Parar tras 2 trozos:\", stopped.body.split(\"\n\n\").length - 1, \"trozos enviados, sin línea done\");' '  console.log(\"Escapado:\", escapeHtml(\"<b>hola</b> & gracias\"));' '}' >> ui4.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribir la página y el flujo, parte 6

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  // Muestra grabada, usada solo si ningún modelo responde.' '  const sample = \"Hola y bienvenido a bordo, ¿adónde quieres ir?\";' '  const reply = await ask([{ role: \"user\", content: \"Saluda a un viajero de autobús en una frase corta.\" }], sample, 40);' '  show(reply.split(\" \").map(function (word, at) { return at === 0 ? word : \" \" + word; }));' '}' 'main();' >> ui4.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Llamar al manejador

Comando
docker exec -w /home/node/project agent node ui4.js

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Una salida posible

LíneaLo que puede mostrar
GET /200 text/html y el tamaño de la página
GET /stream200 text/event-stream, y luego los trozos de una respuesta separados por |
Parar2 trozos enviados, sin línea done
Escapado&lt;b&gt;hola&lt;/b&gt; &amp; gracias, siempre igual

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Sobre este laboratorio y su código: Laboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.