Spaces:
Sleeping
Sleeping
| /** | |
| * ChatWindow Component | |
| * | |
| * Main chat interface component that handles: | |
| * - Message display | |
| * - User input | |
| * - Streaming responses from A2UI protocol | |
| */ | |
| import React, { useEffect, useRef, useState } from "react"; | |
| import { A2UIMessage, MessageType, Tool } from "../types/a2ui"; | |
| import { streamChat, checkHealth, getTools, clearHistory } from "../utils/api"; | |
| import MessageBubble from "./MessageBubble"; | |
| import "./ChatWindow.css"; | |
| interface ChatMessageDisplay extends A2UIMessage { | |
| id: string; | |
| timestamp: string; | |
| } | |
| const ChatWindow: React.FC = () => { | |
| const [messages, setMessages] = useState<ChatMessageDisplay[]>([]); | |
| const [inputValue, setInputValue] = useState(""); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [isConnected, setIsConnected] = useState(false); | |
| const [tools, setTools] = useState<Tool[]>([]); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| // Check backend health on mount | |
| useEffect(() => { | |
| const checkConnection = async () => { | |
| const healthy = await checkHealth(); | |
| setIsConnected(healthy); | |
| if (healthy) { | |
| const availableTools = await getTools(); | |
| setTools(availableTools); | |
| } | |
| }; | |
| checkConnection(); | |
| // Recheck every 30 seconds | |
| const interval = setInterval(checkConnection, 30000); | |
| return () => clearInterval(interval); | |
| }, []); | |
| // Scroll to bottom when new messages arrive | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [messages]); | |
| const handleSendMessage = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!inputValue.trim()) return; | |
| if (!isConnected) { | |
| setError("Not connected to backend. Please check your connection."); | |
| return; | |
| } | |
| const userMessage = inputValue.trim(); | |
| setInputValue(""); | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| // Add user message to display | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| type: MessageType.USER_MESSAGE, | |
| content: userMessage, | |
| id: `msg-${Date.now()}`, | |
| timestamp: new Date().toISOString(), | |
| }, | |
| ]); | |
| // Stream response from backend | |
| await streamChat( | |
| userMessage, | |
| (message: A2UIMessage) => { | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| ...message, | |
| id: `msg-${Date.now()}-${Math.random()}`, | |
| timestamp: message.timestamp || new Date().toISOString(), | |
| }, | |
| ]); | |
| }, | |
| (error: Error) => { | |
| setError(`Error: ${error.message}`); | |
| } | |
| ); | |
| } catch (err) { | |
| setError(`Error: ${err instanceof Error ? err.message : "Unknown error"}`); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleClearHistory = async () => { | |
| if (window.confirm("Are you sure you want to clear chat history?")) { | |
| const success = await clearHistory(); | |
| if (success) { | |
| setMessages([]); | |
| } else { | |
| setError("Failed to clear history"); | |
| } | |
| } | |
| }; | |
| return ( | |
| <div className="chat-window"> | |
| {/* Header */} | |
| <div className="chat-header"> | |
| <h1>AI Agent Chat</h1> | |
| <div className="header-info"> | |
| <span className={`status-indicator ${isConnected ? "connected" : "disconnected"}`}> | |
| {isConnected ? "●" : "●"} {isConnected ? "Connected" : "Disconnected"} | |
| </span> | |
| {tools.length > 0 && ( | |
| <span className="tools-indicator">{tools.length} tools available</span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Messages area */} | |
| <div className="messages-container"> | |
| {messages.length === 0 && ( | |
| <div className="empty-state"> | |
| <h2>Welcome to AI Agent Chat</h2> | |
| <p> | |
| {isConnected | |
| ? "Start a conversation by typing a message below." | |
| : "Waiting for backend connection..."} | |
| </p> | |
| {tools.length > 0 && ( | |
| <div className="available-tools"> | |
| <h3>Available Tools:</h3> | |
| <ul> | |
| {tools.map((tool) => ( | |
| <li key={tool.name}> | |
| <strong>{tool.name}</strong> - {tool.description} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {messages.map((message) => ( | |
| <MessageBubble | |
| key={message.id} | |
| message={message} | |
| isUser={message.type === MessageType.USER_MESSAGE} | |
| /> | |
| ))} | |
| <div ref={messagesEndRef} /> | |
| {isLoading && ( | |
| <div className="loading-indicator"> | |
| <div className="spinner"></div> | |
| <span>Agent is processing...</span> | |
| </div> | |
| )} | |
| </div> | |
| {/* Error display */} | |
| {error && ( | |
| <div className="error-message"> | |
| <span className="error-icon">⚠️</span> | |
| {error} | |
| <button onClick={() => setError(null)} className="close-error"> | |
| × | |
| </button> | |
| </div> | |
| )} | |
| {/* Input area */} | |
| <form onSubmit={handleSendMessage} className="input-area"> | |
| <div className="input-wrapper"> | |
| <input | |
| type="text" | |
| value={inputValue} | |
| onChange={(e) => setInputValue(e.target.value)} | |
| placeholder={isConnected ? "Type your message..." : "Waiting for connection..."} | |
| disabled={!isConnected || isLoading} | |
| className="message-input" | |
| /> | |
| <button | |
| type="submit" | |
| disabled={!isConnected || isLoading || !inputValue.trim()} | |
| className="send-button" | |
| > | |
| {isLoading ? "⏳" : "Send"} | |
| </button> | |
| </div> | |
| {/* Action buttons */} | |
| <div className="action-buttons"> | |
| <button | |
| type="button" | |
| onClick={handleClearHistory} | |
| disabled={messages.length === 0} | |
| className="clear-button" | |
| > | |
| Clear History | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| ); | |
| }; | |
| export default ChatWindow; | |