Using Core with React

The OpenCX Widget provides two approaches for React integration:

  1. Using the React package (@opencx/widget/react) for quick integration
  2. Using the core package directly for more control

Quick Integration with React Package

import { WidgetRoot } from '@opencx/widget/react';

function App() {
  return (
    <WidgetRoot
      config={{
        token: 'YOUR_BOT_TOKEN',
        apiUrl: 'YOUR_API_URL',
        user: {
          email: "user@example.com",
          name: "User Name"
        }
      }}
    >
      <YourChatInterface />
    </WidgetRoot>
  );
}

Available Hooks

// Main chat functionality
const { messages, sendMessage, loading, error } = useChat();

// Configuration access
const { config, updateConfig } = useConfigData();

// Localization
const { t, locale, setLocale } = useLocale();

// Sound effects
const { playMessageSound, playNotificationSound } = useWidgetSoundEffects();

// Contact management
const { contact, updateContact } = useContact();

// Message feedback
const [upvoteState, upvote] = useUpvote(messageId);
const [downvoteState, downvote] = useDownvote(messageId);

Custom Integration with Core Package

For more control over the implementation, you can use the core package directly:

import { 
  createChat, 
  createConfig, 
  ApiCaller, 
  Platform,
  type MessageType 
} from '@opencx/widget';
import { useState, useEffect, createContext, useContext } from 'react';

// Create context for chat functionality
const ChatContext = createContext<{
  messages: MessageType[];
  sendMessage: (content: string) => Promise<void>;
  loading: boolean;
  error: Error | null;
} | null>(null);

// Chat provider component
export function ChatProvider({ children }: { children: React.ReactNode }) {
  const [messages, setMessages] = useState<MessageType[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    // Initialize chat
    const config = createConfig({
      token: process.env.REACT_APP_BOT_TOKEN,
      apiUrl: process.env.REACT_APP_API_URL,
      user: {
        email: "user@example.com",
        name: "User Name"
      }
    });

    const api = new ApiCaller({ config: config.getConfig() });

    const platform: Platform = {
      env: { platform: 'web' },
      storage: {
        getItem: async (key) => localStorage.getItem(key),
        setItem: async (key, value) => localStorage.setItem(key, value),
        removeItem: async (key) => localStorage.removeItem(key)
      },
      logger: {
        level: 'debug',
        prefix: '[OpenChat]',
        enabled: true
      }
    };

    const chat = createChat({ api, config, platform });

    // Subscribe to chat state
    const unsubscribe = chat.chatState.subscribe((state) => {
      setLoading(state.loading.isLoading);
      
      if (state.error.hasError) {
        setError(new Error(state.error.message));
        return;
      }
      
      setMessages(state.messages);
    });

    return () => {
      unsubscribe();
    };
  }, []);

  return (
    <ChatContext.Provider value={{
      messages,
      sendMessage: async (content) => {
        // Implementation of send message
      },
      loading,
      error
    }}>
      {children}
    </ChatContext.Provider>
  );
}

// Custom hook for using chat
export function useChat() {
  const context = useContext(ChatContext);
  if (!context) {
    throw new Error('useChat must be used within a ChatProvider');
  }
  return context;
}

Component Examples

Chat Container

function ChatContainer() {
  const { messages, loading, error } = useChat();
  
  if (error) {
    return <ErrorDisplay error={error} />;
  }

  return (
    <div className="chat-container">
      {loading && <LoadingIndicator />}
      <MessageList messages={messages} />
      <InputArea />
    </div>
  );
}

Message List with Virtual Scrolling

import { FixedSizeList } from 'react-window';

function MessageList({ messages }: { messages: MessageType[] }) {
  const listRef = useRef<FixedSizeList>(null);

  useEffect(() => {
    listRef.current?.scrollToItem(messages.length - 1);
  }, [messages.length]);

  const Row = ({ index, style }: { index: number, style: any }) => (
    <Message 
      message={messages[index]} 
      style={style}
    />
  );

  return (
    <FixedSizeList
      ref={listRef}
      height={400}
      itemCount={messages.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Message Component

function Message({ message, style }: { message: MessageType, style?: any }) {
  const messageClass = message.type === 'FROM_USER' ? 'user-message' : 'bot-message';
  
  return (
    <div className={`message ${messageClass}`} style={style}>
      {message.type === 'FROM_BOT' ? (
        <BotMessageContent message={message as BotMessageType<{ text: string }>} />
      ) : (
        <UserMessageContent message={message} />
      )}
      <MessageFeedback messageId={message.id} />
    </div>
  );
}

Message Feedback

function MessageFeedback({ messageId }: { messageId: string }) {
  const [upvoteState, upvote] = useUpvote(messageId);
  const [downvoteState, downvote] = useDownvote(messageId);

  return (
    <div className="message-feedback">
      <button
        onClick={upvote}
        disabled={upvoteState.loading}
        aria-label="Helpful"
      >
        👍
      </button>
      <button
        onClick={downvote}
        disabled={downvoteState.loading}
        aria-label="Not helpful"
      >
        👎
      </button>
    </div>
  );
}

Input Area

function InputArea() {
  const { sendMessage } = useChat();
  const [input, setInput] = useState('');
  const [sending, setSending] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim()) return;
    
    setSending(true);
    try {
      await sendMessage(input);
      setInput('');
    } catch (error) {
      console.error('Failed to send message:', error);
    } finally {
      setSending(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="input-area">
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Type a message..."
        disabled={sending}
        aria-label="Message input"
      />
      <button 
        type="submit" 
        disabled={sending || !input.trim()}
        aria-label={sending ? 'Sending message' : 'Send message'}
      >
        {sending ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

Error Boundaries

class ChatErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Chat error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container" role="alert">
          <h2>Something went wrong</h2>
          <button 
            onClick={() => this.setState({ hasError: false })}
            aria-label="Try again"
          >
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Best Practices

  1. State Management

    • Use React Context for global chat state
    • Implement proper cleanup in useEffect
    • Handle component unmounting
  2. Performance

    • Use virtual scrolling for long message lists
    • Memoize callbacks and heavy computations
    • Implement proper error boundaries
  3. Accessibility

    • Add proper ARIA labels
    • Handle keyboard navigation
    • Provide focus management
  4. Testing

    • Mock chat service for tests
    • Test error scenarios
    • Verify state updates
  5. Error Handling

    • Use error boundaries
    • Provide clear error messages
    • Implement retry mechanisms