diff --git a/example/src/components/ConversationManager.tsx b/example/src/components/ConversationManager.tsx index 4ce8531..7155c07 100644 --- a/example/src/components/ConversationManager.tsx +++ b/example/src/components/ConversationManager.tsx @@ -39,6 +39,44 @@ export const ConversationManager: React.FC = () => { } }, [settings.agentServerUrl, settings.agentServerApiKey]); + // Cleanup WebSocket connections on unmount + useEffect(() => { + return () => { + if (selectedConversation?.remoteConversation) { + selectedConversation.remoteConversation.stopWebSocketClient().catch(err => { + console.warn('Failed to stop WebSocket client on unmount:', err); + }); + } + }; + }, [selectedConversation?.remoteConversation]); + + // Periodic status refresh for running conversations + useEffect(() => { + if (!selectedConversation?.remoteConversation) return; + + const refreshStatus = async () => { + try { + const status = await selectedConversation.remoteConversation!.state.getAgentStatus(); + setConversations(prev => prev.map(conv => + conv.id === selectedConversation.id + ? { ...conv, agentStatus: status } + : conv + )); + } catch (err) { + console.warn('Failed to refresh agent status:', err); + } + }; + + // Refresh every 2 seconds if the agent is running + const interval = setInterval(() => { + if (selectedConversation.agentStatus === 'running') { + refreshStatus(); + } + }, 2000); + + return () => clearInterval(interval); + }, [selectedConversation?.id, selectedConversation?.agentStatus]); + const loadConversations = async (conversationManager?: SDKConversationManager) => { const mgr = conversationManager || manager; if (!mgr) return; @@ -84,13 +122,46 @@ export const ConversationManager: React.FC = () => { } }; - const conversation = await manager.createConversation(agent, { - initialMessage: 'Hello! I\'m ready to help you with your tasks.', - maxIterations: 50, - }); + const conversation = await RemoteConversation.create( + manager.host, + agent, + { + apiKey: manager.apiKey, + initialMessage: 'Hello! I\'m ready to help you with your tasks.', + maxIterations: 50, + callback: (event: Event) => { + console.log('Received WebSocket event for new conversation:', event); + + // Update the conversation's events in real-time + setConversations(prev => prev.map(conv => { + if (conv.id === conversation.id && conv.events) { + const updatedEvents = [...conv.events, event]; + return { ...conv, events: updatedEvents }; + } + return conv; + })); + }, + } + ); console.log('Created conversation:', conversation); + // Start WebSocket client for real-time updates + try { + await conversation.startWebSocketClient(); + console.log('WebSocket client started for new conversation'); + } catch (wsErr) { + console.warn('Failed to start WebSocket client for new conversation:', wsErr); + } + + // Run the initial message to start the conversation + try { + await conversation.run(); + console.log('Started conversation with initial message'); + } catch (runErr) { + console.warn('Failed to run initial message:', runErr); + } + // Reload conversations to show the new one await loadConversations(); } catch (err) { @@ -127,14 +198,72 @@ export const ConversationManager: React.FC = () => { const selectConversation = async (conversationId: string) => { if (!manager) return; + // Clean up previous conversation's WebSocket if any + if (selectedConversation?.remoteConversation) { + try { + await selectedConversation.remoteConversation.stopWebSocketClient(); + console.log('Stopped WebSocket client for previous conversation'); + } catch (err) { + console.warn('Failed to stop previous WebSocket client:', err); + } + } + console.log('Selecting conversation:', conversationId); setSelectedConversationId(conversationId); // Load conversation details try { - const remoteConversation = await manager.loadConversation(conversationId); + // Create a callback to handle real-time events + const eventCallback = (event: Event) => { + console.log('Received WebSocket event:', event); + + // Update the conversation's events in real-time + setConversations(prev => prev.map(conv => { + if (conv.id === conversationId && conv.events) { + // Add the new event to the existing events + const updatedEvents = [...conv.events, event]; + return { ...conv, events: updatedEvents }; + } + return conv; + })); + + // If it's a status change event, update the agent status + if (event.type === 'agent_status_change' || event.type === 'agent_state_changed') { + // Refresh agent status after a short delay to ensure the server has updated + setTimeout(() => { + if (remoteConversation) { + remoteConversation.state.getAgentStatus().then(status => { + setConversations(prev => prev.map(conv => + conv.id === conversationId + ? { ...conv, agentStatus: status } + : conv + )); + }).catch(err => console.warn('Failed to update agent status:', err)); + } + }, 100); + } + }; + + // Load conversation with callback + const remoteConversation = await RemoteConversation.load( + manager.host, + conversationId, + { + apiKey: manager.apiKey, + callback: eventCallback, + } + ); console.log('Loaded remote conversation:', remoteConversation); + // Start WebSocket client for real-time updates + try { + await remoteConversation.startWebSocketClient(); + console.log('WebSocket client started for conversation:', conversationId); + } catch (wsErr) { + console.warn('Failed to start WebSocket client:', wsErr); + // Don't fail the whole operation if WebSocket fails + } + // Get events const events = await remoteConversation.state.events.getEvents(); console.log('Loaded events:', events); @@ -160,14 +289,19 @@ export const ConversationManager: React.FC = () => { if (!selectedConversation?.remoteConversation || !messageInput.trim()) return; try { + // Send the message await selectedConversation.remoteConversation.sendMessage(messageInput); setMessageInput(''); - // Reload conversation details to show new events - await selectConversation(selectedConversation.id); + // Start the agent to process the message (non-blocking) + await selectedConversation.remoteConversation.run(); + console.log('Agent started to process the message'); + + // The WebSocket will receive events as the agent works + // No need to reload immediately - events will come via WebSocket } catch (err) { - console.error('Failed to send message:', err); - setError(err instanceof Error ? err.message : 'Failed to send message'); + console.error('Failed to send message or start agent:', err); + setError(err instanceof Error ? err.message : 'Failed to send message or start agent'); } }; @@ -185,10 +319,23 @@ export const ConversationManager: React.FC = () => { case 'running': return 'text-green-500'; case 'stopped': return 'text-red-500'; case 'paused': return 'text-orange-500'; + case 'finished': return 'text-blue-500'; + case 'error': return 'text-red-600'; default: return 'text-gray-500'; } }; + const getStatusIcon = (status?: string) => { + switch (status) { + case 'running': return '🔄'; + case 'stopped': return 'âšī¸'; + case 'paused': return 'â¸ī¸'; + case 'finished': return '✅'; + case 'error': return '❌'; + default: return '❓'; + } + }; + return (
@@ -252,7 +399,7 @@ export const ConversationManager: React.FC = () => {
Status: - ● {conversation.status || 'unknown'} + {getStatusIcon(conversation.status)} {conversation.status || 'unknown'}
@@ -292,7 +439,7 @@ export const ConversationManager: React.FC = () => {
Status: - ● {selectedConversation.status || 'unknown'} + {getStatusIcon(selectedConversation.status)} {selectedConversation.status || 'unknown'}
@@ -366,13 +513,55 @@ export const ConversationManager: React.FC = () => { className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-600 focus:shadow-md transition-all duration-200 resize-vertical" rows={3} /> - +
+ + + +
diff --git a/src/events/websocket-client.ts b/src/events/websocket-client.ts index 80f1f5d..5be2360 100644 --- a/src/events/websocket-client.ts +++ b/src/events/websocket-client.ts @@ -2,9 +2,27 @@ * WebSocket client for real-time event streaming */ -import WebSocket from 'ws'; import { Event, ConversationCallbackType } from '../types/base'; +// Use native WebSocket in browser, ws library in Node.js +let WebSocketImpl: any; + +if (typeof window !== 'undefined' && window.WebSocket) { + // Browser environment + WebSocketImpl = window.WebSocket; +} else { + // Node.js environment + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const ws = require('ws'); + WebSocketImpl = ws; + } catch (e) { + throw new Error( + 'WebSocket implementation not available. Install ws package for Node.js environments.' + ); + } +} + export interface WebSocketClientOptions { host: string; conversationId: string; @@ -17,7 +35,7 @@ export class WebSocketCallbackClient { private conversationId: string; private callback: ConversationCallbackType; private apiKey?: string; - private ws?: WebSocket; + private ws?: any; // WebSocket instance (browser or Node.js) private reconnectDelay = 1000; private maxReconnectDelay = 30000; private currentDelay = 1000; @@ -64,38 +82,74 @@ export class WebSocketCallbackClient { // Add API key as query parameter if provided const finalUrl = this.apiKey ? `${wsUrl}?session_api_key=${this.apiKey}` : wsUrl; - this.ws = new WebSocket(finalUrl); - - this.ws.on('open', () => { - console.debug(`WebSocket connected to ${finalUrl}`); - this.currentDelay = this.reconnectDelay; // Reset delay on successful connection - }); - - this.ws.on('message', (data: WebSocket.Data) => { - try { - const message = data.toString(); - const event: Event = JSON.parse(message); - this.callback(event); - } catch (error) { - console.error('Error processing WebSocket message:', error); - } - }); - - this.ws.on('close', (code: number, reason: Buffer) => { - console.debug(`WebSocket closed: ${code} ${reason.toString()}`); - this.ws = undefined; - - if (this.shouldReconnect) { - this.scheduleReconnect(); - } - }); - - this.ws.on('error', (error: Error) => { - console.debug('WebSocket error:', error); - if (this.shouldReconnect) { - this.scheduleReconnect(); - } - }); + this.ws = new WebSocketImpl(finalUrl); + + // Handle events differently for browser vs Node.js + if (typeof window !== 'undefined') { + // Browser WebSocket API + this.ws.onopen = () => { + console.debug(`WebSocket connected to ${finalUrl}`); + this.currentDelay = this.reconnectDelay; // Reset delay on successful connection + }; + + this.ws.onmessage = (event: MessageEvent) => { + try { + const message = event.data; + const eventData: Event = JSON.parse(message); + this.callback(eventData); + } catch (error) { + console.error('Error processing WebSocket message:', error); + } + }; + + this.ws.onclose = (event: CloseEvent) => { + console.debug(`WebSocket closed: ${event.code} ${event.reason}`); + this.ws = undefined; + + if (this.shouldReconnect) { + this.scheduleReconnect(); + } + }; + + this.ws.onerror = (error: Event) => { + console.debug('WebSocket error:', error); + if (this.shouldReconnect) { + this.scheduleReconnect(); + } + }; + } else { + // Node.js ws library API + this.ws.on('open', () => { + console.debug(`WebSocket connected to ${finalUrl}`); + this.currentDelay = this.reconnectDelay; // Reset delay on successful connection + }); + + this.ws.on('message', (data: any) => { + try { + const message = data.toString(); + const event: Event = JSON.parse(message); + this.callback(event); + } catch (error) { + console.error('Error processing WebSocket message:', error); + } + }); + + this.ws.on('close', (code: number, reason: any) => { + console.debug(`WebSocket closed: ${code} ${reason ? reason.toString() : ''}`); + this.ws = undefined; + + if (this.shouldReconnect) { + this.scheduleReconnect(); + } + }); + + this.ws.on('error', (error: Error) => { + console.debug('WebSocket error:', error); + if (this.shouldReconnect) { + this.scheduleReconnect(); + } + }); + } } catch (error) { console.error('Failed to create WebSocket connection:', error); if (this.shouldReconnect) {