chatwoot/app/javascript/react-components/doc.md
2025-12-01 15:25:06 +05:30

18 KiB

Chatwoot React Components - Architecture Documentation

This document provides comprehensive context for the embeddable chat UI components built with React by wrapping Vue components as Web Components.

Table of Contents

  1. Overview
  2. Architecture Diagram
  3. Build System
  4. Component Hierarchy
  5. Key Patterns
  6. Global State & Configuration
  7. Real-time Communication
  8. File Structure
  9. Extension Guide
  10. Known Limitations

Overview

The system enables embedding Chatwoot's conversation UI into any React application. It achieves this through a three-layer architecture:

  1. React Layer - Provider pattern for configuration and React-friendly API
  2. Web Component Layer - Vue components converted to Custom Elements via defineCustomElement
  3. Vue Layer - Existing Chatwoot Vue components reused without modification

Why This Approach?

  • Code Reuse: Leverage existing battle-tested Vue components (components-next/message/*)
  • Framework Agnostic: Web Components work in any framework (React, Angular, vanilla JS)
  • Isolated Styles: Shadow DOM encapsulation prevents CSS conflicts
  • Shared State: Single Vuex store instance across all components

Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                     React Application                            │
├─────────────────────────────────────────────────────────────────┤
│  <ChatwootProvider>                                              │
│    ├── Sets up globals (window.__WOOT_*)                        │
│    ├── Initializes Vuex store                                   │
│    ├── Registers Web Components                                 │
│    └── Initializes ActionCable (WebSocket)                      │
│                                                                  │
│    <ChatwootConversation>                                        │
│      └── <ChatwootMessageListWrapper>                           │
│            └── <chatwoot-message-list> (Web Component)          │
│                  └── [Shadow DOM]                               │
│                        └── MessageList.vue                      │
│                              ├── Message.vue (components-next)  │
│                              ├── TypingIndicator.vue            │
│                              └── LiteReplyBox.vue               │
└─────────────────────────────────────────────────────────────────┘

Build System

Build Modes (vite.config.ts)

The Vite configuration supports multiple build modes via BUILD_MODE environment variable:

Mode Command Output Purpose
library pnpm build:sdk public/packs/js/sdk.js Widget SDK (IIFE)
ui pnpm build:ui public/packs/js/ui.js Standalone UI (IIFE)
react-components pnpm build:react public/packs/react-components.{es,cjs}.js NPM package (ES + CJS)
(default) bin/vite build Standard Vite output Main app build

React Components Build Pipeline

# Full build and package
pnpm package:react

# Build only (no packaging)
pnpm build:react

# Copy mode (skip build, just package)
node scripts/package-react-components.js --copyMode

Build Output (dist/react-components/):

  • index.js - ES module entry
  • index.cjs - CommonJS entry
  • style.css - Bundled styles
  • package.json - Generated with timestamp versioning

Plugin Configuration

// react-components mode plugins
plugins = [
  vue({ ...vueOptions, customElement: true }),  // Enable CE mode
  react()                                        // React JSX transform
];

// Rollup externals - React is peer dependency
rollupOptions = {
  external: ['react', 'react-dom'],
};

Component Hierarchy

React Components (src/components/)

ChatwootProvider.jsx

Purpose: Root provider that initializes the entire Chatwoot environment.

Key Responsibilities:

  1. Validates required props (baseURL, userToken)
  2. Registers Vue Web Components via registerVueWebComponents()
  3. Sets up global window variables for Vue components
  4. Initializes axios with authentication
  5. Dispatches setUser to hydrate auth state
  6. Initializes ActionCable for real-time updates

Props:

interface ChatwootProviderProps {
  baseURL: string;        // Required: Chatwoot API URL
  userToken: string;      // Required: Agent access token
  accountId: number;      // Account ID
  conversationId: number; // Conversation to display
  websocketURL?: string;  // WebSocket server URL
  pubsubToken?: string;   // PubSub authentication token
  disableUpload?: boolean; // Disable file attachments
  disableEditor?: boolean; // Disable reply editor
}

Global Variables Set:

window.__WOOT_API_HOST__      // API base URL
window.__WOOT_ACCOUNT_ID__    // Account ID
window.__WOOT_ACCESS_TOKEN__  // Auth token (used by axios)
window.__WEBSOCKET_URL__      // WebSocket URL
window.__PUBSUB_TOKEN__       // Real-time auth token
window.__WOOT_CONVERSATION_ID__ // Active conversation
window.__EDITOR_DISABLE_UPLOAD__ // Upload flag
window.__DISABLE_EDITOR__     // Editor flag
window.__WOOT_ISOLATED_SHELL__ // Disables audio notifications
window.__CHATWOOT_STORE__     // Vuex store reference
window.WootConstants          // Global constants
window.axios                  // Configured axios instance

ChatwootConversation.jsx

Purpose: High-level wrapper providing styled container.

Pattern: Uses useChatwoot() hook to enforce Provider context.

ChatwootMessageListWrapper.jsx

Purpose: Bridge between React and Web Component.

Key Pattern - Imperative Handle Updates:

// React props → Web Component properties
useEffect(() => {
  element.conversationId = conversationId;
}, [conversationId]);

Event Forwarding:

element.addEventListener('chatwoot:loaded', handleLoad);
element.addEventListener('chatwoot:error', handleError);

Web Component Layer (src/vue-components/)

registerWebComponents.js

Purpose: Converts Vue SFCs to Web Components and registers them.

Key Pattern - Style Injection:

const ceOptions = {
  configureApp(app) {
    app.use(store);
    app.use(VueDOMPurifyHTML, domPurifyConfig);
    app.use(i18n);
    app.provide(I18nInjectionKey, i18n);
    vueActionCable.init(store, window.__PUBSUB_TOKEN__);
  },
  // Styles bundled into Shadow DOM
  styles: [chatwootStyles, multiselectStyles, floatingVueStyles, uploadStyles],
};

const ChatwootMessageListElement = defineCustomElement(
  ChatwootMessageListWebComponent,
  ceOptions
);

Important: Styles are imported with ?inline query to get CSS as string:

import chatwootStyles from '../../../dashboard/assets/scss/app.scss?inline';

ChatwootMessageListWebComponent.vue

Purpose: Thin wrapper that renders the actual MessageList.

Pattern - Additional Styles for Third-Party Components:

<style>
/* vue-upload-component requires these styles */
.file-uploads { /* ... */ }
</style>

Vue Layer (/app/javascript/ui/)

MessageList.vue

Purpose: Core conversation view with infinite scroll.

Key Features:

  1. Infinite Scroll: Uses @vueuse/core's useInfiniteScroll
  2. Typing Indicators: Real-time via conversationTypingStatus store module
  3. Message Rendering: Uses components-next/message/Message.vue
  4. Reply Box: Integrated LiteReplyBox.vue

Data Flow:

// Gets conversation ID from global
const conversationId = computed(() => window.__WOOT_CONVERSATION_ID__);

// Fetches conversation data on mount
onMounted(async () => {
  await store.dispatch('inboxes/get');
  await Promise.all([
    store.dispatch('getConversation', conversationId.value),
    store.dispatch('fetchAllAttachments', conversationId.value),
  ]);
});

Key Pattern - Store Access Without Vue Instance:

// From composables/store.js
export const useStore = () => {
  if (window.__CHATWOOT_STORE__) {
    return window.__CHATWOOT_STORE__;
  }
  // Fallback to Vue instance
  const vm = getCurrentInstance();
  return vm.proxy.$store;
};

LiteReplyBox.vue

Purpose: Message composition editor.

Key Features:

  • Rich text editing via ProseMirror (WootMessageEditor)
  • File attachments with preview
  • Typing indicators (on/off status)
  • Keyboard shortcuts (Cmd/Ctrl+Enter to send)
  • Private notes support

Conditional Features via Globals:

isEditorDisabled() {
  return window.__DISABLE_EDITOR__;
},
allowFileUpload() {
  return window.__EDITOR_DISABLE_UPLOAD__ !== true;
},

axios.js (UI-specific axios factory)

Purpose: Creates axios instance configured for external use.

Pattern - Token from Global:

export default axios => {
  const apiHost = window.__WOOT_API_HOST__;
  const accessToken = window.__WOOT_ACCESS_TOKEN__;

  const wootApi = axios.create({ baseURL: `${apiHost}/` });

  Object.assign(wootApi.defaults.headers.common, {
    api_access_token: accessToken,
  });

  return wootApi;
};

Key Patterns

1. Global Variable Bridge

Vue components expect certain globals. The React provider sets these before rendering:

// Set by ChatwootProvider
window.__WOOT_API_HOST__ = config.baseURL;
window.__WOOT_ACCOUNT_ID__ = config.accountId;
// ...etc

// Consumed by Vue components
const accountIdFromRoute = window.__WOOT_ACCOUNT_ID__;

2. Vuex Store as Global Singleton

The store is created once and shared via window.__CHATWOOT_STORE__:

// In ChatwootProvider
import store from '../../../dashboard/store';
window.__CHATWOOT_STORE__ = store;

// In Vue composables
export const useStore = () => {
  if (window.__CHATWOOT_STORE__) {
    return window.__CHATWOOT_STORE__;
  }
  // ...
};

3. I18n in Web Components

Vue I18n requires special injection for Web Components:

// Must provide I18nInjectionKey for useI18n() to work in CE mode
app.use(i18n);
app.provide(I18nInjectionKey, i18n);

Reference: https://vue-i18n.intlify.dev/guide/advanced/wc

4. Isolated Shell Mode

The __WOOT_ISOLATED_SHELL__ flag disables features not needed in embedded mode:

// In actionCable.js
if (!window.__WOOT_ISOLATED_SHELL__) {
  DashboardAudioNotificationHelper.onNewMessage(data);
}

5. CamelCase Transformation

API returns snake_case, Vue components expect camelCase:

import { useCamelCase } from '../dashboard/composables/useTransformKeys';

const allMessages = computed(() => {
  return useCamelCase(conversation.value.messages, { deep: true }).reverse();
});

6. Web Component Event Communication

Web Components emit custom events that React can listen to:

// In Web Component
this.$emit('chatwoot:loaded');
this.$emit('chatwoot:error', { message: 'Error details' });

// In React wrapper
element.addEventListener('chatwoot:loaded', handleLoad);
element.addEventListener('chatwoot:error', handleError);

Real-time Communication

ActionCable Setup

// BaseActionCableConnector.js
const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined;
this.consumer = createConsumer(websocketURL);
this.subscription = this.consumer.subscriptions.create({
  channel: 'RoomChannel',
  pubsub_token: pubsubToken,
  account_id: app.$store.getters.getCurrentAccountId,
  user_id: app.$store.getters.getCurrentUserID,
});

Events Handled

Event Action
message.created Add message to conversation
message.updated Update existing message
conversation.typing_on Show typing indicator
conversation.typing_off Hide typing indicator
presence.update Update online status
conversation.updated Refresh conversation data

Presence Updates

Heartbeat every 20 seconds:

const PRESENCE_INTERVAL = 20000;
this.triggerPresenceInterval = () => {
  setTimeout(() => {
    this.subscription.updatePresence();
    this.triggerPresenceInterval();
  }, PRESENCE_INTERVAL);
};

File Structure

app/javascript/
├── react-components/
│   ├── src/
│   │   ├── index.jsx                     # Package exports
│   │   ├── components/
│   │   │   ├── ChatwootProvider.jsx      # Root provider
│   │   │   ├── ChatwootConversation.jsx  # Container component
│   │   │   └── ChatwootMessageListWrapper.jsx  # WC bridge
│   │   ├── vue-components/
│   │   │   ├── registerWebComponents.js  # WC registration
│   │   │   └── ChatwootMessageListWebComponent.vue
│   │   └── styles/
│   │       └── upload.css                # File upload styles
│   └── doc.md                            # This file
│
├── ui/
│   ├── MessageList.vue                   # Core message list
│   ├── LiteReplyBox.vue                  # Reply editor
│   ├── axios.js                          # Axios factory
│   └── usage.md                          # Usage docs
│
├── dashboard/
│   ├── store/                            # Vuex store
│   │   ├── index.js
│   │   └── modules/
│   │       ├── conversations/            # Conversation state
│   │       ├── auth.js                   # Auth state
│   │       └── conversationTypingStatus.js
│   ├── components-next/
│   │   └── message/                      # Message components
│   │       ├── Message.vue
│   │       ├── TypingIndicator.vue
│   │       └── bubbles/
│   ├── composables/
│   │   ├── store.js                      # Store helpers
│   │   └── useTransformKeys.js           # Case conversion
│   ├── helper/
│   │   ├── actionCable.js                # WebSocket connector
│   │   └── commons.js                    # Utility functions
│   └── api/
│       ├── ApiClient.js                  # Base API client
│       └── auth.js                       # Auth API
│
└── shared/
    └── helpers/
        └── BaseActionCableConnector.js   # WebSocket base class

scripts/
└── package-react-components.js           # NPM packaging script

vite.config.ts                            # Multi-mode build config

Extension Guide

Adding a New Web Component

  1. Create Vue Component (vue-components/NewComponent.vue):
<script setup>
import { useStore, useMapGetter } from 'dashboard/composables/store';
// Use composables that check window.__CHATWOOT_STORE__
</script>
  1. Register Web Component (registerWebComponents.js):
import NewComponent from './NewComponent.vue';

const NewElement = defineCustomElement(NewComponent, ceOptions);

export const registerVueWebComponents = () => {
  // ...existing registrations
  if (!customElements.get('chatwoot-new-component')) {
    customElements.define('chatwoot-new-component', NewElement);
  }
};
  1. Create React Wrapper (components/NewComponentWrapper.jsx):
export const NewComponentWrapper = (props) => {
  const elementRef = useRef();

  useEffect(() => {
    // Sync React props to WC properties
  }, [props]);

  return <chatwoot-new-component ref={elementRef} />;
};
  1. Export from index.jsx:
export { NewComponentWrapper } from './components/NewComponentWrapper';

Adding New Global Configuration

  1. Add prop to ChatwootProvider.jsx
  2. Set corresponding window.__WOOT_* variable
  3. Access in Vue components via window.__WOOT_*

Adding Store Modules

If the new component needs additional Vuex state:

  1. Ensure module is already in dashboard/store/index.js
  2. Access via useStore().dispatch('module/action') or useMapGetter('module/getter')

Known Limitations

1. Single Conversation Per Page

Currently, only one conversation can be displayed at a time due to global state (__WOOT_CONVERSATION_ID__).

Workaround: For multiple conversations, instantiate separate iframes.

2. Style Isolation

Shadow DOM prevents parent CSS from affecting components, but also means:

  • Parent theme variables don't cascade
  • Global CSS resets don't apply

Workaround: Pass theme configuration via props/globals.

3. React DevTools

Web Components don't appear in React DevTools component tree. Use browser's native Custom Elements inspector.

4. Hot Module Replacement

Vue components inside Web Components don't support HMR well. Full page reload often needed during development.

5. Bundle Size

The react-components bundle includes:

  • Full Vuex store
  • All SCSS styles
  • I18n messages

Future optimization: Tree-shake unused store modules.

6. Single Language (English Only)

Currently hardcoded to English locale:

const i18n = createI18n({
  locale: 'en',
  messages: { en },
});

Debugging Tips

Inspect Vuex Store

// In browser console
window.__CHATWOOT_STORE__.state
window.__CHATWOOT_STORE__.getters.getConversationById(123)

Check WebSocket Connection

// ActionCable consumer status
window.__CHATWOOT_STORE__._modules.root._rawModule // Check if actions dispatch

Verify Web Component Registration

customElements.get('chatwoot-message-list') // Should return constructor

Force Re-render

// Dispatch any action to trigger reactivity
window.__CHATWOOT_STORE__.dispatch('setActiveChat', { data: { id: conversationId } });

  • CLAUDE.md - General Chatwoot development guidelines
  • app/javascript/ui/usage.md - UI components usage guide
  • vite.config.ts - Build configuration documentation (inline comments)