Skip to main content

Chat Implementation

This guide walks you through implementing a complete text chat interface with BlackBox agents using WebSockets. You’ll learn how to establish connections, send messages, receive responses, and handle the full conversation lifecycle.

Prerequisites

Before starting, ensure you have:
  • Web Integration Token: Generated from your agent’s web integration settings
  • Agent Configured: Agent created and enabled with chat capabilities
  • Development Environment: Node.js, browser, or your preferred runtime
Quick Setup: If you haven’t created a web integration yet, see Web Widget Embedding for instructions on creating integrations and generating tokens.

Step 1: Install Dependencies

You only need a WebSocket client library:
  • Browser: Native WebSocket API (no installation needed)
  • Node.js: ws package (npm install ws)

Step 2: Create Connection

class ChatConnection {
  private ws: WebSocket | null = null;
  private messageHandlers: Map<string, Function> = new Map();
  
  constructor(
    private serverUrl: string,
    private token: string
  ) {}
  
  connect() {
    const wsUrl = `wss://${this.serverUrl.replace(/^https?:\/\//, '')}/api/v1/ws/webCall?token=${encodeURIComponent(this.token)}`;
    
    this.ws = new WebSocket(wsUrl);
    
    this.ws.onopen = () => {
      // Send initialization message
      this.send({
        type: 'initialize',
        timestamp: new Date().toISOString(),
        request: {
          callType: 'chat',
          additionalData: {}
        }
      });
    };
    
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };
    
    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    this.ws.onclose = () => {
      console.log('WebSocket closed');
    };
  }
  
  private handleMessage(message: any) {
    switch (message.type) {
      case 'event':
        if (message.name === 'connection') {
          console.log('Connection established');
        }
        break;
        
      case 'text':
        const text = message.content?.text;
        if (text && message.content?.source === 'assistant') {
          // Agent message received
          this.onMessage?.(text);
        }
        break;
        
      case 'error':
        console.error('Error:', message);
        break;
        
      case 'conversationResult':
        console.log('Conversation ended:', message.result);
        break;
    }
  }
  
  sendMessage(text: string) {
    this.send({
      type: 'incomingChatMessage',
      content: text,
      timestamp: new Date().toISOString()
    });
  }
  
  private send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }
  
  disconnect() {
    this.send({
      type: 'terminate',
      timestamp: new Date().toISOString()
    });
    this.ws?.close();
  }
  
  onMessage?: (text: string) => void;
}

Step 3: Send Messages

// Send message
chatConnection.sendMessage('Hello, agent!');

// Or directly via WebSocket
ws.send(JSON.stringify({
  type: 'incomingChatMessage',
  content: 'Hello, agent!',
  timestamp: new Date().toISOString()
}));

Step 4: Receive Messages

Message Types

Chat conversations receive several message types: Text Messages (text type):
{
  type: 'text',
  timestamp: '2025-01-20T10:00:00Z',
  content: {
    source: 'assistant' | 'user',
    text: 'Message content',
    segmentId?: string,
    type?: 'potential' | 'final' | 'confident'
  }
}
Events (event type):
{
  type: 'event',
  name: 'connection' | 'opened' | 'closed',
  timestamp: '2025-01-20T10:00:00Z'
}
Conversation Result (conversationResult type):
{
  type: 'conversationResult',
  result: {
    // Conversation summary and metadata
  },
  timestamp: '2025-01-20T10:00:00Z'
}

Handling Messages

// Handle incoming WebSocket messages
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  switch (message.type) {
    case 'text':
      const content = message.content;
      if (content.source === 'assistant') {
        // Agent message
        displayAgentMessage(content.text);
        addMessageToUI(content.text || '', 'agent');
      } else if (content.source === 'user') {
        // User message (echo of what you sent)
        displayUserMessage(content.text);
      }
      break;
      
    case 'toolCall':
      // Agent is calling a tool
      displayToolCall(message);
      break;
      
    case 'toolCallResult':
      // Tool execution result
      displayToolResult(message);
      break;
      
    case 'event':
      // Handle connection events
      if (message.name === 'connection') {
        console.log('Session ready');
      }
      break;
  }
};

