chih.yikuan
πŸš€ ExamInsight: AI-powered exam analysis for teachers
054d73a
import { useState, useEffect, useRef } from "react";
interface ReasoningStep {
id: string;
type: "tool" | "result" | "error" | "thinking";
content: string;
timestamp: Date;
status: "running" | "completed";
}
interface ReasoningPanelProps {
sessionId?: string;
}
export function ReasoningPanel({ sessionId = "default" }: ReasoningPanelProps) {
const [steps, setSteps] = useState<ReasoningStep[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Subscribe to status updates
useEffect(() => {
let eventSource: EventSource | null = null;
const connect = () => {
// Use relative URL for production, works with both local and HF Spaces
const apiBase = import.meta.env.VITE_API_BASE_URL || "";
eventSource = new EventSource(`${apiBase}/api/status/${sessionId}/stream`);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle reasoning steps from backend
if (data.reasoning && data.reasoning.length > 0) {
setIsStreaming(true);
const newSteps: ReasoningStep[] = data.reasoning.map((step: any, index: number) => ({
id: `step-${index}-${step.timestamp}`,
type: step.type as ReasoningStep["type"],
content: step.content,
timestamp: new Date(step.timestamp),
status: step.status === "active" ? "running" : "completed",
}));
setSteps(newSteps);
}
if (data.completed_at) {
setIsStreaming(false);
}
} catch (e) {
console.error("Error parsing status:", e);
}
};
eventSource.onerror = () => {
eventSource?.close();
// Reconnect after 3 seconds
setTimeout(connect, 3000);
};
};
connect();
return () => eventSource?.close();
}, [sessionId]);
// Auto-scroll to bottom
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [steps]);
const getIcon = (type: ReasoningStep["type"], status: string) => {
if (status === "running") {
return (
<div className="w-4 h-4 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" />
);
}
switch (type) {
case "tool":
return <span className="text-blue-400">πŸ”§</span>;
case "result":
return <span className="text-green-400">βœ“</span>;
case "error":
return <span className="text-red-400">βœ—</span>;
case "thinking":
return <span className="text-purple-400">🧠</span>;
default:
return <span>β€’</span>;
}
};
const getColor = (type: ReasoningStep["type"], status: string) => {
if (status === "running") return "text-blue-300 bg-blue-500/10 border-blue-500/30";
switch (type) {
case "tool":
return "text-blue-300 bg-gray-800/50 border-gray-600";
case "result":
return "text-green-300 bg-green-500/10 border-green-500/30";
case "error":
return "text-red-300 bg-red-500/10 border-red-500/30";
case "thinking":
return "text-purple-300 bg-purple-500/10 border-purple-500/30";
default:
return "text-gray-300 bg-gray-800/50 border-gray-600";
}
};
return (
<div className="bg-gray-900 rounded-xl border border-gray-700 overflow-hidden h-full flex flex-col">
{/* Header */}
<div className="px-4 py-3 bg-gray-800 border-b border-gray-700 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-white">πŸ€– Tool Calls</span>
{isStreaming && (
<span className="flex items-center gap-1 text-xs text-green-400">
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
Live
</span>
)}
</div>
{steps.length > 0 && (
<button
onClick={() => setSteps([])}
className="text-xs text-gray-500 hover:text-gray-300 transition-colors"
>
Clear
</button>
)}
</div>
{/* Tool call log */}
<div
ref={containerRef}
className="p-3 flex-1 overflow-y-auto font-mono text-sm"
>
{steps.length === 0 ? (
<div className="text-gray-500 text-center py-8">
<div className="text-2xl mb-2">πŸ”§</div>
<p>Tool calls will appear here</p>
<p className="text-xs mt-2">When AI uses tools, you'll see them logged</p>
</div>
) : (
<div className="space-y-2">
{steps.map((step) => (
<div
key={step.id}
className={`flex items-start gap-2 p-2 rounded-lg border ${getColor(step.type, step.status)}`}
>
<span className="flex-shrink-0 mt-0.5">
{getIcon(step.type, step.status)}
</span>
<div className="flex-1 min-w-0">
<span className="block break-words whitespace-pre-wrap">
{step.content}
</span>
<span className="text-xs opacity-50 mt-1 block">
{step.timestamp.toLocaleTimeString()}
</span>
</div>
</div>
))}
{isStreaming && (
<div className="flex items-center gap-2 text-gray-500 mt-2 justify-center py-2">
<div className="w-3 h-3 border-2 border-gray-500 border-t-transparent rounded-full animate-spin" />
<span className="text-xs">Waiting for next action...</span>
</div>
)}
</div>
)}
</div>
</div>
);
}