claw-web-v2 / client /src /components /ThinkingBlock.tsx
Claw Web
Claw Web v1.0 — AI Agent Web Interface with MiMo-V2-Flash
7540aea
/**
* ThinkingBlock — displays model thinking/reasoning blocks in the chat.
* Matches original claw-code thinkback feature.
*/
import { useState } from "react";
import { Brain, ChevronDown, ChevronRight, Clock } from "lucide-react";
interface ThinkingBlockProps {
thinking: string;
durationMs?: number;
collapsed?: boolean;
}
export function ThinkingBlock({ thinking, durationMs, collapsed: initialCollapsed = true }: ThinkingBlockProps) {
const [collapsed, setCollapsed] = useState(initialCollapsed);
if (!thinking) return null;
const lines = thinking.split("\n");
const preview = lines.slice(0, 3).join("\n");
const hasMore = lines.length > 3 || thinking.length > 300;
return (
<div className="my-2 rounded-lg border border-purple-500/20 bg-purple-500/5 overflow-hidden">
<button
onClick={() => setCollapsed(!collapsed)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-purple-400 hover:bg-purple-500/10 transition-colors"
>
<Brain className="w-4 h-4 shrink-0" />
<span className="font-medium">Thinking</span>
{durationMs && (
<span className="flex items-center gap-1 text-xs text-muted-foreground ml-auto mr-2">
<Clock className="w-3 h-3" />
{(durationMs / 1000).toFixed(1)}s
</span>
)}
{collapsed ? (
<ChevronRight className="w-4 h-4 shrink-0 ml-auto" />
) : (
<ChevronDown className="w-4 h-4 shrink-0 ml-auto" />
)}
</button>
{!collapsed && (
<div className="px-3 pb-3 text-sm text-muted-foreground font-mono whitespace-pre-wrap border-t border-purple-500/10 pt-2 max-h-96 overflow-y-auto">
{thinking}
</div>
)}
{collapsed && hasMore && (
<div className="px-3 pb-2 text-xs text-muted-foreground font-mono whitespace-pre-wrap opacity-60 line-clamp-2">
{preview.substring(0, 200)}{preview.length >= 200 ? "..." : ""}
</div>
)}
</div>
);
}
/**
* Parse thinking blocks from a message content array.
*/
export function extractThinkingBlocks(content: unknown): { type: "thinking"; thinking: string; durationMs?: number }[] {
if (!Array.isArray(content)) return [];
return content.filter(
(block: any) => block.type === "thinking" && block.thinking
) as { type: "thinking"; thinking: string; durationMs?: number }[];
}