Step 5: Complete Chat Example

Here’s a complete, production-ready chat implementation:
import React, { useState, useEffect, useRef } from 'react';

interface Message {
  id: string;
  text: string;
  sender: 'user' | 'agent';
  timestamp: Date;
}

export function ChatWidget() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isConnected, setIsConnected] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Connect to WebSocket
    const ws = new WebSocket(
      'wss://blackbox.dasha.ai/api/v1/ws/webCall?token=YOUR_WEB_INTEGRATION_TOKEN'
    );
    wsRef.current = ws;

    ws.onopen = () => {
      setIsConnected(true);
      // Send initialization message
      ws.send(JSON.stringify({
        type: 'initialize',
        timestamp: new Date().toISOString(),
        request: {
          callType: 'chat',
          additionalData: {}
        }
      }));
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      if (message.type === 'text' && message.content.source === 'assistant') {
        addMessage(message.content.text || '', 'agent');
      } else if (message.type === 'event' && message.name === 'connection') {
        setIsConnected(true);
      } else if (message.type === 'error') {
        console.error('Chat error:', message);
        addMessage('Sorry, an error occurred. Please try again.', 'agent');
      }
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      setIsConnected(false);
    };

    ws.onclose = () => {
      setIsConnected(false);
    };

    return () => {
      if (wsRef.current) {
        wsRef.current.close();
      }
    };
  }, []);

  const addMessage = (text: string, sender: 'user' | 'agent') => {
    setMessages(prev => [...prev, {
      id: `${Date.now()}-${Math.random()}`,
      text,
      sender,
      timestamp: new Date()
    }]);
  };

  const sendMessage = () => {
    if (!input.trim() || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;

    const text = input.trim();
    addMessage(text, 'user');
    
    // Send message via WebSocket
    wsRef.current.send(JSON.stringify({
      type: 'incomingChatMessage',
      content: text,
      timestamp: new Date().toISOString()
    }));
    
    setInput('');
  };

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <div className="chat-widget">
      <div className="chat-header">
        <h3>Chat with Agent</h3>
        <div className={`status ${isConnected ? 'connected' : 'disconnected'}`}>
          {isConnected ? '● Connected' : '○ Disconnected'}
        </div>
      </div>

      <div className="chat-messages">
        {messages.map(msg => (
          <div key={msg.id} className={`message ${msg.sender}`}>
            <div className="message-content">{msg.text}</div>
            <div className="message-time">
              {msg.timestamp.toLocaleTimeString()}
            </div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <div className="chat-input">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault();
              sendMessage();
            }
          }}
          placeholder="Type a message..."
          disabled={!isConnected}
        />
        <button onClick={sendMessage} disabled={!isConnected || !input.trim()}>
          Send
        </button>
      </div>
    </div>
  );
}

Step 6: Handle Connection States

Monitor and handle connection states for better UX:
let connectionStatus: 'connecting' | 'open' | 'closed' | 'error' = 'closed';

const ws = new WebSocket('wss://blackbox.dasha.ai/api/v1/ws/webCall?token=YOUR_WEB_INTEGRATION_TOKEN');

ws.onopen = () => {
  connectionStatus = 'open';
  showStatus('Connected');
  enableInput();
  hideRetryButton();
};

ws.onclose = () => {
  connectionStatus = 'closed';
  showStatus('Disconnected');
  disableInput();
  showRetryButton();
};

ws.onerror = () => {
  connectionStatus = 'error';
  showStatus('Connection error');
  disableInput();
  showRetryButton();
};

// Track connecting state
connectionStatus = 'connecting';
showStatus('Connecting...');
disableInput();

