コンテンツにスキップ

ストリーミングレスポンス

chat-messages の POST は、aria-busy のアシスタントプレースホルダーをトランスクリプト に残します。このレシピは、サーバーがそれを埋める方法です: プレースホルダーが 自分自身の SSE 接続を所有し、chunk イベントが描画済み HTML をボディへ追記、done がプレースホルダー全体 を完成した最終メッセージに置換します — この置換が同時に EventSource を閉じます(htmx は接続要素が DOM から離れたストリームを閉じます)。 カスタム JavaScript はゼロです。

別名: AI 応答ストリーミング、タイプライター表示。

chat-messages と同じ合成デモを、ストリーム側から観察します: POST が追記したプレースホルダーが自分の SSE 接続を開き、chunk イベントがエスケープ済みテキストを追記する間 aria-busy が読み上げを 保留し、done が完成した最終 <li> に置換して EventSource を閉じます。 Stop は 1 往復で切り詰め、「fail」を含むプロンプトは error イベントと リトライ手段を実演します。このデモは chat-messages の名前空間 (api/recipes/chat-messages/…)を共有しています。

  1. Hi! Send a message and I will stream back a canned reply. Include the word “fail” to watch the error path instead.

これは chat-messages の 200 が返すプレースホルダー <li> です — すべてがこの要素に載ります(htmx の隣に sse.min.js を読み込みます。 sse-updates と同じ です):

<li class="hc-chat__message" data-role="assistant" data-state="streaming"
aria-busy="true" id="reply-42"
data-hx-ext="sse" data-sse-connect="/chat/messages/42/stream"
data-sse-swap="done,error" data-hx-swap="outerHTML">
<div class="hc-chat__body" data-sse-swap="chunk" data-hx-swap="beforeend"></div>
<button class="hc-button" data-size="sm" type="button"
data-hx-post="/chat/messages/42/stop"
data-hx-target="closest li" data-hx-swap="outerHTML">Stop</button>
</li>

各属性がある理由:

属性目的
data-sse-connect(<li> 上)プレースホルダーがストリームを所有します。done / error<li> をスワップした瞬間に htmx が EventSource を閉じるので、後始末のコードは不要です。
data-sse-swap="chunk" + data-hx-swap="beforeend"(ボディ)chunk イベントのデータがボディへ追記されます。
data-sse-swap="done,error" + data-hx-swap="outerHTML"(<li>)最終イベントは完全な置換用 <li> を運びます。
停止ボタン1 往復で完結: サーバー側でキャンセルし、途中までの最終メッセージに置換し、ストリームを閉じます。
イベントデータ効果
chunkHTML テキストフラグメント — サーバーでエスケープ済み、1 行(SSE は data: 1 行を 1 フレームにします)ボディへ追記。サブツリーが aria-busy のため、読み上げは保留されたままです。
done完成した最終メッセージ <li> — サーバー描画(hc-code トークン込み)、aria-busy なし、data-state なし、ストリーム属性なしプレースホルダーを置換。aria-busy が外れることが、role="log" リージョンに完成した応答を 1 回だけ読み上げさせます。
errorリトライ手段を持つ最終 <li data-state="error">(aria-busy なし — 失敗は読み上げられます)done と同じスワップとクローズ。

描画とエスケープはすべてサーバーの責務です — chunk のテキストも 含めて。クライアント側 markdown は使わず、描画済み HTML をストリーム してください。複数行の data: にはせず、イベントを分けて送ります。

  • チャンクは aria-busy="true" のサブツリー内に着地するため、支援 技術がトークンごとに読み上げに追われることはありません。最終 スワップが属性を外したときに、応答は一度だけ、完成形で読み上げ られます。
  • 停止ボタンはプレースホルダー内の実ボタンです — ストリーム実行中は 到達可能で、最終メッセージからは消えます。
  • 実 SSE のブラウザテスト(test-browser/streaming-response.spec.mjs) で固定: aria-busy 保持中のボディ成長、done / error スワップ、 1 往復の停止、ストリーミング中の最下部追従、ストリーム途中の axe。
  • chat-messages — このプレースホルダーを追記するコンポーザーの往復(添付の合成もここで文書化)。
  • チャット — トランスクリプトコンポーネント。チャンク着地中も installChatScroll() が読者を最下部に保ちます。
  • sse-updates — このレシピが乗る汎用 SSE フラグメント契約。