ocwi-core
v2.3.0
Published
OCWI – Embeddable AI Open Chat WIdget
Maintainers
Readme
OCWI — Embeddable AI Chat Widget
Framework-agnostic, single-file JS chat widget built with TypeScript and Lit.
Ships as one IIFE bundle — drop it into any web page with a single <script> tag.
Key properties:
- No React / Vue / Svelte — only Lit (Web Components)
- Single JS file:
dist/ocwi.min.js(Lit bundled in, ~50 kB gzip) - Shadow DOM + CSS variables for full theme isolation
- Connects to Luma (config server) and streams AI responses from Dana
- Config layering: default → Luma remote → local overrides
- i18n with runtime language switch and custom dictionaries (
en,cs,debuilt-in) - Automatic reconnect with exponential backoff
How it works
OCWI is the browser-side piece of a three-component system:
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ OCWI (Web Component — this package) │ │
│ │ Chat widget embedded into any web page │ │
│ └───────────────────┬──────────────────────────────┘ │
└───────────────────────│─────────────────────────────────┘
│ HTTPS (SSE stream + REST)
▼
┌───────────────────────────────────────────────────────────┐
│ Dana (FastAPI / Python — AI backend) │
│ RAG pipeline, LLM integration, SSE streaming │
└───────────────────────────────────────────────────────────┘OCWI fetches its UI configuration (colors, agent name, language, …) from Luma at startup and caches it in local storage for instant subsequent loads. Luma returns a Dana public-key chat URL; OCWI then sends chat, replay, interaction and health requests directly to Dana. Dana is the runtime security boundary: it enforces the public key, published state, organization state and rate limits. Luma remains the configuration and publishing control plane.
Installation
CDN (recommended for most use cases)
<!-- jsDelivr (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/ocwi-core/dist/ocwi-core.min.js"></script>
<!-- unpkg (alternative) -->
<script src="https://unpkg.com/ocwi-core/dist/ocwi.min.js"></script>Both CDNs serve the latest tag by default. To pin to a specific version:
<!-- jsDelivr (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ocwi.min.js"></script>
<!-- unpkg (alternative) -->
<script src="https://unpkg.com/[email protected]/dist/ocwi.min.js"></script>npm
npm install ocwiQuick start
<script src="https://cdn.jsdelivr.net/npm/ocwi-core/dist/ocwi.min.js"></script>
<div id="chat"></div>
<script>
const inst = window.OCWI('#chat', {
api: {
lumaUrl: 'https://your-luma-server.example.com/public/abc123'
}
});
</script>Without a lumaUrl, the widget starts in demo mode using a built-in mock API:
<script src="https://cdn.jsdelivr.net/npm/ocwi-core/dist/ocwi.min.js"></script>
<div id="chat"></div>
<script>
window.OCWI('#chat', {
ui: { name: 'Demo Assistant' },
theme: { primary: '#0ea5e9' },
locale: 'cs'
});
</script>Public API
window.OCWI(target?, config?)
Creates an <ocwi-chat> element and applies config. On a first visit (no cached config yet)
it attaches to the DOM only after the config verdict is in, so a hidden/unpublished agent never
paints; with a cached config it attaches immediately and refreshes in the background.
| Parameter | Type | Description |
| --------- | --------------------------- | ------------------------------------------------------------------- |
| target | string \| Element \| null | CSS selector or Element to mount into. Defaults to document.body. |
| config | Partial<OcwiConfig> | Optional local config (merged last — highest priority). |
Returns the <ocwi-chat> element instance.
Instance methods
| Method | Description |
| ----------------------- | ------------------------------------------------------------------------- |
| updateConfig(partial) | Deep-merge partial config; live update (no reload). |
| getState() | Returns current widget state { messages, isStreaming, windowState, … }. |
| destroy() | Cleans up all listeners and removes the element from DOM. |
Configuration
window.OCWI('#chat', {
api: {
lumaUrl: 'https://luma.example.com/public/abc123', // Luma public link
timeoutMs: 30000
},
locale: 'cs', // Top-level shortcut for i18n.lang
i18n: {
lang: 'cs',
dictionary: {
cs: { send: 'Odeslat' } // Override individual keys
}
},
theme: {
primary: '#0ea5e9',
ocwiWidth: '360px',
ocwiHeight: '520px',
ocwiRadius: '16px'
},
ui: {
name: 'Asistent',
placeholder: 'Napište zprávu…',
introductionMessage: 'Dobrý den! Jak vám mohu pomoci?',
position: 'bottom-right',
initialState: 'collapsed', // 'collapsed' | 'minimized' | 'expanded'
keybindForSend: 'Enter' // 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Alt+Enter' | 'None'
},
features: {
minimize: true,
close: true,
serverStatus: true,
messageCopy: true,
messageEdit: true,
messageRefresh: true,
sendButton: true,
stopButton: true,
placeholder: true
}
});api
| Field | Type | Description |
| ----------- | -------- | ------------------------------------------------------------------------------------------ |
| lumaUrl | string | Luma config URL. Widget fetches remote config from here during mount. |
| danaUrl | string | Direct Dana chat URL returned by Luma config, normally ending in /api/chat/<public-key>. |
| timeoutMs | number | HTTP timeout in ms. Default: 70000. |
theme
| Field | CSS variable | Example |
| ------------------------- | ------------------------------ | ------------ |
| primary | --ocwi-primary | '#0ea5e9' |
| ocwiWidth | --ocwi-width | '360px' |
| ocwiHeight | --ocwi-height | '520px' |
| ocwiRadius | --ocwi-radius | '12px' |
| ocwiSpacing | --ocwi-spacing | '8px' |
| ocwiBg | --ocwi-bg | '#ffffff' |
| ocwiHeaderBg | --ocwi-header-bg | '#f9fafb' |
| ocwiHeaderText | --ocwi-header-text | '#111111' |
| ocwiBubbleUserBg | --ocwi-bubble-user-bg | '#2563eb' |
| ocwiBubbleUserText | --ocwi-bubble-user-text | '#ffffff' |
| ocwiBubbleAssistantBg | --ocwi-bubble-assistant-bg | '#f3f4f6' |
| ocwiBubbleAssistantText | --ocwi-bubble-assistant-text | '#1f2937' |
| ocwiInputBg | --ocwi-input-bg | '#ffffff' |
| ocwiSendBg | --ocwi-send-bg | '#2563eb' |
| ocwiFabBg | --ocwi-fab-bg | '#2563eb' |
| ocwiZIndex | --ocwi-z-index | 2147480000 |
| ocwiInputRows | (textarea rows) | 1 |
CSS variables can also be set directly on the host element from the outside:
document.querySelector('ocwi-chat').style.setProperty('--ocwi-primary', '#8b5cf6');HTTP (Dana) API contract
Embed code normally provides only api.lumaUrl:
window.OCWI('#chat', {
api: {
lumaUrl: 'https://luma.example.com/api/v1/config/<config_token>/'
}
});OCWI fetches that URL and expects Luma to return Dana's direct public chat URL:
{
"api": {
"danaUrl": "https://dana.example.com/api/chat/<public_key>",
"timeoutMs": 30000
}
}OCWI derives Dana endpoints from that chat URL:
| Purpose | Method | URL suffix |
| ------------- | ------ | ----------------------------------------------- |
| Health check | GET | /api/health |
| Send message | POST | /api/chat/<public_key> |
| Interrupt | POST | /api/chat/<public_key>/interrupt |
| Restore state | GET | /api/chat/<public_key>/conversations/<conversation_id>/messages |
When sending a message, the widget POSTs to the derived chat URL:
POST https://dana.example.com/api/chat/<public_key>
Content-Type: application/json
Accept: text/event-stream, application/json
{
"message": "Hello",
"conversation_id": null,
"session_id": null,
"client_message_id": "550e8400-e29b-41d4-a716-446655440000",
"stream": true,
"rewrite_from_message_id": null
}client_message_id is generated by OCWI for each user message and reused only for short retry/idempotency flows. The minimum compatible request body is { "message": "Hello" }.
The response is an SSE stream. The widget reads chunk, meta, done, and error events and accumulates chunks as the assistant message text. Dana signals errors via structured SSE events:
event: error
data: {"error": "DANA_LLM_UNAVAILABLE"}OCWI detects these events, aborts the stream, and displays a localised error message in the chat.
api.danaUrl may be a Dana origin (OCWI appends /api/chat) or the full public
chat URL ending in /api/chat/<public_key>. Luma proxy URLs under
/api/v1/proxy/ are intentionally rejected.
i18n
Built-in languages: en (default), cs, de.
window.OCWI('#chat', {
locale: 'cs',
i18n: {
dictionary: {
cs: { send: 'Pošli' } // override individual keys
}
}
});To add a new language entirely:
window.OCWI('#chat', {
locale: 'sk',
i18n: {
lang: 'sk',
dictionary: {
sk: {
send: 'Odoslať',
stop: 'Zastaviť'
// … add all keys (falls back to 'en' for missing ones)
}
}
}
});Accessibility
- Widget container has
role="region"andaria-labelwith the agent name - Textarea has
aria-label(uses placeholder text) - All icon buttons have
aria-label - Send button is focusable and keyboard-operable
Entersubmits (configurable),Escapeclears the input
Development
npm install # install deps
npm run build # → dist/ocwi.min.js
npm run dev # watch mode
npm run lint # ESLint
npm test # mobile embed hitbox regression
npm run format # PrettierBefore publishing a release, run npm test and verify that closed and minimized
mobile widgets do not intercept clicks outside the visible widget surface.
License
OCWI is distributed under the OCWI Sustainable Use License. Source available. Free for personal, non-commercial, and internal business use. Commercial redistribution or selling the widget as a standalone product requires a separate agreement.
