File size: 6,008 Bytes
054d73a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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>
  );
}