feat: add doc.md
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user