Reasoning
패키지 네임스페이스가 아니라 레시피입니다. 모델의 추론은 답의 Message.Root 안에 넣은 Disclosure입니다. 추론이 도착하는 동안 열려 있고, 답이 시작되면 닫히며, 읽고 싶을 때 언제든 다시 열 수 있습니다. Reasoning.* export는 없습니다.
이 페이지의 내용
Ask again을 눌러 추론이 도착하고, 끝나면 닫히고, 답이 이어지는 모습을 보세요.
Thought for 6 seconds
They are asking whether to show it at all. Showing it explains a slow reply, but it is long and most people only want the answer. So keep it, closed by default, and open it only while it is arriving.
Show it, but keep it out of the way. Open the block while the model thinks, so the wait has something to watch, then close it when the answer starts. Anyone who wants to check the reasoning can open it again.
블록
summary는 블록에 무엇이 있는지 말로 알립니다. 추론이 도착하는 동안에는 "Thinking", 그 뒤에는 걸린 시간입니다. "Thinking" 옆의 Loader dots는 스크린 리더에서 숨깁니다. 단어가 이미 같은 것을 말하기 때문입니다. 문구와 걸린 시간의 로케일은 제품의 것이므로 제품의 문자열에서 가져오세요. 블록은 Message.Content 앞에 두고, 추론은 Text에 담아 넘기거나 markdown이면 Prose에 담아 넘기세요.
import { Disclosure, Loader } from '@tounsoo/todae';
import type { ReactNode } from 'react';
const duration = new Intl.NumberFormat('en', { style: 'unit', unit: 'second', unitDisplay: 'long' });
export function Reasoning({ thinking, seconds, children }: { thinking: boolean; seconds: number; children: ReactNode }) {
return (
<Disclosure.Root open={thinking}>
<Disclosure.Summary>
{thinking ? 'Thinking' : `Thought for ${duration.format(seconds)}`}
{thinking && (
<>
{' '}
<Loader.Root variant="dots" aria-hidden="true" />
</>
)}
</Disclosure.Summary>
{children}
</Disclosure.Root>
);
}
생각하는 동안 열기
답은 세 단계를 거칩니다. 추론이 도착하는 동안의 thinking, 답이 도착하는 동안의 answering, 그리고 done입니다. seconds는 thinking 단계에 걸린 시간으로, stream의 타임스탬프에서 구합니다.
open={thinking}은 추론이 시작되면 블록을 열고, thinking이 false가 되면 닫습니다. React는 prop이 바뀔 때만 open을 설정하므로, 블록이 마운트되어 있는 동안에는 streaming 중에 블록을 닫은 사람에게는 닫힌 채로, 끝난 뒤에 연 사람에게는 열린 채로 남습니다. 다시 마운트된 블록은 현재 단계에서 다시 시작하고 사용자가 바꾼 상태를 잃으므로, 각 메시지의 key를 그대로 유지하세요. 기록에서 불러온 답은 thinking이 false로 렌더링되므로 블록이 닫힌 채로 시작합니다.
메시지는 추론과 답 내내 streaming 상태이므로, Conversation.Content 안에서 답은 시작될 때 많아야 한 번, 끝날 때 한 번 알려지고, 추론과 답 사이에는 다시 알려지지 않습니다.
<Message.Root streaming={phase !== 'done'} aria-label="Assistant">
<Reasoning thinking={phase === 'thinking'} seconds={seconds}>
<Text.Root tone="muted">
{reasoning}
</Text.Root>
</Reasoning>
<Message.Content>…</Message.Content>
{phase === 'answering' && (
<Message.Actions>
<Loader.Root variant="dots" aria-label="Writing" />
</Message.Actions>
)}
</Message.Root>
접근성을 지키는 조합
Todae가 맡는 것: Disclosure.Root는 네이티브 <details>이므로 브라우저가 키보드를 처리하고 펼침 상태를 알립니다. 추론 안의 링크가 포커스를 가진 채로 블록이 닫히면 Chromium, Firefox, WebKit에서는 메인 진입점의 Disclosure.Summary가 포커스를 자기에게로 옮겨 포커스가 페이지로 빠지지 않습니다. streaming을 준 Message.Root는 aria-busy를 설정하고, Conversation은 이를 읽어 메시지가 도착하기 시작할 때와 다 도착했을 때 알립니다.
직접 제공할 것: dots는 숨겨지므로 상태를 말로 알리는 summary 텍스트. 제품의 stream에서 얻는 단계와 걸린 시간.
주의할 점: 추론을 live region에 넣는 것. 스크린 리더가 도착하는 단어를 하나하나 읽습니다. 단어는 그대로이고 dots만 바뀌는 summary. 추론이 끝났는지는 dots가 아니라 "Thinking"과 "Thought for 6 seconds"가 알려 줍니다.
언제 쓰나요
쓸 때: 채팅에서 답 옆에 보여 주는 모델의 추론이나 그 요약.
쓰지 않을 때: 에이전트가 차례로 진행하는 계획 → Steps. streaming 없이 요청할 때까지 숨겨 두는 텍스트 → 평범한 Disclosure.