ocwi-core
v1.1.4
Published
OCWI – Embeddable AI Open Chat WIdget
Downloads
323
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)
▼
┌───────────────────────────────────────────────────────────┐
│ Luma (Django / DRF backend) https://cdn.jsdelivr.net/npm/ocwi-core/dist/ocwi.min.js │
│ Agent config, auth, SSE proxy to Dana │
└───────────────────────┬───────────────────────────────────┘
│ HTTP (internal network)
▼
┌───────────────────────────────────────────────────────────┐
│ 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 a cookie for instant subsequent loads. All chat messages are sent to Luma, which proxies them to Dana and streams the AI response back over SSE. Dana's URL is never exposed to the browser — Luma acts as the security boundary.
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, mounts it into the DOM and applies config.
| 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 | Luma proxy base URL returned by config, or an advanced direct chat URL. |
| 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
In the proxied setup, embed code only provides 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 the proxy base URL:
{
"api": {
"danaUrl": "https://luma.example.com/api/v1/proxy/<config_token>/",
"timeoutMs": 30000
}
}OCWI derives Luma proxy endpoints from that base URL:
| Purpose | Method | URL suffix |
| ------------- | ------ | ----------------------------------------------- |
| Health check | GET | api/health |
| Send message | POST | api/chat |
| Interrupt | POST | api/chat/interrupt |
| Restore state | GET | api/chat/conversations/<conversation_id>/messages |
When sending a message, the widget POSTs to the derived chat URL:
POST https://luma.example.com/api/v1/proxy/<config_token>/api/chat
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.
OCWI does not know Dana public_key in proxy mode. If api.danaUrl already ends with /api/chat or /api/chat/<key>, OCWI uses it as the chat URL; otherwise it treats api.danaUrl as a base URL and appends the Luma proxy suffixes above.
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.