Step 7: Handle Errors

Implement robust error handling:
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.type === 'error') {
    // Parse error message
    const errorMessage = message.data?.message || message.message || 'An unexpected error occurred';
    console.error('Error details:', message);
    
    // Show user-friendly error message
    showErrorMessage(errorMessage);
    
    // Optionally retry connection
    if (shouldRetry(message)) {
      setTimeout(() => {
        ws.close();
        connect(); // Reconnect function
      }, 3000);
    }
  }
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
  showErrorMessage('Connection error occurred');
};

Step 8: Graceful Disconnection

Properly terminate conversations:
let conversationResult: any = null;

// Listen for conversation result
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  if (message.type === 'conversationResult') {
    conversationResult = message.result;
  }
};

// Option 1: Immediate disconnect
ws.close();

// Option 2: Graceful end (wait for conversation result)
function gracefulEnd(timeoutMs: number = 3000): Promise<'result' | 'timeout' | 'error'> {
  return new Promise((resolve) => {
    // Send terminate message
    ws.send(JSON.stringify({
      type: 'terminate',
      timestamp: new Date().toISOString()
    }));
    
    // Wait for conversation result
    const timeout = setTimeout(() => {
      resolve('timeout');
      ws.close();
    }, timeoutMs);
    
    const checkResult = () => {
      if (conversationResult) {
        clearTimeout(timeout);
        resolve('result');
        ws.close();
      }
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.type === 'conversationResult') {
        conversationResult = message.result;
        checkResult();
      }
    };
  });
}

// Usage
const result = await gracefulEnd(3000);
if (result === 'result') {
  console.log('Conversation completed:', conversationResult);
} else if (result === 'timeout') {
  console.log('Timeout waiting for result');
} else {
  console.log('Error during graceful end');
}

Advanced Features

Message History

Maintain and access full conversation history:
const messageHistory: any[] = [];

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  messageHistory.push(message);
  
  // Filter by type
  const textMessages = messageHistory.filter(msg => msg.type === 'text');
  const toolCalls = messageHistory.filter(msg => msg.type === 'toolCall');
  
  // Access conversation result
  const result = messageHistory.find(msg => msg.type === 'conversationResult')?.result;
};

Streaming Responses

Handle streaming/partial messages:
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.type === 'text') {
    const content = message.content;
    
    // Handle partial messages
    if (content.type === 'potential') {
      // Agent is still thinking (streaming)
      updatePartialMessage(content.text);
    } else if (content.type === 'final' || content.type === 'confident') {
      // Final message
      finalizeMessage(content.text);
    }
  }
};

Custom Data

Pass custom context to agent:
ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'initialize',
    timestamp: new Date().toISOString(),
    request: {
      callType: 'chat',
      additionalData: {
        userId: 'user123',
        productId: 'prod456',
        sessionData: {
          page: '/products',
          referrer: 'google.com'
        }
      }
    }
  }));
};

Troubleshooting

Connection Fails

Symptoms: Connection status stays at connecting or goes to error Solutions:
  1. Verify token is valid and not expired
  2. Check server URL is correct
  3. Ensure HTTPS/WSS is used (not HTTP/WS)
  4. Check network/firewall allows WebSocket connections
  5. Verify agent is enabled and configured

Messages Not Received

Symptoms: Messages sent but no response from agent Solutions:
  1. Check WebSocket message handler is registered
  2. Verify WebSocket readyState is OPEN before sending
  3. Check browser console for WebSocket errors
  4. Ensure agent has valid LLM configuration
  5. Verify AllowWebChat feature is enabled

Connection Drops

Symptoms: Connection closes unexpectedly Solutions:
  1. Implement reconnection logic
  2. Check for network interruptions
  3. Monitor WebSocket connection state (readyState)
  4. Handle WebSocket onclose and onerror events
  5. Send terminate message before closing connection

Next Steps