diff --git a/example/src/App.tsx b/example/src/App.tsx index 135d7eb..d5240a0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,62 +1,19 @@ -import { useState, useEffect } from 'react' import './App.css' -// Import the OpenHands SDK -import { - RemoteConversation, - RemoteWorkspace, - HttpClient, - AgentExecutionStatus, - EventSortOrder -} from '@openhands/agent-server-typescript-client' - // Import settings components import { SettingsModal } from './components/SettingsModal' +import { ConversationManager } from './components/ConversationManager' import { useSettings } from './contexts/SettingsContext' function App() { - const [sdkStatus, setSdkStatus] = useState('Loading...') - const [sdkInfo, setSdkInfo] = useState(null) - // Use settings context const { settings, updateSettings, isModalOpen, openModal, closeModal, isFirstVisit } = useSettings() - useEffect(() => { - // Test that the SDK imports work correctly - try { - // Check that all main classes are available - const classes = { - RemoteConversation: typeof RemoteConversation, - RemoteWorkspace: typeof RemoteWorkspace, - HttpClient: typeof HttpClient, - AgentExecutionStatus: typeof AgentExecutionStatus, - EventSortOrder: typeof EventSortOrder, - } - - // Check that enums have expected values - const enumValues = { - AgentExecutionStatus: Object.keys(AgentExecutionStatus), - EventSortOrder: Object.keys(EventSortOrder), - } - - setSdkInfo({ - classes, - enumValues, - importTime: new Date().toISOString(), - }) - - setSdkStatus('✅ SDK imported successfully!') - } catch (error) { - setSdkStatus(`❌ SDK import failed: ${error}`) - console.error('SDK import error:', error) - } - }, []) - return (
-

OpenHands SDK Example

+

OpenHands Conversation Manager

@@ -68,59 +25,7 @@ function App() {
)} -
-

SDK Import Status

-

{sdkStatus}

- - {sdkInfo && ( -
-

Available Classes:

-
    - {Object.entries(sdkInfo.classes).map(([name, type]) => ( -
  • - {name}: {type as string} -
  • - ))} -
- -

Enum Values:

-
-
- AgentExecutionStatus: -
    - {sdkInfo.enumValues.AgentExecutionStatus.map((value: string) => ( -
  • {value}
  • - ))} -
-
-
- EventSortOrder: -
    - {sdkInfo.enumValues.EventSortOrder.map((value: string) => ( -
  • {value}
  • - ))} -
-
-
- -

- Imported at: {sdkInfo.importTime} -

-
- )} -
- -
-

Hello World from React + TypeScript + OpenHands SDK!

-

- This is a basic React application that successfully imports and uses the - OpenHands Agent Server TypeScript Client SDK. -

-

- The SDK is built locally and linked as a file dependency, demonstrating - that the build process works correctly. -

