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

616 lines
18 KiB
Markdown

# 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](#overview)
2. [Architecture Diagram](#architecture-diagram)
3. [Build System](#build-system)
4. [Component Hierarchy](#component-hierarchy)
5. [Key Patterns](#key-patterns)
6. [Global State & Configuration](#global-state--configuration)
7. [Real-time Communication](#real-time-communication)
8. [File Structure](#file-structure)
9. [Extension Guide](#extension-guide)
10. [Known Limitations](#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
```bash
# 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
```javascript
// 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**:
```typescript
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**:
```javascript
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**:
```jsx
// React props → Web Component properties
useEffect(() => {
element.conversationId = conversationId;
}, [conversationId]);
```
**Event Forwarding**:
```jsx
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**:
```javascript
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:
```javascript
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**:
```vue
<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**:
```javascript
// 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**:
```javascript
// 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**:
```javascript
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**:
```javascript
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:
```javascript
// 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__`:
```javascript
// 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:
```javascript
// 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:
```javascript
// In actionCable.js
if (!window.__WOOT_ISOLATED_SHELL__) {
DashboardAudioNotificationHelper.onNewMessage(data);
}
```
### 5. CamelCase Transformation
API returns snake_case, Vue components expect camelCase:
```javascript
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:
```javascript
// 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
```javascript
// 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:
```javascript
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`):
```vue
<script setup>
import { useStore, useMapGetter } from 'dashboard/composables/store';
// Use composables that check window.__CHATWOOT_STORE__
</script>
```
2. **Register Web Component** (`registerWebComponents.js`):
```javascript
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);
}
};
```
3. **Create React Wrapper** (`components/NewComponentWrapper.jsx`):
```jsx
export const NewComponentWrapper = (props) => {
const elementRef = useRef();
useEffect(() => {
// Sync React props to WC properties
}, [props]);
return <chatwoot-new-component ref={elementRef} />;
};
```
4. **Export from index.jsx**:
```javascript
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:
```javascript
const i18n = createI18n({
locale: 'en',
messages: { en },
});
```
---
## Debugging Tips
### Inspect Vuex Store
```javascript
// In browser console
window.__CHATWOOT_STORE__.state
window.__CHATWOOT_STORE__.getters.getConversationById(123)
```
### Check WebSocket Connection
```javascript
// ActionCable consumer status
window.__CHATWOOT_STORE__._modules.root._rawModule // Check if actions dispatch
```
### Verify Web Component Registration
```javascript
customElements.get('chatwoot-message-list') // Should return constructor
```
### Force Re-render
```javascript
// Dispatch any action to trigger reactivity
window.__CHATWOOT_STORE__.dispatch('setActiveChat', { data: { id: conversationId } });
```
---
## Related Files
- `CLAUDE.md` - General Chatwoot development guidelines
- `app/javascript/ui/usage.md` - UI components usage guide
- `vite.config.ts` - Build configuration documentation (inline comments)