コンテンツにスキップ

チャットメッセージ

会話とは、サーバーが描画するトランスクリプトと、そこへ追記する フォームにすぎません。1 回の POST が 1 つのボディに 3 つの フラグメントを返します: ユーザーのメッセージ、アシスタントの aria-busy プレースホルダー — streaming-response レシピが SSE で埋めるハンドルです — そして帯域外でスワップされる 新しいコンポーザー。カスタム JavaScript はゼロで、auto-init の installChatScroll() が読者を最下部に保ちます。

メッセージを入力して送信してみてください — POST が 3 フラグメントの ボディを返します: エスケープ済みのユーザー <li>aria-busy の アシスタントプレースホルダー、そして帯域外のコンポーザーリセット。 その後プレースホルダーが応答をストリームします — そちら側は streaming-response レシピです。 空のまま送信すると 422 がコンポーザーだけに返り、「fail」を含む プロンプトはエラー経路を、Stop はストリーム途中の切り詰めを実演します。 エンドポイントはサーバレスポンス契約を実装した名前空間付きデモで、 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.

コンポーザーはチャット ルートの最後の子として入れ子になり、やり取り全体が 1 つの合成に なります:

<div class="hc-chat" role="log" aria-label="Conversation">
<ol class="hc-chat__list" tabindex="0" id="chat-list">
<!-- server-rendered history -->
</ol>
<button class="hc-chat__jump hc-button" data-size="sm" type="button">↓ Latest</button>
<form class="hc-field" id="composer" method="post" action="/chat/messages"
data-hx-post="/chat/messages"
data-hx-target="#chat-list" data-hx-swap="beforeend">
<label class="hc-field__label" for="prompt">Message</label>
<textarea class="hc-input" id="prompt" name="prompt" rows="2"></textarea>
<button class="hc-button" data-variant="primary" type="submit">Send</button>
</form>
</div>

各属性がある理由:

属性目的
role="log"(ルート)追記された完成したメッセージが読み上げられます。このロールは <ol> には許可されません。
tabindex="0"(リスト)スクロール領域にキーボードで到達できます。
data-hx-target="#chat-list" + data-hx-swap="beforeend"POST はトランスクリプトへ追記します — 全体の再描画はしません。
method="post" + actionno-JS の経路: 素のフォーム送信で、303 が返ります。
ケースステータスボディ
メッセージ受理200ユーザーの <li data-role="user"> + アシスタントのプレースホルダー <li data-role="assistant" data-state="streaming" aria-busy="true" id="reply-…">(どちらも beforeend で着地)、さらに data-hx-swap-oob="outerHTML" 付きの新しいコンポーザー — トランスクリプトに触れずに textarea がクリアされます。
空 / 不正なプロンプト422帯域外のコンポーザー再描画のみ: data-invalid、textarea の aria-invalid + aria-describedby.hc-field__message のエラー。トランスクリプトを対象にするものが無いため、偽エントリは現れません。一度きりの htmx:beforeSwap 422 許可が必要です。
JS なし303会話ページへのリダイレクト(HX-Request ヘッダーで分岐)。フルページに新しいやり取りが含まれます。

描画はサーバーの責務です: エコーするプロンプトは必ずエスケープ してください(トランスクリプトは HTML シンクです)。クライアント側 markdown は使いません — アシスタントの応答はサーバー描画の HTML フラグメント(hc-code ブロック込み)で届きます。

  • プレースホルダーの aria-busy="true" が log リージョンの読み上げを 保留します: 最終スワップ(streaming-response の done イベント、または非ストリーミングでの完成)が属性を外した瞬間に、 支援技術は応答を 1 回だけ読み上げます。
  • 422 のコンポーザーは field-errors のパターン(aria-invalidaria-describedby)に従います。
  • 実 htmx のブラウザテスト(test-browser/chat-messages.spec.mjs)で 固定: 追記 + プレースホルダー、OOB リセット、422 の隔離、6 往復 にわたる最下部追従、axe。

コンポーザーはアップロード系のパターンと端から端まで合成できます — 新しい契約は不要です:

  1. コンポーザーを enctype="multipart/form-data" にし、textarea の 隣にドロップゾーン (または素の file input)を置きます。
  2. POST のアップロード中は、installUploadProgress() (file-upload レシピ) が保留中の各添付カードの 進捗行を駆動します。
  3. サーバーはファイルを ユーザーの <li> の内側hc-attachments リストとしてエコーします — 添付カードは メッセージのマークアップの一部なので、トランスクリプトは 1 つの サーバー描画ドキュメントのままです。
  4. 応答側は不変です: プレースホルダーと ストリーミング契約は 添付の有無にかかわらず同じです。

ライブで試せます — 同じデモのマルチパート版コンポーザーです: ファイル(1 MB 以下・種類は自由)を添付して送信すると、サーバーが 添付カードをユーザーの <li> の内側にエコーしてから応答をストリーム します。

  1. Hi! Attach a file (1 MB or smaller), add a message if you like, and send — the attachment card is echoed inside your message.