チャットメッセージ
会話とは、サーバーが描画するトランスクリプトと、そこへ追記する
フォームにすぎません。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/ 配下にあります。
blessed マークアップ
Section titled “blessed マークアップ”コンポーザーはチャット ルートの最後の子として入れ子になり、やり取り全体が 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" + action | no-JS の経路: 素のフォーム送信で、303 が返ります。 |
サーバレスポンス契約
Section titled “サーバレスポンス契約”| ケース | ステータス | ボディ |
|---|---|---|
| メッセージ受理 | 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 ブロック込み)で届きます。
アクセシビリティ
Section titled “アクセシビリティ”- プレースホルダーの
aria-busy="true"が log リージョンの読み上げを 保留します: 最終スワップ(streaming-response のdoneイベント、または非ストリーミングでの完成)が属性を外した瞬間に、 支援技術は応答を 1 回だけ読み上げます。 - 422 のコンポーザーは field-errors のパターン(
aria-invalid、aria-describedby)に従います。 - 実 htmx のブラウザテスト(
test-browser/chat-messages.spec.mjs)で 固定: 追記 + プレースホルダー、OOB リセット、422 の隔離、6 往復 にわたる最下部追従、axe。
添付ファイル
Section titled “添付ファイル”コンポーザーはアップロード系のパターンと端から端まで合成できます — 新しい契約は不要です:
- コンポーザーを
enctype="multipart/form-data"にし、textarea の 隣にドロップゾーン (または素の file input)を置きます。 - POST のアップロード中は、
installUploadProgress()(file-upload レシピ) が保留中の各添付カードの 進捗行を駆動します。 - サーバーはファイルを ユーザーの
<li>の内側にhc-attachmentsリストとしてエコーします — 添付カードは メッセージのマークアップの一部なので、トランスクリプトは 1 つの サーバー描画ドキュメントのままです。 - 応答側は不変です: プレースホルダーと ストリーミング契約は 添付の有無にかかわらず同じです。
ライブで試せます — 同じデモのマルチパート版コンポーザーです:
ファイル(1 MB 以下・種類は自由)を添付して送信すると、サーバーが
添付カードをユーザーの <li> の内側にエコーしてから応答をストリーム
します。
- チャット — トランスクリプトコンポーネントとそのトークン。
- ストリーミングレスポンス — プレースホルダーを SSE で埋めます(汎用のプッシュチャネルは sse-updatesが文書化しています)。
- field-errors — このコンポーザーが再利用する 422 の規約。
- file-upload — OOB の新フォームリセットパターン。