Message Handling

Learn how to handle message events, track conversations, and integrate with analytics or other systems.

The onMessagesChange Callback

InAppAI React uses controlled mode, which means you manage the message state. The onMessagesChange callback is called whenever the messages array changes.

import { useState } from 'react';
import { InAppAI, Message } from '@inappai/react';

function App() {
  const [messages, setMessages] = useState<Message[]>([]);

  return (
    <InAppAI
      agentId="your-agent-id"
      messages={messages}
      onMessagesChange={(newMessages) => {
        console.log('Messages updated:', newMessages);
        setMessages(newMessages);
      }}
    />
  );
}

Tracking Message Events

Since you control the messages array, you can detect when messages are sent or received by comparing the old and new arrays:

function ChatWithTracking() {
  const [messages, setMessages] = useState<Message[]>([]);

  const handleMessagesChange = (newMessages: Message[]) => {
    // Detect new messages by comparing lengths
    if (newMessages.length > messages.length) {
      const newMessage = newMessages[newMessages.length - 1];

      if (newMessage.role === 'user') {
        console.log('User sent:', newMessage.content);
        // Track user message
        analytics.track('User Message', {
          length: newMessage.content.length,
        });
      } else if (newMessage.role === 'assistant') {
        console.log('AI responded:', newMessage.content);
        // Track AI response
        analytics.track('AI Response', {
          length: newMessage.content.length,
        });
      }
    }

    setMessages(newMessages);
  };

  return (
    <InAppAI
      agentId="your-agent-id"
      messages={messages}
      onMessagesChange={handleMessagesChange}
    />
  );
}

Common Patterns

Save to localStorage

const handleMessagesChange = (newMessages: Message[]) => {
  setMessages(newMessages);
  localStorage.setItem('messages', JSON.stringify(newMessages));
};

Save to Backend

const handleMessagesChange = async (newMessages: Message[]) => {
  setMessages(newMessages);

  // Debounce or save only on new messages
  if (newMessages.length > messages.length) {
    await api.saveMessages(conversationId, newMessages);
  }
};

Auto-Generate Conversation Title

const handleMessagesChange = (newMessages: Message[]) => {
  setMessages(newMessages);

  // Auto-generate title from first user message
  if (newMessages.length === 1 && newMessages[0].role === 'user') {
    const title = newMessages[0].content.slice(0, 50);
    setConversationTitle(title);
  }
};

Track Response Time

function ChatWithResponseTime() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [lastUserMessageTime, setLastUserMessageTime] = useState<number>(0);

  const handleMessagesChange = (newMessages: Message[]) => {
    if (newMessages.length > messages.length) {
      const newMessage = newMessages[newMessages.length - 1];

      if (newMessage.role === 'user') {
        setLastUserMessageTime(Date.now());
      } else if (newMessage.role === 'assistant' && lastUserMessageTime > 0) {
        const responseTime = Date.now() - lastUserMessageTime;
        console.log('Response time:', responseTime, 'ms');
        analytics.track('AI Response Time', { ms: responseTime });
      }
    }

    setMessages(newMessages);
  };

  return (
    <InAppAI
      agentId="your-agent-id"
      messages={messages}
      onMessagesChange={handleMessagesChange}
    />
  );
}

Analytics Integration

Complete Analytics Example

import { useState, useRef } from 'react';
import { InAppAI, Message } from '@inappai/react';

function AnalyticsChat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [stats, setStats] = useState({
    messagesSent: 0,
    messagesReceived: 0,
    avgResponseTime: 0,
  });
  const lastUserMessageTime = useRef<number>(0);

  const handleMessagesChange = (newMessages: Message[]) => {
    // Detect new message
    if (newMessages.length > messages.length) {
      const newMessage = newMessages[newMessages.length - 1];

      if (newMessage.role === 'user') {
        // Track user message
        setStats(prev => ({
          ...prev,
          messagesSent: prev.messagesSent + 1,
        }));
        lastUserMessageTime.current = Date.now();

        analytics.track('User Message Sent', {
          length: newMessage.content.length,
          conversationLength: newMessages.length,
        });
      } else if (newMessage.role === 'assistant') {
        // Track AI response
        const responseTime = Date.now() - lastUserMessageTime.current;

        setStats(prev => ({
          ...prev,
          messagesReceived: prev.messagesReceived + 1,
          avgResponseTime: Math.round(
            (prev.avgResponseTime * prev.messagesReceived + responseTime) /
            (prev.messagesReceived + 1)
          ),
        }));

        analytics.track('AI Response Received', {
          length: newMessage.content.length,
          responseTime,
        });
      }
    }

    setMessages(newMessages);
  };

  return (
    <div>
      {/* Stats Dashboard */}
      <div className="stats">
        <div>Messages Sent: {stats.messagesSent}</div>
        <div>AI Responses: {stats.messagesReceived}</div>
        <div>Avg Response Time: {stats.avgResponseTime}ms</div>
      </div>

      <InAppAI
        agentId="your-agent-id"
        messages={messages}
        onMessagesChange={handleMessagesChange}
      />
    </div>
  );
}

Error Tracking

Since errors may occur during message handling, wrap your handler in try-catch:

const handleMessagesChange = async (newMessages: Message[]) => {
  try {
    setMessages(newMessages);
    await api.saveMessages(conversationId, newMessages);
  } catch (error) {
    console.error('Failed to save messages:', error);
    Sentry.captureException(error);
    // Show user-friendly error message
    setError('Failed to save your conversation');
  }
};

Best Practices

1. Don’t Modify Messages in Handler

// Bad - can cause infinite loops
onMessagesChange={(newMessages) => {
  setMessages([...newMessages, extraMessage]); // Triggers onMessagesChange again!
}}

// Good - just update state
onMessagesChange={(newMessages) => {
  setMessages(newMessages);
}}

2. Debounce Backend Saves

import { useCallback } from 'react';
import debounce from 'lodash/debounce';

const saveToBackend = useCallback(
  debounce((messages: Message[]) => {
    api.saveMessages(conversationId, messages);
  }, 1000),
  [conversationId]
);

const handleMessagesChange = (newMessages: Message[]) => {
  setMessages(newMessages);
  saveToBackend(newMessages);
};

3. Use Refs for Timing Data

// Use refs for data that doesn't need to trigger re-renders
const lastMessageTime = useRef<number>(0);

const handleMessagesChange = (newMessages: Message[]) => {
  if (newMessage.role === 'user') {
    lastMessageTime.current = Date.now(); // No re-render needed
  }
  setMessages(newMessages);
};

4. Handle Async Operations Properly

const handleMessagesChange = async (newMessages: Message[]) => {
  // Update state immediately for responsive UI
  setMessages(newMessages);

  // Then handle async operations
  try {
    await api.saveMessages(conversationId, newMessages);
  } catch (error) {
    console.error('Save failed:', error);
    // Don't throw - the chat should continue working
  }
};

Next Steps