chatwoot/app/javascript/react-components/doc.md
2026-03-05 16:01:03 +05:30

28 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. Runtime Theme Customization
  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
  disableSignature?: boolean; // Disable signature controls and insertion
  signature?: string;      // Custom message signature (overrides user profile signature)
}

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_DISABLE_SIGNATURE__ // Disable signature controls and insertion
window.__WOOT_CUSTOM_SIGNATURE__ // Custom signature (overrides profile)
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);

7. CSS Variable Theming for Bubbles

Message bubble styles use CSS custom properties that can be overridden from outside the Shadow DOM:

// In Base.vue - uses CSS variables for colors
const varaintBaseMap = {
  [MESSAGE_VARIANTS.AGENT]: 'bg-[rgb(var(--bubble-agent-bg))] text-[rgb(var(--bubble-agent-text))]',
  [MESSAGE_VARIANTS.USER]: 'bg-[rgb(var(--bubble-user-bg))] text-[rgb(var(--bubble-user-text))]',
  // ...
};

// Border radius also uses variables
const orientationMap = {
  [ORIENTATION.LEFT]: 'rounded-[var(--bubble-radius)] ltr:rounded-bl-[var(--bubble-radius-sm)]...',
  // ...
};

Variables are defined in _next-colors.scss and can be overridden via bubble-overrides.css.


Runtime Theme Customization

The Web Component supports runtime theming via CSS custom properties. These variables inherit through the Shadow DOM boundary, allowing consumers to customize bubble appearance without modifying the source.

Available CSS Variables

Colors (RGB values without commas)

Variable Description Default
--chatwoot-bubble-agent-bg Agent message background --solid-blue
--chatwoot-bubble-agent-text Agent message text --slate-12
--chatwoot-bubble-user-bg User/customer message background --slate-4
--chatwoot-bubble-user-text User message text --slate-12
--chatwoot-bubble-private-bg Private note background --solid-amber
--chatwoot-bubble-private-text Private note text --amber-12
--chatwoot-bubble-bot-bg Bot/template message background --solid-iris
--chatwoot-bubble-bot-text Bot message text --slate-12
--chatwoot-bubble-error-bg Error message background --ruby-4
--chatwoot-bubble-error-text Error message text --ruby-12
--chatwoot-bubble-unsupported-bg Unsupported message background --amber-4
--chatwoot-bubble-unsupported-text Unsupported message text --amber-12

Meta (timestamp) - Per Variant

Variable Description Default
--chatwoot-bubble-agent-meta Meta text for agent messages --slate-11
--chatwoot-bubble-user-meta Meta text for user messages --slate-11
--chatwoot-bubble-private-meta Meta text for private notes (50% opacity applied) --amber-12
--chatwoot-bubble-bot-meta Meta text for bot messages --slate-11
--chatwoot-bubble-error-meta Meta text for error messages --ruby-11

Message Status Icons - Per Variant

Variable Description Default
--chatwoot-bubble-agent-status Agent: sent/delivered/progress color --slate-10
--chatwoot-bubble-agent-status-read Agent: read receipt color 126 182 255
--chatwoot-bubble-user-status User: sent/delivered/progress color --slate-10
--chatwoot-bubble-user-status-read User: read receipt color 126 182 255
--chatwoot-bubble-private-status Private: sent/delivered/progress color --amber-11
--chatwoot-bubble-private-status-read Private: read receipt color 126 182 255
--chatwoot-bubble-bot-status Bot: sent/delivered/progress color --slate-10
--chatwoot-bubble-bot-status-read Bot: read receipt color 126 182 255

Spacing

Variable Description Default
--chatwoot-bubble-spacing-ratio Scale factor for border radius 1

Border

Variable Description Default
--chatwoot-bubble-border-width Border width (0 = invisible) 0
--chatwoot-bubble-agent-border Agent message border color --blue-6
--chatwoot-bubble-user-border User message border color --slate-6
--chatwoot-bubble-private-border Private note border color --amber-6
--chatwoot-bubble-bot-border Bot message border color --iris-6
--chatwoot-bubble-error-border Error message border color --ruby-6
--chatwoot-bubble-unsupported-border Unsupported message border color --amber-12

