思考區塊 (Thinking Block)
- 新提案
- →
- 封閉測試
- →
- 公開測試
- →
- 已穩定
成熟度說明
- 新提案:未完成開發、請勿使用。
- 封閉測試:開發暫時性完成,可使用。可能仍有親和力或其他使用上問題待透過實測發現。
- 公開測試:開發暫時性完成,歡迎使用。具有完整親和力報告。
- 已穩定:開發完成、歡迎使用。應無任何親和力或使用上問題。
基本摘要
思考摘要 摘要已完成
已確認申請條件、應備文件與受理方式,接著整理成可直接採行的說明。
HTML
<thinking-block data-label="思考摘要" data-state="complete">
<details class="thinking-block" open>
<summary class="thinking-block__summary">
<span class="thinking-block__label">思考摘要</span>
<span class="thinking-block__status">摘要已完成</span>
</summary>
<div class="thinking-block__content">已確認申請條件、應備文件與受理方式,接著整理成可直接採行的說明。</div>
<p class="thinking-block__error" hidden></p>
</details>
</thinking-block>- 使用原生
<details>/<summary>,可由使用者自行開合。 - 摘要只放適合公開閱讀的純文字;必要警示與最終答覆不應只放在此處。
多個摘要
思考摘要 摘要已完成
已比對服務對象與申請資格,確認此案適用一般申請流程。
資料核對摘要 摘要已完成
已核對申請表與附件欄位;缺少的文件會在後續答覆中明確列出。
HTML
<div class="thinking-block-demo">
<thinking-block data-label="思考摘要" data-state="complete">
<details class="thinking-block" open>
<summary class="thinking-block__summary">
<span class="thinking-block__label">思考摘要</span>
<span class="thinking-block__status">摘要已完成</span>
</summary>
<div class="thinking-block__content">已比對服務對象與申請資格,確認此案適用一般申請流程。</div>
<p class="thinking-block__error" hidden></p>
</details>
</thinking-block>
<thinking-block data-label="資料核對摘要" data-state="complete">
<details class="thinking-block">
<summary class="thinking-block__summary">
<span class="thinking-block__label">資料核對摘要</span>
<span class="thinking-block__status">摘要已完成</span>
</summary>
<div class="thinking-block__content">已核對申請表與附件欄位;缺少的文件會在後續答覆中明確列出。</div>
<p class="thinking-block__error" hidden></p>
</details>
</thinking-block>
</div>- 每個摘要可獨立開合,不會因開啟另一個摘要而收合。
動態摘要
此示範只產生可公開的純文字摘要,不會呼叫模型或任何外部 API。
尚未開始示範。
已寫入宣告區域
- 尚未有任何已確認寫入。
HTML
<thinking-block data-label="思考摘要" data-state="streaming">
<details class="thinking-block" open>
<summary class="thinking-block__summary">
<span class="thinking-block__label">思考摘要</span>
<span class="thinking-block__status">摘要更新中</span>
</summary>
<div class="thinking-block__content">正在整理可公開的摘要。</div>
<p class="thinking-block__error" hidden></p>
</details>
</thinking-block>- 「已寫入宣告區域」只表示示範的狀態文字已寫入 DOM,不代表螢幕閱讀器已完成語音播報。
- 按鈕僅做本機模擬;摘要正文不會逐段放進 live region,準備階段只顯示於畫面,不另外播報。
- 示範執行期間,三個啟動按鈕會標記為不可用,並保留目前按鈕的鍵盤焦點;完成後才恢復操作。
使用方式
- 靜態:伺服器輸出已完成的摘要時,保留下方完整結構即可;沒有 JavaScript 時仍可閱讀與開合。已有文字的結構預設為完成狀態。
- 動態:載入
thinking-block.js,在元件接到 DOM 且customElements.whenDefined('thinking-block')完成後,再以appendText()追加文字並呼叫終結方法。
<thinking-block data-label="思考摘要" data-state="complete">
<details class="thinking-block" open>
<summary class="thinking-block__summary">
<span class="thinking-block__label">思考摘要</span>
<span class="thinking-block__status">摘要已完成</span>
</summary>
<div class="thinking-block__content">可公開閱讀的摘要。</div>
<p class="thinking-block__error" hidden></p>
</details>
</thinking-block>const block = document.createElement('thinking-block');
block.setAttribute('data-label', '思考摘要');
container.append(block);
await customElements.whenDefined('thinking-block');
block.appendText('先整理適合公開的摘要。');
block.appendText('再追加下一段。');
block.complete();API
| 名稱 | 說明 |
|---|---|
state | 唯讀,值為 idle、streaming、complete、aborted、error;data-state 同步反映目前狀態 |
appendText(text) | 追加非空純文字;第一段非空文字讓狀態由 idle 進入 streaming,只有空白的第一段會被拒絕 |
complete()、abort()、error(message) | 結束摘要並保留已有文字;error() 只在有摘要時顯示錯誤說明,未提供訊息時使用預設文字 |
thinking-block:statechange | 會冒泡的事件,detail 為 { state, hasContent };首段、完成、停止與錯誤各發出一次,片段追加、開合與重新插入不發出 |
- 所有方法都回傳布林值。只有已初始化且仍連接到 DOM 的執行個體會接受操作;終結後再新增或再次終結,以及元件移出 DOM 期間的呼叫,都會回傳
false。 - 重新插入會保留文字與開合狀態;要重試請建立新的執行個體。
- 已有文字的結構若設定
data-state="streaming"或data-state="idle",載入後會以streaming狀態繼續接受追加。
摘要內容
元件只顯示可公開閱讀的純文字,不解析 HTML 或 Markdown。不要傳入模型的原始思考、簽章、加密資料或其他不適合公開的內容;含 Markdown 的摘要須先轉為純文字。供應商回傳的事件,請先由採用端整理成下列其中一種情況:
| 情況 | 做法 |
|---|---|
| 逐段到達 | 以 appendText() 逐段追加,最後呼叫 complete()、abort() 或 error()。多段摘要請自行加入段落分隔(例如 \n\n);收到完整快照後,不要再追加相同的片段 |
| 一次到齊 | 載入既有對話時,在插入 DOM 前寫入文字並設定 data-state="complete",元件直接完成且不發出事件;回合進行中才到齊時,連續呼叫 appendText() 與 complete() |
| 沒有摘要 | 例如未啟用摘要,或只回傳空字串、簽章時,可以不建立元件。若已建立,終結方法仍會發出 hasContent: false 的事件,但不顯示可開合區塊與錯誤說明,採用端不應據此播報 |
更新中的摘要可由使用者收合原生 details 隱藏;元件不管理後端請求的取消。
狀態播報
元件不建立 live region,也不自動播報。動態摘要需要狀態通知時,由採用端透過頁面上唯一的即時宣告 <live-announcer> 送出簡短狀態,並遵守下列規則:
- 以事件的
target對應自己的回合;同一回合只由一個協調者送出摘要狀態,避免和輸入中提示或訊息氣泡競爭同一個宣告頻道。 - 沒有可見摘要(
hasContent: false)時不播報。 - 狀態在寫入前又變化時,只送出最新一則,並以
live-announcer:announced事件確認實際寫入;不建立全域佇列。 <live-announcer>是全頁共用頻道,其他元件的呼叫可能覆蓋尚未寫入的狀態。請設定確認期限:逾時不代表已播報,只是停止等待,改由摘要標題中的可見狀態提供資訊。- 答案開始後由訊息氣泡負責通知,摘要不再送出狀態。若同時使用輸入中提示,也要先完成它的宣告交接。
- 回合在答案開始前取消時,移除監聽器與計時器;已交給即時宣告元件的訊息無法撤回,也可能被後續訊息覆蓋。
下列範例實作以上規則,適用於尚未開始答案的摘要階段:
- 在第一段文字抵達前安裝監聽器;
block是新建立、尚未呼叫appendText()的摘要實例。 - 答案資料抵達時呼叫
beginAnswer(start);摘要狀態確認寫入或逾時後,才會執行start。 - 回合在答案開始前取消時呼叫
cancelSummary(),之後不會啟動答案,也不再送出摘要狀態。若「已停止」通知由其他元件負責(例如訊息氣泡),先呼叫cancelSummary()再呼叫block.abort(),摘要只更新可見狀態;若要由摘要提出「摘要已停止更新」的宣告,則先呼叫block.abort()再呼叫cancelSummary()。
await customElements.whenDefined('live-announcer');
const announcer = document.querySelector('live-announcer');
if (!announcer || typeof announcer.announce !== 'function') {
throw new Error('缺少 live-announcer');
}
const MESSAGES = {
streaming: '摘要更新中',
complete: '摘要已完成',
aborted: '摘要已停止更新',
error: '摘要未完成',
};
const CONFIRM_TIMEOUT = 5000;
let closed = false; // 答案已開始或回合已取消
let pending = null;
let timer = null;
let startAnswer = null;
function settle() {
clearTimeout(timer);
pending = null;
const next = startAnswer;
startAnswer = null;
if (next) next();
}
function onAnnounced(event) {
if (event.target !== announcer || pending === null) return;
if (event.detail.level === 'polite' && event.detail.message === pending) settle();
}
announcer.addEventListener('live-announcer:announced', onAnnounced);
function onStatechange(event) {
if (event.target !== block || closed) return;
const { state, hasContent } = event.detail;
if (!hasContent) return; // 沒有可見摘要時不播報
pending = MESSAGES[state];
announcer.announce(pending);
clearTimeout(timer);
timer = setTimeout(settle, CONFIRM_TIMEOUT);
}
block.addEventListener('thinking-block:statechange', onStatechange);
function detach() {
clearTimeout(timer);
pending = null;
startAnswer = null;
announcer.removeEventListener('live-announcer:announced', onAnnounced);
block.removeEventListener('thinking-block:statechange', onStatechange);
}
function beginAnswer(start) {
if (closed) return;
const run = () => {
closed = true;
detach();
start(); // 之後由訊息氣泡負責通知
};
if (pending === null) run();
else startAnswer = run;
}
function cancelSummary() {
if (closed) return;
closed = true;
detach();
}CSS
.thinking-block:摘要的原生開合容器。.thinking-block__summary/__label/__status:標題與目前狀態。.thinking-block__content/__error:純文字內容與錯誤說明。
親和力
- 原生
summary提供鍵盤開合,不需額外加role、button或aria-expanded。 - 元件不建立 live region,也不自動收合、移焦或捲動;動態摘要的狀態通知規則見「狀態播報」。
- 摘要若位於聊天
role="log",串流組合可設定aria-live="off",但這不是保證靜音;需以實際使用的螢幕閱讀器與瀏覽器驗證焦點在 log 內時是否重複或漏播。
JavaScript
元件為 <thinking-block> 自訂元素,以 ES module 撰寫,載入必須加 type="module"。若跨網域載入,伺服器須提供 CORS 標頭;路徑請依實際部署位置調整。
<script type="module" src="js/components/thinking-block.js"></script>- 靜態完成摘要不需要 JavaScript。動態摘要需載入
thinking-block.js;若需要狀態通知,還需先在頁面掛載一個live-announcer.js/<live-announcer>。文件頁的示範腳本不是產品依賴。 - 建立摘要前先確定要公開的內容。元件只處理呈現,不管理模型 API、網路取消、答案串流、重試、焦點或捲動。
參考
- The Details disclosure element - HTML Living Standard
- Disclosure (Show/Hide) Pattern - ARIA Authoring Practices Guide