purcar-chat-api / src /components /ThinkingBox.tsx
ihatebaselines's picture
Deploy Purcar gateway and local model engine
7e92e00 verified
Raw History Blame Contribute Delete
4.62 kB
"use client";
import React, { useState, useEffect } from "react";
import { Brain, ChevronDown, ChevronRight, Sparkles, Clock, CheckCircle2 } from "lucide-react";
interface ThinkingBoxProps {
thinking: string;
isStreaming?: boolean;
thinkingTimeSeconds?: number;
defaultExpanded?: boolean;
}
export const ThinkingBox: React.FC<ThinkingBoxProps> = ({
thinking,
isStreaming = false,
thinkingTimeSeconds,
defaultExpanded = true,
}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [elapsedSeconds, setElapsedSeconds] = useState(thinkingTimeSeconds || 0);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isStreaming) {
const startTime = Date.now() - (elapsedSeconds * 1000);
timer = setInterval(() => {
setElapsedSeconds(Number(((Date.now() - startTime) / 1000).toFixed(1)));
}, 100);
} else if (thinkingTimeSeconds) {
setElapsedSeconds(thinkingTimeSeconds);
}
return () => clearInterval(timer);
}, [isStreaming, thinkingTimeSeconds]);
if (!thinking && !isStreaming) return null;
return (
<div className="mb-4 rounded-xl border border-white/[0.08] bg-black/40 overflow-hidden backdrop-blur-md transition-all duration-300">
{/* Header Button */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-3.5 py-2.5 bg-white/[0.02] hover:bg-white/[0.05] transition-colors text-left group"
>
<div className="flex items-center gap-2.5">
<div className="relative">
<Brain
className={`w-4 h-4 transition-colors ${
isStreaming ? "text-cyan-400 animate-pulse" : "text-zinc-400 group-hover:text-zinc-200"
}`}
/>
{isStreaming && (
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-cyan-400 animate-ping" />
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-mono font-medium text-zinc-300 group-hover:text-white transition-colors">
{isStreaming ? "Thinking Process" : "Thought Process"}
</span>
<span className="text-[11px] font-mono text-zinc-500 flex items-center gap-1">
<Clock className="w-3 h-3" />
{elapsedSeconds > 0 ? `${elapsedSeconds.toFixed(1)}s` : "0.8s"}
</span>
</div>
</div>
<div className="flex items-center gap-2">
{isStreaming ? (
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase px-2 py-0.5 rounded-full bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
<Sparkles className="w-2.5 h-2.5 animate-spin" />
Reasoning
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-mono uppercase px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
<CheckCircle2 className="w-2.5 h-2.5" />
Verified
</span>
)}
{isExpanded ? (
<ChevronDown className="w-3.5 h-3.5 text-zinc-500 group-hover:text-zinc-300 transition-transform" />
) : (
<ChevronRight className="w-3.5 h-3.5 text-zinc-500 group-hover:text-zinc-300 transition-transform" />
)}
</div>
</button>
{/* Collapsible Content */}
{isExpanded && (
<div className="px-4 py-3 border-t border-white/[0.04] bg-black/60 text-xs font-mono text-zinc-400 leading-relaxed space-y-1.5 overflow-x-auto">
{thinking.split("\n").map((line, idx) => {
if (!line.trim()) return <div key={idx} className="h-1" />;
const isStepHeader = line.match(/^(\d+\.|\[.*?\])/);
return (
<div
key={idx}
className={`flex gap-2 ${
isStepHeader ? "text-zinc-200 font-semibold pt-1 border-t border-white/[0.03] first:border-0 first:pt-0" : "text-zinc-400 pl-4"
}`}
>
<span>{line}</span>
</div>
);
})}
{isStreaming && (
<div className="flex items-center gap-1.5 text-cyan-400 text-[11px] pt-1">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-ping" />
<span className="italic">Evaluating transformer self-attention states...</span>
</div>
)}
</div>
)}
</div>
);
};