Skip to content

Commit e76ac48

Browse files
authored
Merge pull request #14 from All-Hands-AI/fix-conversation-run-flow
Fix conversation run flow in example app
2 parents 95ee3a1 + 07591df commit e76ac48

File tree

2 files changed

+295
-52
lines changed

2 files changed

+295
-52
lines changed

example/src/components/ConversationManager.tsx

Lines changed: 207 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,44 @@ export const ConversationManager: React.FC = () => {
3939
}
4040
}, [settings.agentServerUrl, settings.agentServerApiKey]);
4141

42+
// Cleanup WebSocket connections on unmount
43+
useEffect(() => {
44+
return () => {
45+
if (selectedConversation?.remoteConversation) {
46+
selectedConversation.remoteConversation.stopWebSocketClient().catch(err => {
47+
console.warn('Failed to stop WebSocket client on unmount:', err);
48+
});
49+
}
50+
};
51+
}, [selectedConversation?.remoteConversation]);
52+
53+
// Periodic status refresh for running conversations
54+
useEffect(() => {
55+
if (!selectedConversation?.remoteConversation) return;
56+
57+
const refreshStatus = async () => {
58+
try {
59+
const status = await selectedConversation.remoteConversation!.state.getAgentStatus();
60+
setConversations(prev => prev.map(conv =>
61+
conv.id === selectedConversation.id
62+
? { ...conv, agentStatus: status }
63+
: conv
64+
));
65+
} catch (err) {
66+
console.warn('Failed to refresh agent status:', err);
67+
}
68+
};
69+
70+
// Refresh every 2 seconds if the agent is running
71+
const interval = setInterval(() => {
72+
if (selectedConversation.agentStatus === 'running') {
73+
refreshStatus();
74+
}
75+
}, 2000);
76+
77+
return () => clearInterval(interval);
78+
}, [selectedConversation?.id, selectedConversation?.agentStatus]);
79+
4280
const loadConversations = async (conversationManager?: SDKConversationManager) => {
4381
const mgr = conversationManager || manager;
4482
if (!mgr) return;
@@ -84,13 +122,46 @@ export const ConversationManager: React.FC = () => {
84122
}
85123
};
86124

87-
const conversation = await manager.createConversation(agent, {
88-
initialMessage: 'Hello! I\'m ready to help you with your tasks.',
89-
maxIterations: 50,
90-
});
125+
const conversation = await RemoteConversation.create(
126+
manager.host,
127+
agent,
128+
{
129+
apiKey: manager.apiKey,
130+
initialMessage: 'Hello! I\'m ready to help you with your tasks.',
131+
maxIterations: 50,
132+
callback: (event: Event) => {
133+
console.log('Received WebSocket event for new conversation:', event);
134+
135+
// Update the conversation's events in real-time
136+
setConversations(prev => prev.map(conv => {
137+
if (conv.id === conversation.id && conv.events) {
138+
const updatedEvents = [...conv.events, event];
139+
return { ...conv, events: updatedEvents };
140+
}
141+
return conv;
142+
}));
143+
},
144+
}
145+
);
91146

92147
console.log('Created conversation:', conversation);
93148

