Spaces:
Runtime error
Runtime error
| 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> | |
| ); | |
| } | |