The frontend overlay replaces and extends a carefully bounded set of upstream Vue components, Vuex modules, and API helpers. Stage 1 of the Docker build copies every file fromDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/jAtInn71/chatwoot-costom/llms.txt
Use this file to discover all available pages before exploring further.
custom/widget/ and custom/dashboard/ over the cloned Chatwoot source before running vite build, so the compiled bundle already contains all customizations. No raw .vue files are shipped in the final image — everything goes through the Vite pipeline.
Widget components
All components live incustom/widget/components/ and are copied over their upstream counterparts at the paths listed in the Dockerfile.
ElevenLabsVoiceButton.vue
The main voice button. It is the single point of integration between the Chatwoot widget UI and the ElevenLabs Conversational AI service.
How it loads the ElevenLabs embed. The component lazy-loads https://unpkg.com/@elevenlabs/convai-widget-embed the first time hasElevenLabsVoiceEnabled becomes true. It mounts an <elevenlabs-convai agent-id="..."> custom element inside a hidden host <div> that is positioned at left: -10000px; top: -10000px — completely off-screen. A global (non-scoped) stylesheet targeting elevenlabs-convai[data-chatwoot] suppresses any floating UI the embed portals into document.body.
Why not @elevenlabs/client? The npm package bundles LiveKit JS SDK 2.x, which negotiates protocol=17. ElevenLabs’ production LiveKit server runs protocol=16, causing NegotiationError: negotiation timed out. The CDN embed bundle ships its own older LiveKit build that speaks protocol 16, so audio works end-to-end.
Call lifecycle. The button shows only when all three conditions are true: isVoiceAgentEnabled, voiceAgentProvider === 'elevenlabs', and agentId resolves to a non-empty string. Clicks toggle between startCall() and endCall().
startCall() requests microphone permission, ensures the embed is mounted, then clicks the embed’s internal “Start call” shadow-DOM button. If the shadow DOM is not yet rendered, it retries after 250 ms.
endCall() tears down in three layers to ensure the WebSocket and WebRTC peer connection are actually closed:
- Call any public API methods exposed on the element (
endSession,endCall,disconnect,stop,close). - Click the embed’s internal end button as a fallback.
- Remove the element from the DOM after 300 ms —
disconnectedCallbackforce-closes all connections — then remount a fresh element so the next call starts cold.
convai-message, message, and transcript events on the embed element. Each event carries { source: 'user'|'ai', message }. The handler POSTs to /api/v1/widget/conversations/voice_transcript and then dispatches conversation/syncLatestMessages so the visitor sees their spoken turns in the chat bubble area in real time.
ChatInputWrap.vue
Wraps the chat input area and renders ElevenLabsVoiceButton next to the emoji button when the text input is empty. This is the only place in the widget where the voice button is mounted — a previous instance in HeaderActions.vue was removed to ensure there is never more than one call button visible at a time.
HeaderActions.vue
Provides the exit-chat button at the top of the widget. The endChat method implements the soft-exit pattern:
- Calls
toggleStatusto resolve the conversation server-side. - Dispatches
contacts/softExitChatto clear Vuex state and session storage without postingexitChator reloading the iframe. - Navigates to the
homeroute. - Posts
{ event: 'closeWindow' }to the parent SDK so the panel collapses.
Branding.vue
Replaces the upstream Branding.vue component, which renders Powered by Chatwoot as an anchor pointing to https://www.chatwoot.com. The custom version renders a plain <span> with no outbound link and no Chatwoot logo, using whatever label is set in the POWERED_BY i18n key.
Widget store modules
All modules live incustom/widget/store/modules/ and are registered in the custom store/index.js.
voiceAgentConfig.js
Fetches and stores per-inbox voice configuration. The fetchVoiceAgentConfig action calls GET /api/v1/widget/conversations/inbox_config and commits the response fields. It is dispatched by App.vue every time the widget panel opens.
Key getters:
| Getter | Returns |
|---|---|
isVoiceAgentEnabled | true when selected_feature_flags includes 'elevenlabs_voice' (or legacy 'voice_agent') |
getVoiceAgentProvider | Provider string, e.g. "elevenlabs" |
getAgentId | Resolves in order: voice_agent_config_data.agent_id → elevenlabs_agent_id column → window.chatwootConfig.elevenLabsAgentId → null |
elevenlabsVoice.js
Tracks the active call state. Exposes setActive(bool) and setConnecting(bool) actions consumed by ElevenLabsVoiceButton to keep call status in sync with the Vuex store.
contacts.js
Extends the upstream contacts module with auth token storage and the softExitChat action. softExitChat clears the Vuex contact and conversation state, removes auth headers from the shared axios instance, and calls clearSessionStorage(). It does not post exitChat and does not call window.location.reload().
appConfig.js
Stores widget-level configuration including the ElevenLabs API key field (apiKey: '' — populated at runtime from dashboard config) and any widget behaviour overrides.
Widget API
custom/widget/api/contacts.js
Handles contact creation and auth token management. When the widget initialises without an existing auth token, this module creates a new anonymous contact and stores the returned token in session storage. On soft-exit, the token is cleared so the next session starts fresh.
custom/widget/api/conversation.js
Wraps conversation creation and message sending. Uses the custom axios instance to ensure auth token headers are sent with every request.
Widget helpers and mixins
custom/widget/helpers/axios.js
Creates the shared axios instance used by all API modules. It injects the contact auth token from session storage into every request header, removing the need for each API module to manage auth manually.
custom/widget/mixins/configMixin.js
A shared Vue mixin providing access to the current widget configuration. Used by ElevenLabsVoiceButton and other components to read widget color, website token, and other inbox-level settings without duplicating config access logic.
Dashboard
custom/dashboard/ConfigurationPage.vue
Overrides the upstream inbox configuration page at app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue. The custom version adds a Voice Agent section below the existing widget settings with the following fields:
- Enable voice agent — toggle that sets the
elevenlabs_voicefeature flag (bit 5). - Provider — dropdown, currently
"elevenlabs"(built to accept additional providers). - API key — text input written to
voice_agent_api_key; never exposed to the widget. - Agent ID — text input written to both
elevenlabs_agent_idandvoice_agent_config_data.agent_id.
voice_agent_config_data is serialised to a JSON string before the form submits — this is required because EDITABLE_ATTRS lists it as a bare symbol. See the backend note for the full explanation.
Widget changes (
custom/widget/) require a Vite rebuild — run ./build.sh and redeploy. Dashboard changes (custom/dashboard/) are compiled by the same Vite pipeline in Stage 1 of the Dockerfile; there is no separate asset pipeline step for dashboard files.