Showing How the AI Thinks
Recent LLMs can walk through their reasoning step by step using Chain-of-Thought. Claude's <thinking> block is a prime example. In XGen, we built a ThinkBlock component to visualize this reasoning process.
What Is ThinkBlock
ThinkBlock is a component that renders the AI's reasoning as a collapsible block. Users can read just the final response, or expand the block to see exactly how the AI worked through the problem.
Component Implementation
interface ThinkBlockProps {
thinking: string;
isStreaming?: boolean;
}
const ThinkBlock: React.FC<ThinkBlockProps> = ({ thinking, isStreaming }) => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="my-3 border border-purple-200 rounded-lg overflow-hidden">
<button
className="w-full px-4 py-2 flex items-center gap-2 bg-purple-50 hover:bg-purple-100 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<BrainIcon className="w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-purple-700">
AI 추론 과정
</span>
{isStreaming && (
<span className="text-xs text-purple-400 animate-pulse">
생각 중...
</span>
)}
<ChevronIcon
className={`ml-auto w-4 h-4 transition-transform ${
isExpanded ? 'rotate-180' : ''
}`}
/>
</button>
{isExpanded && (
<div className="px-4 py-3 text-sm text-gray-600 bg-purple-25 border-t border-purple-100">
<MarkdownRenderer content={thinking} />
</div>
)}
</div>
);
};
Handling ThinkBlock During Streaming
When a <thinking> tag appears in an SSE stream, we create a ThinkBlock and append content to the thinking region until the tag closes. Once the closing tag arrives, all subsequent text is treated as the normal response.
function parseStreamChunk(buffer: string): ParseResult {
const thinkingMatch = buffer.match(/<thinking>([\s\S]*?)<\/thinking>/);
if (thinkingMatch) {
const thinking = thinkingMatch[1];
const answer = buffer.replace(/<thinking>[\s\S]*?<\/thinking>/, '').trim();
return { thinking, answer, isThinkingComplete: true };
}
// thinking 태그가 아직 닫히지 않은 경우
const openMatch = buffer.match(/<thinking>([\s\S]*)/);
if (openMatch) {
return { thinking: openMatch[1], answer: '', isThinkingComplete: false };
}
return { thinking: '', answer: buffer, isThinkingComplete: true };
}
Design Decision: Collapsed vs. Expanded by Default
We initially defaulted ThinkBlock to expanded, but user testing showed that most users care about the final answer rather than the reasoning trace, so we switched to collapsed by default. The exception is developer mode, where the block is expanded by default.
Visualizing Multi-Step Reasoning
In complex workflows, multiple nodes may reason sequentially. Nesting each node's ThinkBlock lets users follow the full reasoning flow from start to finish.
Animation Details
We used framer-motion's AnimatePresence to animate the height transition as ThinkBlock collapses and expands. It's a small detail, but it meaningfully improves UX polish.