149+
// Start WebSocket client for real-time updates
150+
try {
151+
await conversation.startWebSocketClient();
152+
console.log('WebSocket client started for new conversation');
153+
} catch (wsErr) {
154+
console.warn('Failed to start WebSocket client for new conversation:', wsErr);
155+
}
156+
157+
// Run the initial message to start the conversation
158+
try {
159+
await conversation.run();
160+
console.log('Started conversation with initial message');
161+
} catch (runErr) {
162+
console.warn('Failed to run initial message:', runErr);
163+
}
164+
94165
// Reload conversations to show the new one
95166
await loadConversations();
96167
} catch (err) {
@@ -127,14 +198,72 @@ export const ConversationManager: React.FC = () => {
127198
const selectConversation = async (conversationId: string) => {
128199
if (!manager) return;
129200

201+
// Clean up previous conversation's WebSocket if any
202+
if (selectedConversation?.remoteConversation) {
203+
try {
204+
await selectedConversation.remoteConversation.stopWebSocketClient();
205+
console.log('Stopped WebSocket client for previous conversation');
206+
} catch (err) {
207+
console.warn('Failed to stop previous WebSocket client:', err);
208+
}
209+
}
210+
130211
console.log('Selecting conversation:', conversationId);
131212
setSelectedConversationId(conversationId);
132213

133214
// Load conversation details
134215
try {
135-
const remoteConversation = await manager.loadConversation(conversationId);
216+
// Create a callback to handle real-time events
217+
const eventCallback = (event: Event) => {
218+
console.log('Received WebSocket event:', event);
219+
220+
// Update the conversation's events in real-time
221+
setConversations(prev => prev.map(conv => {
222+
if (conv.id === conversationId && conv.events) {
223+
// Add the new event to the existing events
224+
const updatedEvents = [...conv.events, event];
225+
return { ...conv, events: updatedEvents };
226+
}
227+
return conv;
228+
}));
229+
230+
// If it's a status change event, update the agent status
231+
if (event.type === 'agent_status_change' || event.type === 'agent_state_changed') {
232+
// Refresh agent status after a short delay to ensure the server has updated
233+
setTimeout(() => {
234+
if (remoteConversation) {
235+
remoteConversation.state.getAgentStatus().then(status => {
236+
setConversations(prev => prev.map(conv =>
237+
conv.id === conversationId
238+
? { ...conv, agentStatus: status }
239+
: conv
240+
));
241+
}).catch(err => console.warn('Failed to update agent status:', err));
242+
}
243+
}, 100);
244+
}
245+
};
246+
247+
// Load conversation with callback
248+
const remoteConversation = await RemoteConversation.load(
249+
manager.host,
250+
conversationId,
251+
{
252+
apiKey: manager.apiKey,
253+
callback: eventCallback,
254+
}
255+
);
136256
console.log('Loaded remote conversation:', remoteConversation);
137257

258+
// Start WebSocket client for real-time updates
259+
try {
260+
await remoteConversation.startWebSocketClient();
261+
console.log('WebSocket client started for conversation:', conversationId);
262+
} catch (wsErr) {
263+
console.warn('Failed to start WebSocket client:', wsErr);
264+
// Don't fail the whole operation if WebSocket fails
265+
}
266+
138267
// Get events
139268
const events = await remoteConversation.state.events.getEvents();
140269
console.log('Loaded events:', events);
@@ -160,14 +289,19 @@ export const ConversationManager: React.FC = () => {
160289
if (!selectedConversation?.remoteConversation || !messageInput.trim()) return;
161290

162291
try {
292+
// Send the message
163293
await selectedConversation.remoteConversation.sendMessage(messageInput);
164294
setMessageInput('');
165295

166-
// Reload conversation details to show new events
167-
await selectConversation(selectedConversation.id);
296+
// Start the agent to process the message (non-blocking)
297+
await selectedConversation.remoteConversation.run();
298+
console.log('Agent started to process the message');
299+
300+
// The WebSocket will receive events as the agent works
301+
// No need to reload immediately - events will come via WebSocket
168302
} catch (err) {
169-
console.error('Failed to send message:', err);
170-
setError(err instanceof Error ? err.message : 'Failed to send message');
303+
console.error('Failed to send message or start agent:', err);
304+
setError(err instanceof Error ? err.message : 'Failed to send message or start agent');
171305
}
172306
};
173307

@@ -185,10 +319,23 @@ export const ConversationManager: React.FC = () => {
185319
case 'running': return 'text-green-500';
186320
case 'stopped': return 'text-red-500';
187321
case 'paused': return 'text-orange-500';
322+
case 'finished': return 'text-blue-500';
323+
case 'error': return 'text-red-600';
188324
default: return 'text-gray-500';
189325
}
190326
};
191327

328+
const getStatusIcon = (status?: string) => {
329+
switch (status) {
330+
case 'running': return '🔄';
331+
case 'stopped': return '⏹️';
332+
case 'paused': return '⏸️';
333+
case 'finished': return '✅';
334+
case 'error': return '❌';
335+
default: return '❓';
336+
}
337+
};
338+
192339
return (
193340
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 m-0">
194341
<div className="flex justify-between items-center mb-6 pb-4 border-b border-gray-200 dark:border-gray-700">
@@ -252,7 +399,7 @@ export const ConversationManager: React.FC = () => {
252399
<div className="flex items-center gap-2">
253400
<span className="text-gray-600 dark:text-gray-400">Status:</span>
254401
<span className={`font-medium ${getStatusColorClass(conversation.status)}`}>
255-
{conversation.status || 'unknown'}
402+
{getStatusIcon(conversation.status)} {conversation.status || 'unknown'}
256403
</span>
257404
</div>
258405
</div>
@@ -292,7 +439,7 @@ export const ConversationManager: React.FC = () => {
292439
<div className="flex justify-between">
293440
<span className="font-medium text-gray-900 dark:text-white">Status:</span>
294441
<span className={`font-medium ${getStatusColorClass(selectedConversation.status)}`}>
295-
{selectedConversation.status || 'unknown'}
442+
{getStatusIcon(selectedConversation.status)} {selectedConversation.status || 'unknown'}
296443
</span>
297444
</div>
298445
<div className="flex justify-between">
@@ -366,13 +513,55 @@ export const ConversationManager: React.FC = () => {
366513
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"
367514
rows={3}
368515
/>
369-
<button
370-
onClick={sendMessage}
371-
disabled={!messageInput.trim() || loading}
372-
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
373-
>
374-
Send Message
375-
</button>
516+
<div className="flex gap-2">
517+
<button
518+
onClick={sendMessage}
519+
disabled={!messageInput.trim() || loading}
520+
className="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
521+
>
522+
Send & Run
523+
</button>
524+
<button
525+
onClick={async () => {
526+
if (selectedConversation?.remoteConversation) {
527+
try {
528+
await selectedConversation.remoteConversation.run();
529+
console.log('Agent started manually');
530+
// Reload conversation details
531+
await selectConversation(selectedConversation.id);
532+
} catch (err) {
533+
console.error('Failed to start agent:', err);
534+
setError(err instanceof Error ? err.message : 'Failed to start agent');
535+
}
536+
}
537+
}}
538+
disabled={loading}
539+
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
540+
title="Start/Resume the agent"
541+
>
542+
▶️ Run
543+
</button>
544+
<button
545+
onClick={async () => {
546+
if (selectedConversation?.remoteConversation) {
547+
try {
548+
await selectedConversation.remoteConversation.pause();
549+
console.log('Agent paused manually');
550+
// Reload conversation details
551+
await selectConversation(selectedConversation.id);
552+
} catch (err) {
553+
console.error('Failed to pause agent:', err);
554+
setError(err instanceof Error ? err.message : 'Failed to pause agent');
555+
}
556+
}
557+
}}
558+
disabled={loading}
559+
className="bg-orange-600 hover:bg-orange-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
560+
title="Pause the agent"
561+
>
562+
⏸️ Pause
563+
</button>
564+
</div>
376565
</div>
377566
</div>
378567
</div>

0 commit comments

Comments
 (0)