@elia-assistant/chatui 1.0.26 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,20 +15,74 @@ Ships as a pre-built ESM bundle and a CDN-ready IIFE bundle. Config-compatible w
15
15
  npm install @elia-assistant/chatui
16
16
  ```
17
17
 
18
- ```ts
19
- // src/main.tsx
20
- import { App as ChatWidget } from '@elia-assistant/chatui'
21
- import { useSettingsStore } from '@elia-assistant/chatui/store'
18
+ ```tsx
19
+ // src/ChatWidget.tsx
20
+ import { useEffect, useRef } from 'react'
21
+ import { createChat, type ChatInstance } from '@elia-assistant/chatui'
22
+
23
+ export function ChatWidget() {
24
+ const containerRef = useRef<HTMLDivElement>(null)
25
+ const instanceRef = useRef<ChatInstance | null>(null)
26
+
27
+ useEffect(() => {
28
+ if (!containerRef.current || instanceRef.current) return
29
+ instanceRef.current = createChat({
30
+ target: containerRef.current,
31
+ webhookUrl: 'https://your-n8n.example.com/webhook/abc',
32
+ mode: 'window',
33
+ })
34
+ return () => {
35
+ // Deferred so StrictMode's double-invoke doesn't unmount mid-render.
36
+ const instance = instanceRef.current
37
+ instanceRef.current = null
38
+ setTimeout(() => instance?.unmount(), 0)
39
+ }
40
+ }, [])
41
+
42
+ return <div ref={containerRef} style={{ height: '100%' }} />
43
+ }
44
+ ```
45
+
46
+ `createChat()` mounts into a shadow root, so the widget's CSS is fully isolated from your app's.
47
+
48
+ The host's `vite.config.ts` needs `optimizeDeps.exclude: ['@elia-assistant/chatui']` so Vite doesn't re-bundle the already-bundled package. See [example.html](./example.html) for the full integration guide.
22
49
 