-
+ div { + margin: 2px 0; +} + +.events-section { + background-color: white; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 15px; +} + +.events-section h4 { + margin: 0 0 15px 0; + color: #1f2937; + font-size: 1rem; +} + +.no-events { + text-align: center; + color: #6b7280; + padding: 20px; + font-style: italic; +} + +.message-section { + background-color: white; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 15px; +} + +.message-section h4 { + margin: 0 0 10px 0; + color: #1f2937; + font-size: 1rem; +} + +.event-message { + margin-top: 8px; + padding: 8px; + background-color: #f3f4f6; + border-radius: 4px; + font-size: 13px; + line-height: 1.4; + color: #374151; +} + +/* Events and Messages Styles */ +.conversation-events { + background-color: white; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 15px; + margin-bottom: 20px; + max-height: 400px; + overflow-y: auto; +} + +.conversation-events h3 { + margin: 0 0 15px 0; + color: #1f2937; + font-size: 1rem; +} + +.events-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.event-item { + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 10px; + background-color: #f9fafb; +} + +.event-item.event-message { + border-left: 4px solid #3b82f6; +} + +.event-item.event-action { + border-left: 4px solid #f59e0b; +} + +.event-item.event-observation { + border-left: 4px solid #10b981; +} + +.event-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 12px; +} + +.event-type { + background-color: #e5e7eb; + padding: 2px 6px; + border-radius: 3px; + font-weight: 500; + text-transform: uppercase; +} + +.event-timestamp { + color: #6b7280; + font-size: 11px; +} + +.event-content { + font-size: 13px; + line-height: 1.4; + color: #374151; +} + +.message-content { + margin-top: 5px; + padding: 8px; + background-color: white; + border-radius: 3px; + border: 1px solid #e5e7eb; +} + +.action-args { + margin-top: 8px; +} + +.action-args pre { + background-color: #f3f4f6; + padding: 8px; + border-radius: 3px; + font-size: 11px; + overflow-x: auto; + margin: 4px 0 0 0; +} + +.observation-content { + margin-top: 5px; + padding: 8px; + background-color: white; + border-radius: 3px; + border: 1px solid #e5e7eb; + white-space: pre-wrap; +} + +/* Responsive design */ +@media (max-width: 768px) { + .conversation-content { + grid-template-columns: 1fr; + } + + .conversation-header { + flex-direction: column; + gap: 15px; + align-items: stretch; + } + + .header-actions { + justify-content: center; + } +} \ No newline at end of file diff --git a/example/src/components/ConversationManager.tsx b/example/src/components/ConversationManager.tsx new file mode 100644 index 0000000..dbc442d --- /dev/null +++ b/example/src/components/ConversationManager.tsx @@ -0,0 +1,372 @@ +import React, { useState, useEffect } from 'react'; +import { + ConversationManager as SDKConversationManager, + ConversationInfo, + RemoteConversation, + AgentBase, + AgentExecutionStatus, + Event +} from '@openhands/agent-server-typescript-client'; +import { useSettings } from '../contexts/SettingsContext'; +import './ConversationManager.css'; + +interface ConversationData extends ConversationInfo { + remoteConversation?: RemoteConversation; + events?: Event[]; + agentStatus?: AgentExecutionStatus; +} + +export const ConversationManager: React.FC = () => { + const { settings } = useSettings(); + const [conversations, setConversations] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [manager, setManager] = useState(null); + const [selectedConversationId, setSelectedConversationId] = useState(null); + const [messageInput, setMessageInput] = useState(''); + + // Get selected conversation data + const selectedConversation = conversations.find(c => c.id === selectedConversationId); + + // Initialize conversation manager + useEffect(() => { + if (settings.agentServerUrl) { + const conversationManager = new SDKConversationManager({ + host: settings.agentServerUrl, + apiKey: settings.agentServerApiKey + }); + setManager(conversationManager); + loadConversations(conversationManager); + } + }, [settings.agentServerUrl, settings.agentServerApiKey]); + + const loadConversations = async (conversationManager?: SDKConversationManager) => { + const mgr = conversationManager || manager; + if (!mgr) return; + + setLoading(true); + setError(null); + try { + const conversationList = await mgr.getAllConversations(); + console.log('Loaded conversations:', conversationList); + + // Convert to our data structure + const conversationData: ConversationData[] = conversationList.map((conv: ConversationInfo) => ({ + ...conv, + // Ensure we have the basic properties + id: conv.id, + agent: conv.agent, + created_at: conv.created_at, + updated_at: conv.updated_at, + status: conv.status + })); + + setConversations(conversationData); + } catch (err) { + console.error('Failed to load conversations:', err); + setError(err instanceof Error ? err.message : 'Failed to load conversations'); + } finally { + setLoading(false); + } + }; + + const createConversation = async () => { + if (!manager) return; + + setLoading(true); + setError(null); + try { + // Create a simple agent configuration + const agent: AgentBase = { + name: 'CodeActAgent', + llm: { + model: 'gpt-4o-mini', + api_key: settings.apiKey || '', + base_url: 'https://api.openai.com/v1' + } + }; + + const conversation = await manager.createConversation(agent, { + initialMessage: 'Hello! I\'m ready to help you with your tasks.', + maxIterations: 50, + }); + + console.log('Created conversation:', conversation); + + // Reload conversations to show the new one + await loadConversations(); + } catch (err) { + console.error('Failed to create conversation:', err); + setError(err instanceof Error ? err.message : 'Failed to create conversation'); + } finally { + setLoading(false); + } + }; + + const deleteConversation = async (conversationId: string) => { + if (!manager) return; + + setLoading(true); + setError(null); + try { + await manager.deleteConversation(conversationId); + + // Clear selection if this conversation was selected + if (selectedConversationId === conversationId) { + setSelectedConversationId(null); + } + + // Reload conversations list + await loadConversations(); + } catch (err) { + console.error('Failed to delete conversation:', err); + setError(err instanceof Error ? err.message : 'Failed to delete conversation'); + } finally { + setLoading(false); + } + }; + + const selectConversation = async (conversationId: string) => { + if (!manager) return; + + console.log('Selecting conversation:', conversationId); + setSelectedConversationId(conversationId); + + // Load conversation details + try { + const remoteConversation = await manager.loadConversation(conversationId); + console.log('Loaded remote conversation:', remoteConversation); + + // Get events + const events = await remoteConversation.state.events.getEvents(); + console.log('Loaded events:', events); + + // Get agent status + const agentStatus = await remoteConversation.state.getAgentStatus(); + console.log('Agent status:', agentStatus); + + // Update the conversation in our state with additional details + setConversations(prev => prev.map(conv => + conv.id === conversationId + ? { ...conv, remoteConversation, events, agentStatus } + : conv + )); + + } catch (err) { + console.error('Failed to load conversation details:', err); + setError(err instanceof Error ? err.message : 'Failed to load conversation details'); + } + }; + + const sendMessage = async () => { + if (!selectedConversation?.remoteConversation || !messageInput.trim()) return; + + try { + await selectedConversation.remoteConversation.sendMessage(messageInput); + setMessageInput(''); + + // Reload conversation details to show new events + await selectConversation(selectedConversation.id); + } catch (err) { + console.error('Failed to send message:', err); + setError(err instanceof Error ? err.message : 'Failed to send message'); + } + }; + + const formatDate = (dateString?: string) => { + if (!dateString) return 'Unknown'; + return new Date(dateString).toLocaleString(); + }; + + const getAgentName = (agent: AgentBase) => { + return agent.name || 'Unknown Agent'; + }; + + const getStatusColor = (status?: string) => { + switch (status) { + case 'running': return '#4CAF50'; + case 'stopped': return '#f44336'; + case 'paused': return '#ff9800'; + default: return '#9e9e9e'; + } + }; + + return ( +
+
+

Conversation Manager

+
+ + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + +
+
+

Conversations ({conversations.length})

+ + {loading && conversations.length === 0 && ( +
Loading conversations...
+ )} + + {conversations.length === 0 && !loading ? ( +
+

No conversations yet. Create your first conversation!

+
+ ) : ( +
+ {conversations.map((conversation) => ( +
selectConversation(conversation.id)} + > +
+
+ ID: {conversation.id.substring(0, 8)}... +
+
+
Agent: {getAgentName(conversation.agent)}
+
Created: {formatDate(conversation.created_at)}
+
+ Status: + ● {conversation.status || 'unknown'} + +
+
+
+ +
+ ))} +
+ )} +
+ +
+

Conversation Details

+ + {!selectedConversation ? ( +
+

Select a conversation to view details

+
+ ) : ( +
+
+
+ ID: {selectedConversation.id} +
+
+ Status: + + ● {selectedConversation.status || 'unknown'} + +
+
+ Agent: {getAgentName(selectedConversation.agent)} +
+
+ Model: {selectedConversation.agent.llm?.model || 'Unknown'} +
+
+ Created: {formatDate(selectedConversation.created_at)} +
+
+ Updated: {formatDate(selectedConversation.updated_at)} +
+ {selectedConversation.agentStatus && ( +
+ Agent Status: {selectedConversation.agentStatus} +
+ )} +
+ Total Events: {selectedConversation.events?.length || 0} +
+
+ +
+

Events & Messages

+
+ {selectedConversation.events && selectedConversation.events.length > 0 ? ( + selectedConversation.events.map((event, index) => ( +
+
+ {event.type} + + {event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : ''} + +
+ {event.message && ( +
{event.message}
+ )} + {event.content && ( +
{JSON.stringify(event.content, null, 2)}
+ )} +
+ )) + ) : ( +
No events yet
+ )} +
+
+ +
+

Send Message

+
+