Usage Examples

Option 1: Global CSS

/* In your app's CSS file */
:root {
  --chatwoot-bubble-agent-bg: 59 130 246;    /* Blue */
  --chatwoot-bubble-user-bg: 243 244 246;    /* Light gray */
  --chatwoot-bubble-spacing-ratio: 1.5;      /* 50% more rounded */
  --chatwoot-bubble-border-width: 1px;       /* Add borders */
  --chatwoot-bubble-agent-border: 59 130 246; /* Blue border */
}

Option 2: Wrapper with Inline Styles

function App() {
  return (
    <div
      style={{
        '--chatwoot-bubble-agent-bg': '59 130 246',
        '--chatwoot-bubble-user-bg': '243 244 246',
        '--chatwoot-bubble-spacing-ratio': '1.5',
        '--chatwoot-bubble-border-width': '1px',
        '--chatwoot-bubble-agent-border': '59 130 246',
      }}
    >
      <ChatwootProvider {...props}>
        <ChatwootConversation />
      </ChatwootProvider>
    </div>
  );
}

Option 3: Dynamic Theming

function App() {
  const [theme, setTheme] = useState({
    '--chatwoot-bubble-agent-bg': '59 130 246',
    '--chatwoot-bubble-border-width': '1px',
    '--chatwoot-bubble-agent-border': '59 130 246',
  });

  return (
    <div style={theme}>
      <button onClick={() => setTheme({
        '--chatwoot-bubble-agent-bg': '34 197 94',      // Switch to green
        '--chatwoot-bubble-border-width': '2px',
        '--chatwoot-bubble-agent-border': '34 197 94',  // Green border
      })}>
        Change Theme
      </button>
      <ChatwootProvider {...props}>
        <ChatwootConversation />
      </ChatwootProvider>
    </div>
  );
}

Option 4: Borders Only

function App() {
  return (
    <div
      style={{
        '--chatwoot-bubble-border-width': '2px',
        '--chatwoot-bubble-agent-border': '59 130 246',   // Blue border
        '--chatwoot-bubble-user-border': '156 163 175',   // Gray border
        '--chatwoot-bubble-private-border': '251 191 36', // Amber border
        '--chatwoot-bubble-bot-border': '129 140 248',    // Iris border
      }}
    >
      <ChatwootProvider {...props}>
        <ChatwootConversation />
      </ChatwootProvider>
    </div>
  );
}

How It Works

┌─────────────────────────────────────────────────────────────────┐
│  Consumer's CSS                                                  │
│  :root {                                                         │
│    --chatwoot-bubble-agent-bg: 59 130 246;                      │
│    --chatwoot-bubble-border-width: 1px;                         │
│  }                                                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼ (CSS inheritance)
┌─────────────────────────────────────────────────────────────────┐
│  Shadow DOM (:host)                                              │
│  bubble-overrides.css:                                          │
│    --bubble-agent-bg: var(--chatwoot-bubble-agent-bg,           │
│                           var(--solid-blue));                   │
│    --bubble-border-width: var(--chatwoot-bubble-border-width,   │
│                               0);                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼ (used by components)
┌─────────────────────────────────────────────────────────────────┐
│  Base.vue                                                        │
│  class="bg-[rgb(var(--bubble-agent-bg))]                        │
│         border-[length:var(--bubble-border-width)]              │
│         border-[rgb(var(--bubble-agent-border))]"               │
└─────────────────────────────────────────────────────────────────┘

Key Files

File Purpose
src/styles/bubble-overrides.css Defines :host overrides that bridge external variables
dashboard/assets/scss/_next-colors.scss Default theme values
dashboard/components-next/message/bubbles/Base.vue Consumes CSS variables

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
│   │       └── bubble-overrides.css      # Runtime theme customization
│   └── 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:

  • Global CSS resets don't apply
  • Most parent styles don't cascade

Solution: Message bubble colors and border radius can be customized via CSS custom properties (see Runtime Theme Customization). CSS custom properties DO inherit through Shadow DOM boundaries.

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)