23
- useSettingsStore.getState().setConfig({
50
+ #### Raw component tree (no shadow root)
51
+
52
+ If you need the widget inside your own React tree rather than a shadow root, render `<App />` directly. It reads its stores and translations from context, so you must build them and supply both providers:
53
+
54
+ ```tsx
55
+ import { I18nextProvider } from 'react-i18next'
56
+ import {
57
+ App,
58
+ StoreProvider,
59
+ createChatI18n,
60
+ createSettingsStore,
61
+ createChatStore,
62
+ } from '@elia-assistant/chatui'
63
+
64
+ // Create these once, outside the component - not on every render.
65
+ const i18n = createChatI18n()
66
+ const settingsStore = createSettingsStore(i18n)
67
+ const chatStore = createChatStore()
68
+
69
+ settingsStore.getState().setConfig({
24
70
  webhookUrl: 'https://your-n8n.example.com/webhook/abc',
25
71
  mode: 'window',
26
72
  })
27
73
 
28
- // then render <ChatWidget /> inside a full-height container
74
+ export function ChatWidget() {
75
+ return (
76
+ <I18nextProvider i18n={i18n}>
77
+ <StoreProvider settingsStore={settingsStore} chatStore={chatStore}>
78
+ <App />
79
+ </StoreProvider>
80
+ </I18nextProvider>
81
+ )
82
+ }
29
83
  ```
30
84
 
31
- The host's `vite.config.ts` needs `optimizeDeps.exclude: ['@elia-assistant/chatui']` so Vite doesn't re-bundle the already-bundled package. See [example.html](./example.html) for the full integration guide.
85
+ Without both providers `<App />` throws `useSettingsStore must be used within a <StoreProvider>`. Note that this path does not inject the widget's CSS for you - prefer `createChat()` unless you specifically need to avoid the shadow root.
32
86
 
33
87
  ### Plain HTML (CDN, no bundler)
34
88
 
@@ -56,9 +110,24 @@ const instance = createChat({
56
110
  webhookUrl: '...',
57
111
  mode: 'window',
58
112
  })
59
- // instance.unmount() to remove
113
+
114
+ instance.setConfig({ botName: 'Support' }) // patch config without remounting
115
+ instance.setTheme('cosmos')
116
+ instance.setLanguage('sk')
117
+ instance.unmount() // remove
60
118
  ```
61
119
 
120
+ ### Multiple widgets on one page
121
+
122
+ Every `createChat()` call gets its own config, theme, language and conversation state, so two widgets never fight over each other's settings. They do share `localStorage`, though - pass `storageNamespace` to keep their persisted state apart:
123
+
124
+ ```ts
125
+ createChat({ target: '#support', webhookUrl: '...' })
126
+ createChat({ target: '#demo', webhookUrl: '...', storageNamespace: 'demo' })
127
+ ```
128
+
129
+ Omit `storageNamespace` for the single-widget case and the default storage keys are used, so existing installs keep their saved settings and history.
130
+
62
131
  ---
63
132
 
64
133
  ## Features
@@ -74,10 +143,11 @@ const instance = createChat({
74
143
  - **CTA popup** — timed speech-bubble with optional Web Audio notification, window mode only
75
144
  - **Conversation history** — optional sidebar with persistent multi-session history
76
145
  - **Per-language content** — initial messages, bot name, CTA text, welcome subtitle, tab titles, all configurable per language
77
- - **Multilingual UI** — English and Slovak bundled; extend via `i18next.addResourceBundle` at runtime
146
+ - **Multilingual UI** — English and Slovak bundled; extend via `instance.i18n.addResourceBundle` at runtime
78
147
  - **Markdown rendering** — bot messages render full GFM via `react-markdown`
79
148
  - **Configurable "Powered by" footer** — change the link text/URL or hide it entirely
80
- - **Persistent settings** — config + theme + language survive page reloads (`localStorage`)
149
+ - **Persistent settings** — theme + language + conversation history survive page reloads (`localStorage`); config is reset to *defaults + your `createChat()` options* on every mount, so your code stays the source of truth
150
+ - **Multi-instance safe** — every `createChat()` call is fully isolated; `storageNamespace` separates their persisted state too
81
151
  - **Lockable UI** — `hideSettings: true` removes the gear, theme picker, and settings modal (default for npm consumers)
82
152
  - **Export config** — generate ready-to-paste host code from the settings modal
83
153
 
@@ -85,7 +155,7 @@ const instance = createChat({
85
155
 
86
156
  ## Configuration
87
157
 
88
- All options live on `ChatConfig`. Set them once at boot via `setConfig()` (or pass to `createChat()`).
158
+ All options live on `ChatConfig`. Pass them to `createChat()`, or patch them later via `instance.setConfig()`.
89
159
 
90
160
  ### Required
91
161
 
@@ -160,7 +230,7 @@ A tab is shown only if its block has `feedUrl` or `items`. If neither tab is con
160
230
  | `initialMessages` | `string[]` | Global fallback initial bot messages |
161
231
  | `i18n[lang]` | `LangOverride` | Per-language content overrides |
162
232
 
163
- To set the initial UI language, call `useSettingsStore.getState().setLanguage('sk')` after `setConfig()` (or pass `language: 'sk'` to `createChat()`). On a fresh browser, the default falls back to `navigator.language` then `'en'`.
233
+ To set the initial UI language, pass `language: 'sk'` to `createChat()` (or call `instance.setLanguage('sk')` later). On a fresh browser, the default falls back to `navigator.language` then `'en'`.
164
234
 
165
235
  `LangOverride` fields: `initialMessages`, `ctaText`, `botName`, `welcomeSubtitle`, `tabs.{notifications,help,chat}.title`.
166
236
 
@@ -262,19 +332,22 @@ The FAQ tab includes a search box that filters by case-insensitive substring acr
262
332
  Bundled languages: `en`, `sk` (split into `dist/chunks/translation-*.js`, lazy-loaded).
263
333
  To add another without rebuilding chatui, register the bundle yourself:
264
334
 
335
+ Each instance owns a private i18next instance, exposed as `instance.i18n`. Register the bundle there - a bundle added to the global `i18next` singleton will not reach the widget:
336
+
265
337
  ```ts
266
- import i18n from 'i18next'
267
- import { useSettingsStore } from '@elia-assistant/chatui/store'
338
+ const instance = createChat({ target: '#chat', webhookUrl: '...' })
268
339
 
269
- i18n.addResourceBundle('fr', 'translation', {
340
+ instance.i18n?.addResourceBundle('fr', 'translation', {
270
341
  welcome: { subtitle: 'Commencez une conversation.' },
271
342
  input: { placeholder: 'Tapez un message...' },
272
343
  // ...full keys: see node_modules/@elia-assistant/chatui/dist/chunks/translation-*.js
273
344
  })
274
345
 
275
- useSettingsStore.getState().setLanguage('fr')
346
+ instance.setLanguage('fr')
276
347
  ```
277
348
 
349
+ `instance.i18n` is `undefined` only when `createChat()` was called before `DOMContentLoaded` and the widget has not mounted yet.
350
+
278
351
  Then any per-language overrides live under `config.i18n.fr`.
279
352
 
280
353
  ---
@@ -284,13 +357,14 @@ Then any per-language overrides live under `config.i18n.fr`.
284
357
  Hidden by default for npm consumers — configuration is expected to live in code. Flip `hideSettings: false` to expose the gear, theme picker, and settings modal (handy for admin dashboards, internal tools, live demos):
285
358
 
286
359
  ```ts
287
- useSettingsStore.getState().setConfig({
360
+ createChat({
361
+ target: '#chat',
288
362
  webhookUrl: '...',
289
363
  hideSettings: false,
290
364
  })
291
365
  ```
292
366
 
293
- The settings modal includes an **Export config** button that generates the exact `setConfig()` call for pasting into a host project.
367
+ The settings modal includes an **Export config** button that generates the exact `instance.setConfig()` call for pasting into a host project.
294
368
 
295
369
  The chatui repo's own `npm run dev` unlocks the UI automatically.
296
370
 
@@ -339,6 +413,26 @@ React 19 is a peer dependency for the ESM bundle. The IIFE bundle includes React
339
413
 
340
414
  ---
341
415
 
416
+ ## Migrating from 1.x
417
+
418
+ 2.0 removed the module-level global stores. Each `createChat()` call now owns its config, theme, language and conversation state, which is what makes multiple widgets on one page possible. `createChat()` itself is unchanged, so **CDN / `Chatui.createChat()` integrations need no changes** - and the default `localStorage` keys are unchanged, so saved settings and chat history carry over.
419
+
420
+ What did change:
421
+
422
+ | 1.x | 2.0 |
423
+ |---|---|
424
+ | `import { useSettingsStore } from '@elia-assistant/chatui/store'` | removed - use the `createChat()` instance |
425
+ | `useSettingsStore.getState().setConfig({...})` | `instance.setConfig({...})`, or pass the options to `createChat()` |
426
+ | `useSettingsStore.getState().setTheme(id)` | `instance.setTheme(id)` |
427
+ | `useSettingsStore.getState().setLanguage(lang)` | `instance.setLanguage(lang)` |
428
+ | `useChatStore` from `@elia-assistant/chatui/chat-store` | removed - no global chat store exists |
429
+ | `i18next.addResourceBundle(...)` on the global singleton | `instance.i18n?.addResourceBundle(...)` |
430
+ | `<App />` rendered directly | still supported, but must be wrapped in `<I18nextProvider>` + `<StoreProvider>` (see [Install](#raw-component-tree-no-shadow-root)) |
431
+
432
+ The `./store` and `./chat-store` subpath exports now expose only the `createSettingsStore` / `createChatStore` factories and their state types. Importing `useSettingsStore` from them fails at import time rather than silently returning something without `.getState()`.
433
+
434
+ ---
435
+
342
436
  ## License
343
437
 
344
438
  MIT - see [LICENSE](./LICENSE). Built by [Igor Demovic](https://github.com/idemovic).
package/dist/App.d.ts CHANGED
@@ -1,3 +1,2 @@
1
- import './i18n.ts';
2
1
  import './index.css';
3
2
  export declare function App(): import("react/jsx-runtime").JSX.Element;
@@ -1,149 +1,151 @@
1
- import { n as e, t } from "./chunks/middleware-Brf3TxqP.js";
1
+ import { n as e, r as t, t as n } from "./chunks/middleware-BG8DEvI8.js";
2
2
  //#region src/store/chatStore.ts
3
- function n() {
3
+ function r() {
4
4
  return Math.random().toString(36).slice(2) + Date.now().toString(36);
5
5
  }
6
- var r = e()(t((e, t) => ({
7
- sessions: [],
8
- activeSessionId: null,
9
- messages: {},
10
- isStreaming: !1,
11
- awaitingAgentReply: {},
12
- tokens: {},
13
- createSession() {
14
- let t = n(), r = {
15
- id: t,
16
- title: "New conversation",
17
- createdAt: Date.now()
18
- };
19
- return e((e) => ({
20
- sessions: [r, ...e.sessions],
21
- activeSessionId: t,
22
- messages: {
23
- ...e.messages,
24
- [t]: []
25
- }
26
- })), t;
27
- },
28
- setToken(t, n, r) {
29
- e((e) => ({ tokens: {
30
- ...e.tokens,
31
- [t]: {
32
- token: n,
33
- expiresAt: r
34
- }
35
- } }));
36
- },
37
- setActiveSession(t) {
38
- e({ activeSessionId: t });
39
- },
40
- addMessage(t, n) {
41
- e((e) => {
42
- let r = e.messages[t] ?? [];
43
- if (r.some((e) => e.id === n.id)) return {};
44
- let i = [...r, n];
45
- return {
46
- sessions: e.sessions.map((e) => e.id !== t || e.title !== "New conversation" || n.role !== "user" ? e : {
47
- ...e,
48
- title: n.content.slice(0, 50)
49
- }),
6
+ function i(i) {
7
+ return e()(n((e, t) => ({
8
+ sessions: [],
9
+ activeSessionId: null,
10
+ messages: {},
11
+ isStreaming: !1,
12
+ awaitingAgentReply: {},
13
+ tokens: {},
14
+ createSession() {
15
+ let t = r(), n = {
16
+ id: t,
17
+ title: "New conversation",
18
+ createdAt: Date.now()
19
+ };
20
+ return e((e) => ({
21
+ sessions: [n, ...e.sessions],
22
+ activeSessionId: t,
50
23
  messages: {
51
24
  ...e.messages,
52
- [t]: i
25
+ [t]: []
53
26
  }
54
- };
55
- });
56
- },
57
- appendToLastBot(t, n) {
58
- e((e) => {
59
- let r = e.messages[t] ?? [];
60
- if (r.length === 0) return {};
61
- let i = r[r.length - 1];
62
- if (i.role !== "bot") return {};
63
- let a = [...r.slice(0, -1), {
64
- ...i,
65
- content: i.content + n
66
- }];
67
- return { messages: {
68
- ...e.messages,
69
- [t]: a
70
- } };
71
- });
72
- },
73
- removeLastBotIfEmpty(t) {
74
- e((e) => {
75
- let n = e.messages[t] ?? [];
76
- if (n.length === 0) return {};
77
- let r = n[n.length - 1];
78
- return r.role !== "bot" || r.content !== "" ? {} : { messages: {
79
- ...e.messages,
80
- [t]: n.slice(0, -1)
81
- } };
82
- });
83
- },
84
- setStreaming(t) {
85
- e({ isStreaming: t });
86
- },
87
- setAwaitingAgentReply(t, n) {
88
- e((e) => {
89
- let r = { ...e.awaitingAgentReply };
90
- return n ? r[t] = !0 : delete r[t], { awaitingAgentReply: r };
91
- });
92
- },
93
- updateMessageStatus(t, n, r) {
94
- e((e) => {
95
- let i = e.messages[t];
96
- if (!i) return {};
97
- let a = !1, o = i.map((e) => e.id !== n || e.status === r ? e : (a = !0, {
98
- ...e,
99
- status: r
100
- }));
101
- return a ? { messages: {
102
- ...e.messages,
103
- [t]: o
104
- } } : {};
105
- });
106
- },
107
- deleteSession(t) {
108
- e((e) => {
109
- let n = e.sessions.filter((e) => e.id !== t), r = { ...e.messages };
110
- delete r[t];
111
- let i = { ...e.awaitingAgentReply };
112
- return delete i[t], {
113
- sessions: n,
114
- messages: r,
115
- awaitingAgentReply: i,
116
- activeSessionId: e.activeSessionId === t ? n[0]?.id ?? null : e.activeSessionId
117
- };
118
- });
119
- },
120
- renameSession(t, n) {
121
- e((e) => ({ sessions: e.sessions.map((e) => e.id === t ? {
122
- ...e,
123
- title: n
124
- } : e) }));
125
- },
126
- clearMessages(n) {
127
- e((e) => {
128
- let t = { ...e.awaitingAgentReply };
129
- return delete t[n], {
130
- messages: {
27
+ })), t;
28
+ },
29
+ setToken(t, n, r) {
30
+ e((e) => ({ tokens: {
31
+ ...e.tokens,
32
+ [t]: {
33
+ token: n,
34
+ expiresAt: r
35
+ }
36
+ } }));
37
+ },
38
+ setActiveSession(t) {
39
+ e({ activeSessionId: t });
40
+ },
41
+ addMessage(t, n) {
42
+ e((e) => {
43
+ let r = e.messages[t] ?? [];
44
+ if (r.some((e) => e.id === n.id)) return {};
45
+ let i = [...r, n];
46
+ return {
47
+ sessions: e.sessions.map((e) => e.id !== t || e.title !== "New conversation" || n.role !== "user" ? e : {
48
+ ...e,
49
+ title: n.content.slice(0, 50)
50
+ }),
51
+ messages: {
52
+ ...e.messages,
53
+ [t]: i
54
+ }
55
+ };
56
+ });
57
+ },
58
+ appendToLastBot(t, n) {
59
+ e((e) => {
60
+ let r = e.messages[t] ?? [];
61
+ if (r.length === 0) return {};
62
+ let i = r[r.length - 1];
63
+ if (i.role !== "bot") return {};
64
+ let a = [...r.slice(0, -1), {
65
+ ...i,
66
+ content: i.content + n
67
+ }];
68
+ return { messages: {
131
69
  ...e.messages,
132
- [n]: []
133
- },
134
- awaitingAgentReply: t
135
- };
136
- }), t().renameSession(n, "New conversation");
137
- }
138
- }), {
139
- name: "chatui-chat",
140
- partialize: (e) => ({
141
- sessions: e.sessions,
142
- activeSessionId: e.activeSessionId,
143
- messages: e.messages
144
- })
145
- }));
70
+ [t]: a
71
+ } };
72
+ });
73
+ },
74
+ removeLastBotIfEmpty(t) {
75
+ e((e) => {
76
+ let n = e.messages[t] ?? [];
77
+ if (n.length === 0) return {};
78
+ let r = n[n.length - 1];
79
+ return r.role !== "bot" || r.content !== "" ? {} : { messages: {
80
+ ...e.messages,
81
+ [t]: n.slice(0, -1)
82
+ } };
83
+ });
84
+ },
85
+ setStreaming(t) {
86
+ e({ isStreaming: t });
87
+ },
88
+ setAwaitingAgentReply(t, n) {
89
+ e((e) => {
90
+ let r = { ...e.awaitingAgentReply };
91
+ return n ? r[t] = !0 : delete r[t], { awaitingAgentReply: r };
92
+ });
93
+ },
94
+ updateMessageStatus(t, n, r) {
95
+ e((e) => {
96
+ let i = e.messages[t];
97
+ if (!i) return {};
98
+ let a = !1, o = i.map((e) => e.id !== n || e.status === r ? e : (a = !0, {
99
+ ...e,
100
+ status: r
101
+ }));
102
+ return a ? { messages: {
103
+ ...e.messages,
104
+ [t]: o
105
+ } } : {};
106
+ });
107
+ },
108
+ deleteSession(t) {
109
+ e((e) => {
110
+ let n = e.sessions.filter((e) => e.id !== t), r = { ...e.messages };
111
+ delete r[t];
112
+ let i = { ...e.awaitingAgentReply };
113
+ return delete i[t], {
114
+ sessions: n,
115
+ messages: r,
116
+ awaitingAgentReply: i,
117
+ activeSessionId: e.activeSessionId === t ? n[0]?.id ?? null : e.activeSessionId
118
+ };
119
+ });
120
+ },
121
+ renameSession(t, n) {
122
+ e((e) => ({ sessions: e.sessions.map((e) => e.id === t ? {
123
+ ...e,
124
+ title: n
125
+ } : e) }));
126
+ },
127
+ clearMessages(n) {
128
+ e((e) => {
129
+ let t = { ...e.awaitingAgentReply };
130
+ return delete t[n], {
131
+ messages: {
132
+ ...e.messages,
133
+ [n]: []
134
+ },
135
+ awaitingAgentReply: t
136
+ };
137
+ }), t().renameSession(n, "New conversation");
138
+ }
139
+ }), {
140
+ name: t("chatui-chat", i),
141
+ partialize: (e) => ({
142
+ sessions: e.sessions,
143
+ activeSessionId: e.activeSessionId,
144
+ messages: e.messages
145
+ })
146
+ }));
147
+ }
146
148
  //#endregion
147
- export { r as useChatStore };
149
+ export { i as createChatStore };
148
150
 
149
151
  //# sourceMappingURL=chat-store.js.map