acecoderz-chat-ui 1.0.5 → 1.0.8

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.
Files changed (34) hide show
  1. package/README.md +107 -0
  2. package/dist/adapters/react/ChatUI.d.ts.map +1 -1
  3. package/dist/adapters/react/ChatUI.js +29 -13
  4. package/dist/adapters/vanilla/createChatUI.d.ts +3 -0
  5. package/dist/adapters/vanilla/createChatUI.d.ts.map +1 -0
  6. package/dist/adapters/vanilla/createChatUI.js +292 -0
  7. package/dist/adapters/vanilla/event-handlers.d.ts +30 -0
  8. package/dist/adapters/vanilla/event-handlers.d.ts.map +1 -0
  9. package/dist/adapters/vanilla/event-handlers.js +251 -0
  10. package/dist/adapters/vanilla/index.d.ts +4 -37
  11. package/dist/adapters/vanilla/index.d.ts.map +1 -1
  12. package/dist/adapters/vanilla/index.js +2 -547
  13. package/dist/adapters/vanilla/message-actions.d.ts +17 -0
  14. package/dist/adapters/vanilla/message-actions.d.ts.map +1 -0
  15. package/dist/adapters/vanilla/message-actions.js +108 -0
  16. package/dist/adapters/vanilla/renderers.d.ts +17 -0
  17. package/dist/adapters/vanilla/renderers.d.ts.map +1 -0
  18. package/dist/adapters/vanilla/renderers.js +87 -0
  19. package/dist/adapters/vanilla/theme.d.ts +3 -0
  20. package/dist/adapters/vanilla/theme.d.ts.map +1 -0
  21. package/dist/adapters/vanilla/theme.js +14 -0
  22. package/dist/adapters/vanilla/types.d.ts +40 -0
  23. package/dist/adapters/vanilla/types.d.ts.map +1 -0
  24. package/dist/adapters/vanilla/types.js +1 -0
  25. package/dist/adapters/vanilla/utils.d.ts +13 -0
  26. package/dist/adapters/vanilla/utils.d.ts.map +1 -0
  27. package/dist/adapters/vanilla/utils.js +46 -0
  28. package/dist/browser/chatbot-ui.js +604 -282
  29. package/dist/browser/chatbot-ui.js.map +4 -4
  30. package/dist/browser/chatbot-ui.min.js +45 -49
  31. package/dist/src/LaravelIntegration.stories.d.ts +71 -0
  32. package/dist/src/LaravelIntegration.stories.d.ts.map +1 -0
  33. package/dist/src/LaravelIntegration.stories.js +335 -0
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,10 @@ A framework-agnostic, fully customizable chat UI package that works with React,
11
11
  - 🎯 **TypeScript** - Full TypeScript support
12
12
  - 📦 **Modular** - Import only what you need
13
13
  - ✅ **Package Manager Compatible** - Works with both npm and pnpm
14
+ - 💾 **Message Persistence** - Automatic localStorage save/restore
15
+ - ✂️ **Message Actions** - Copy, delete, and edit messages
16
+ - ♿ **Accessible** - Full ARIA support and keyboard navigation
17
+ - 🎭 **Improved Loading** - Animated indicators with accessibility
14
18
 
15
19
  ## Installation
16
20
 
@@ -189,17 +193,26 @@ const container = document.getElementById('chat-container');
189
193
  const chatUI = createChatUI({
190
194
  config: {
191
195
  apiUrl: 'http://localhost:3000/api',
196
+ enablePersistence: true, // Optional: Save messages to localStorage
197
+ storageKey: 'my-chat-app', // Optional: Custom storage key
192
198
  },
193
199
  theme: {
194
200
  primaryColor: '#3b82f6',
195
201
  },
196
202
  container,
203
+ // Optional: Enable message actions (copy, delete, edit)
204
+ enableMessageActions: true,
205
+ showCopyButton: true,
206
+ showDeleteButton: true,
207
+ showEditButton: true,
197
208
  });
198
209
 
199
210
  // Later, to destroy:
200
211
  // chatUI.destroy();
201
212
  ```
202
213
 
214
+ **Note**: The vanilla adapter now handles form submission automatically - no workarounds needed! See [INTEGRATION_GUIDE.md](../../INTEGRATION_GUIDE.md) for complete integration examples.
215
+
203
216
  ### Core Only (Advanced)
204
217
 
205
218
  ```typescript
@@ -259,9 +272,103 @@ interface ChatConfig {
259
272
  onError?: (error: Error) => void; // Error callback
260
273
  enableWebSocket?: boolean; // Enable WebSocket
261
274
  websocketUrl?: string; // WebSocket URL
275
+ enablePersistence?: boolean; // Enable localStorage persistence (default: false)
276
+ storageKey?: string; // Storage key prefix (default: 'chat-ui')
262
277
  }
263
278
  ```
264
279
 
280
+ ## New Features
281
+
282
+ ### Message Persistence
283
+
284
+ Automatically save and restore conversations across page reloads:
285
+
286
+ ```javascript
287
+ const chatUI = createChatUI({
288
+ config: {
289
+ apiUrl: '/api/chat',
290
+ enablePersistence: true, // Enable localStorage
291
+ storageKey: 'my-app', // Optional: custom key
292
+ },
293
+ container: document.getElementById('chat'),
294
+ });
295
+ ```
296
+
297
+ ### Message Actions
298
+
299
+ Enable copy, delete, and edit buttons on messages:
300
+
301
+ ```javascript
302
+ const chatUI = createChatUI({
303
+ config: { apiUrl: '/api/chat' },
304
+ container: document.getElementById('chat'),
305
+ enableMessageActions: true,
306
+ showCopyButton: true,
307
+ showDeleteButton: true,
308
+ showEditButton: true,
309
+ onMessageCopy: (message) => console.log('Copied:', message.content),
310
+ onMessageDelete: (message) => console.log('Deleted:', message.id),
311
+ onMessageEdit: (message, newContent) => console.log('Edited:', newContent),
312
+ });
313
+ ```
314
+
315
+ ### Initial Greeting
316
+
317
+ Display a welcome message when the chat first opens:
318
+
319
+ **Vanilla JS:**
320
+ ```javascript
321
+ const chatUI = createChatUI({
322
+ config: { apiUrl: '/api/chat' },
323
+ container: document.getElementById('chat'),
324
+ initialGreeting: "Hello! 👋 I'm your assistant. How can I help you today?",
325
+ });
326
+ ```
327
+
328
+ **React:**
329
+ ```tsx
330
+ <ChatUI
331
+ config={config}
332
+ initialGreeting="Hello! 👋 I'm your assistant. How can I help you today?"
333
+ />
334
+ ```
335
+
336
+ The greeting will:
337
+ - Appear automatically when the chat opens
338
+ - Only show once per conversation
339
+ - Reappear after clearing the chat
340
+ - Not trigger an API call (it's a local message)
341
+
342
+ ### Improved Loading States
343
+
344
+ - Animated loading indicators
345
+ - Better accessibility with ARIA live regions
346
+ - Smooth transitions
347
+
348
+ See [NEW_FEATURES.md](./NEW_FEATURES.md) for complete feature documentation.
349
+
350
+ ## Integration Guide
351
+
352
+ For complete integration instructions, examples, and troubleshooting, see the [Integration Guide](../../INTEGRATION_GUIDE.md).
353
+
354
+ The integration guide includes:
355
+ - Complete vanilla JS examples with modal
356
+ - React integration examples
357
+ - Backend configuration
358
+ - Feature documentation
359
+ - Troubleshooting guide
360
+
361
+ ### Laravel Integration
362
+
363
+ For Laravel-specific integration with modal interface, event handling, and CORS configuration, see [LARAVEL_INTEGRATION.md](./LARAVEL_INTEGRATION.md).
364
+
365
+ The Laravel integration guide includes:
366
+ - Complete setup instructions
367
+ - Blade template examples
368
+ - Event handling for Laravel forms
369
+ - CORS configuration
370
+ - Troubleshooting common issues
371
+
265
372
  ## Customization
266
373
 
267
374
  ### Custom Message Renderer (React)
@@ -1 +1 @@
1
- {"version":3,"file":"ChatUI.d.ts","sourceRoot":"","sources":["../../../adapters/react/ChatUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAuD3E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9D,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAC5B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,MAAM,EAAE,MAAM,IAAI,CAAC;QACnB,QAAQ,EAAE,OAAO,CAAC;KACnB,KAAK,KAAK,CAAC,SAAS,CAAC;IAEtB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAsPxC,CAAC"}
1
+ {"version":3,"file":"ChatUI.d.ts","sourceRoot":"","sources":["../../../adapters/react/ChatUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAuD3E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9D,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAC5B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,MAAM,EAAE,MAAM,IAAI,CAAC;QACnB,QAAQ,EAAE,OAAO,CAAC;KACnB,KAAK,KAAK,CAAC,SAAS,CAAC;IAEtB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAsQxC,CAAC"}
@@ -39,24 +39,36 @@ const MarkdownContent = ({ content }) => {
39
39
  };
40
40
  export const ChatUI = ({ config, theme = {}, className = '', placeholder = 'Type your message...', showTimestamp = true, showAvatar = true, userAvatar, assistantAvatar, maxHeight = '600px', disabled = false, initialGreeting, messagesContainerClassName = '', messageClassName = '', inputContainerClassName = '', inputClassName = '', buttonClassName = '', errorClassName = '', emptyStateClassName = '', customMessageRenderer, customInputRenderer, dataAttributes = {}, }) => {
41
41
  const { messages, input, isLoading, error, sendMessage, setInput, addMessage, } = useChat(config);
42
- // Track if we've already added the initial greeting for the current initialGreeting value
43
- const greetingAddedRef = React.useRef(null);
44
- // Add initial greeting message when component mounts
42
+ // Track if we've already added the initial greeting
43
+ const greetingAddedRef = React.useRef(false);
44
+ const previousInitialGreetingRef = React.useRef(initialGreeting);
45
+ const messagesInitializedRef = React.useRef(false);
46
+ // Reset greeting flag if initialGreeting prop changes
47
+ if (previousInitialGreetingRef.current !== initialGreeting) {
48
+ greetingAddedRef.current = false;
49
+ previousInitialGreetingRef.current = initialGreeting;
50
+ }
51
+ // Mark when messages are initialized (loaded from storage or empty)
52
+ React.useEffect(() => {
53
+ if (messages.length >= 0) {
54
+ messagesInitializedRef.current = true;
55
+ }
56
+ }, [messages.length]);
57
+ // Add initial greeting message once when component mounts and messages are ready
45
58
  React.useEffect(() => {
46
- if (!initialGreeting) {
47
- greetingAddedRef.current = null;
59
+ // Skip if no greeting or already added
60
+ if (!initialGreeting || greetingAddedRef.current) {
61
+ return;
62
+ }
63
+ // Wait for messages to be initialized
64
+ if (!messagesInitializedRef.current) {
48
65
  return;
49
66
  }
50
67
  // Check if there's already a greeting message with the same content
51
68
  const hasGreeting = messages.some(msg => msg.role === 'assistant' && msg.content === initialGreeting);
52
- // Only add greeting if:
53
- // 1. We haven't already added this specific greeting (tracked by ref)
54
- // 2. There's no existing greeting message with the same content
55
- // 3. Messages array is empty
56
- if (greetingAddedRef.current !== initialGreeting &&
57
- !hasGreeting &&
58
- messages.length === 0) {
59
- greetingAddedRef.current = initialGreeting;
69
+ // Add greeting if it doesn't already exist
70
+ if (!hasGreeting) {
71
+ greetingAddedRef.current = true;
60
72
  const greetingMessage = {
61
73
  id: `greeting-${Date.now()}`,
62
74
  content: initialGreeting,
@@ -65,6 +77,10 @@ export const ChatUI = ({ config, theme = {}, className = '', placeholder = 'Type
65
77
  };
66
78
  addMessage(greetingMessage);
67
79
  }
80
+ else {
81
+ // If greeting already exists, mark as added
82
+ greetingAddedRef.current = true;
83
+ }
68
84
  }, [initialGreeting, messages, addMessage]);
69
85
  const handleSubmit = (e) => {
70
86
  e.preventDefault();
@@ -0,0 +1,3 @@
1
+ import type { ChatUIOptions, ChatUIInstance } from './types.js';
2
+ export declare function createChatUI(options: ChatUIOptions): ChatUIInstance;
3
+ //# sourceMappingURL=createChatUI.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createChatUI.d.ts","sourceRoot":"","sources":["../../../adapters/vanilla/createChatUI.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAOhE,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,CAgWnE"}
@@ -0,0 +1,292 @@
1
+ import { ChatEngine } from '../../core/src/ChatEngine';
2
+ import { escapeHtml, getContainerElement } from './utils.js';
3
+ import { applyTheme } from './theme.js';
4
+ import { renderAvatar, renderMessage } from './renderers.js';
5
+ import { createMessageActionHelpers } from './message-actions.js';
6
+ import { attachEventHandlers } from './event-handlers.js';
7
+ export function createChatUI(options) {
8
+ console.log('[ChatUI] createChatUI called', { container: options.container });
9
+ const { config, theme = {}, container, placeholder = 'Type your message...', showTimestamp = true, showAvatar = true, userAvatar, assistantAvatar, maxHeight = '600px', disabled = false, autoScroll = true, enableMarkdown = true, showClearButton = true, emptyStateMessage = 'Start a conversation...', loadingMessage = 'Thinking...', customMessageRenderer, onInit, initialGreeting, enableMessageActions = false, showCopyButton = true, showDeleteButton = true, showEditButton = true, onMessageCopy, onMessageDelete, onMessageEdit, stopPropagationRoot, } = options;
10
+ const containerElement = getContainerElement(container);
11
+ console.log('[ChatUI] Container element found', {
12
+ containerElement: !!containerElement,
13
+ containerId: containerElement.id,
14
+ });
15
+ const engine = new ChatEngine(config);
16
+ let messagesContainer = null;
17
+ let inputElement = null;
18
+ let inputWrapperElement = null;
19
+ let sendButtonElement = null;
20
+ let clearButtonElement = null;
21
+ const isUpdatingInputProgrammatically = { current: false };
22
+ let greetingAdded = false;
23
+ let previousInitialGreeting = initialGreeting;
24
+ let messagesInitialized = false;
25
+ const eventHandlers = [];
26
+ const applyThemeToContainer = () => {
27
+ applyTheme(containerElement, theme, maxHeight);
28
+ };
29
+ const scrollToBottom = () => {
30
+ if (messagesContainer && autoScroll) {
31
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
32
+ }
33
+ };
34
+ const updateSendButtonState = () => {
35
+ if (sendButtonElement && inputElement) {
36
+ const domInput = inputElement.value || '';
37
+ const isLoading = engine.getIsLoading();
38
+ const shouldDisable = disabled || isLoading || !domInput.trim();
39
+ console.log('[ChatUI] updateSendButtonState', {
40
+ domInput,
41
+ domInputLength: domInput.length,
42
+ isLoading,
43
+ disabled,
44
+ shouldDisable,
45
+ });
46
+ if (shouldDisable) {
47
+ sendButtonElement.disabled = true;
48
+ }
49
+ else {
50
+ sendButtonElement.disabled = false;
51
+ sendButtonElement.removeAttribute('disabled');
52
+ }
53
+ }
54
+ };
55
+ const renderOpts = {
56
+ showAvatar,
57
+ userAvatar,
58
+ assistantAvatar,
59
+ enableMarkdown,
60
+ showTimestamp,
61
+ enableMessageActions,
62
+ showCopyButton,
63
+ showDeleteButton,
64
+ showEditButton,
65
+ customMessageRenderer,
66
+ };
67
+ const messageActions = enableMessageActions
68
+ ? createMessageActionHelpers(containerElement, engine, {
69
+ onMessageCopy,
70
+ onMessageDelete,
71
+ onMessageEdit,
72
+ })
73
+ : undefined;
74
+ const render = () => {
75
+ console.log('[ChatUI] RENDER FUNCTION CALLED - THIS SHOULD APPEAR');
76
+ const messages = engine.getMessages();
77
+ const input = engine.getInput();
78
+ const isLoading = engine.getIsLoading();
79
+ const error = engine.getError();
80
+ const wasInputFocused = inputElement && document.activeElement === inputElement;
81
+ const inputCursorPosition = inputElement ? inputElement.selectionStart || 0 : 0;
82
+ applyThemeToContainer();
83
+ containerElement.className = `chat-container ${containerElement.className || ''}`.trim();
84
+ containerElement.setAttribute('data-chat-ui', 'true');
85
+ containerElement.setAttribute('role', 'log');
86
+ containerElement.setAttribute('aria-label', 'Chat conversation');
87
+ containerElement.setAttribute('aria-live', 'polite');
88
+ containerElement.setAttribute('aria-atomic', 'false');
89
+ containerElement.innerHTML = `
90
+ <div class="chat-messages-container" role="log" aria-live="polite" aria-label="Chat messages">
91
+ ${messages.length === 0 ? `<div class="chat-empty-state">${escapeHtml(emptyStateMessage)}</div>` : ''}
92
+ ${messages.map((m) => renderMessage(m, renderOpts)).join('')}
93
+ ${isLoading ? `
94
+ <div class="chat-message chat-message-assistant chat-message-loading" role="status" aria-live="polite" aria-label="Loading response">
95
+ ${renderAvatar('assistant', renderOpts)}
96
+ <div class="chat-message-content chat-message-content-assistant">
97
+ <div class="chat-message-text">
98
+ <span class="chat-loading-dots">
99
+ <span></span><span></span><span></span>
100
+ </span>
101
+ <span class="chat-loading-text">${escapeHtml(loadingMessage)}</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ ` : ''}
106
+ </div>
107
+ ${error ? `<div class="chat-error">
108
+ <div class="chat-error-message">Error: ${escapeHtml(error.message)}</div>
109
+ <button class="chat-retry-button" type="button">Retry</button>
110
+ </div>` : ''}
111
+ ${showClearButton && messages.length > 0 ? `
112
+ <div class="chat-actions">
113
+ <button class="chat-clear-button" type="button" title="Clear conversation">Clear</button>
114
+ </div>
115
+ ` : ''}
116
+ <div class="chat-input-container">
117
+ <div
118
+ class="chat-input-form"
119
+ data-chat-form="true"
120
+ role="form"
121
+ aria-label="Chat input form"
122
+ >
123
+ <input
124
+ type="text"
125
+ class="chat-input"
126
+ value="${escapeHtml(input)}"
127
+ placeholder="${escapeHtml(placeholder)}"
128
+ ${disabled || isLoading ? 'disabled' : ''}
129
+ aria-label="Chat input"
130
+ />
131
+ <button
132
+ type="button"
133
+ class="chat-send-button"
134
+ aria-label="Send message"
135
+ >
136
+ Send
137
+ </button>
138
+ </div>
139
+ </div>
140
+ `;
141
+ messagesContainer = containerElement.querySelector('.chat-messages-container');
142
+ inputElement = containerElement.querySelector('.chat-input');
143
+ inputWrapperElement = containerElement.querySelector('.chat-input-form');
144
+ sendButtonElement = containerElement.querySelector('.chat-send-button');
145
+ clearButtonElement = containerElement.querySelector('.chat-clear-button');
146
+ const retryButton = containerElement.querySelector('.chat-retry-button');
147
+ console.log('[ChatUI] Element references stored', {
148
+ hasMessagesContainer: !!messagesContainer,
149
+ hasInputElement: !!inputElement,
150
+ hasSendButtonElement: !!sendButtonElement,
151
+ sendButtonElement: sendButtonElement,
152
+ sendButtonClasses: sendButtonElement?.className,
153
+ sendButtonDisabled: sendButtonElement?.disabled,
154
+ containerHTML: containerElement.innerHTML.substring(0, 200),
155
+ });
156
+ setTimeout(() => {
157
+ updateSendButtonState();
158
+ }, 0);
159
+ if (wasInputFocused && inputElement) {
160
+ requestAnimationFrame(() => {
161
+ if (inputElement) {
162
+ inputElement.focus();
163
+ const newCursorPosition = Math.min(inputCursorPosition, inputElement.value.length);
164
+ inputElement.setSelectionRange(newCursorPosition, newCursorPosition);
165
+ }
166
+ });
167
+ }
168
+ eventHandlers.length = 0;
169
+ attachEventHandlers({
170
+ containerElement,
171
+ engine,
172
+ disabled,
173
+ placeholder,
174
+ enableMessageActions,
175
+ stopPropagationRoot,
176
+ refs: {
177
+ inputElement,
178
+ sendButtonElement,
179
+ clearButtonElement,
180
+ retryButton,
181
+ },
182
+ isUpdatingInputProgrammatically,
183
+ updateSendButtonState,
184
+ eventHandlers,
185
+ messageActions,
186
+ });
187
+ setTimeout(() => {
188
+ scrollToBottom();
189
+ }, 0);
190
+ };
191
+ const addInitialGreetingIfNeeded = () => {
192
+ if (!initialGreeting || greetingAdded) {
193
+ return;
194
+ }
195
+ if (previousInitialGreeting !== initialGreeting) {
196
+ greetingAdded = false;
197
+ previousInitialGreeting = initialGreeting;
198
+ }
199
+ const messages = engine.getMessages();
200
+ const hasGreeting = messages.some((msg) => msg.role === 'assistant' && msg.content === initialGreeting);
201
+ if (!hasGreeting) {
202
+ greetingAdded = true;
203
+ const greetingMessage = {
204
+ id: `greeting-${Date.now()}`,
205
+ content: initialGreeting,
206
+ role: 'assistant',
207
+ timestamp: new Date(),
208
+ };
209
+ engine.addMessage(greetingMessage);
210
+ }
211
+ else {
212
+ greetingAdded = true;
213
+ }
214
+ };
215
+ engine.on('messagesChange', () => {
216
+ if (!messagesInitialized) {
217
+ messagesInitialized = true;
218
+ addInitialGreetingIfNeeded();
219
+ if (greetingAdded) {
220
+ return;
221
+ }
222
+ }
223
+ render();
224
+ setTimeout(() => {
225
+ scrollToBottom();
226
+ }, 100);
227
+ });
228
+ engine.on('inputChange', (value) => {
229
+ if (inputElement && inputElement.isConnected) {
230
+ const wasFocused = document.activeElement === inputElement;
231
+ const cursorPosition = inputElement.selectionStart || 0;
232
+ isUpdatingInputProgrammatically.current = true;
233
+ inputElement.value = value || '';
234
+ isUpdatingInputProgrammatically.current = false;
235
+ updateSendButtonState();
236
+ if (wasFocused) {
237
+ requestAnimationFrame(() => {
238
+ if (inputElement) {
239
+ inputElement.focus();
240
+ const newCursorPosition = Math.min(cursorPosition, inputElement.value.length);
241
+ inputElement.setSelectionRange(newCursorPosition, newCursorPosition);
242
+ }
243
+ });
244
+ }
245
+ }
246
+ else {
247
+ render();
248
+ }
249
+ });
250
+ engine.on('loadingChange', () => {
251
+ updateSendButtonState();
252
+ render();
253
+ setTimeout(() => {
254
+ scrollToBottom();
255
+ }, 100);
256
+ });
257
+ engine.on('error', render);
258
+ render();
259
+ setTimeout(() => {
260
+ if (!messagesInitialized) {
261
+ messagesInitialized = true;
262
+ addInitialGreetingIfNeeded();
263
+ }
264
+ }, 100);
265
+ const instance = {
266
+ engine,
267
+ render,
268
+ destroy: () => {
269
+ eventHandlers.forEach(({ element, event, handler, options }) => {
270
+ element.removeEventListener(event, handler, options);
271
+ });
272
+ eventHandlers.length = 0;
273
+ engine.destroy();
274
+ containerElement.innerHTML = '';
275
+ messagesContainer = null;
276
+ inputElement = null;
277
+ inputWrapperElement = null;
278
+ sendButtonElement = null;
279
+ clearButtonElement = null;
280
+ },
281
+ clear: () => {
282
+ greetingAdded = false;
283
+ engine.clearMessages();
284
+ },
285
+ scrollToBottom,
286
+ getContainer: () => containerElement,
287
+ };
288
+ if (onInit) {
289
+ onInit(instance);
290
+ }
291
+ return instance;
292
+ }
@@ -0,0 +1,30 @@
1
+ import type { ChatEngine } from '../../core/src/ChatEngine';
2
+ import type { MessageActionHelpers } from './message-actions.js';
3
+ export type EventHandlerEntry = {
4
+ element: HTMLElement;
5
+ event: string;
6
+ handler: EventListener;
7
+ options?: boolean | AddEventListenerOptions;
8
+ };
9
+ export interface EventHandlerContext {
10
+ containerElement: HTMLElement;
11
+ engine: ChatEngine;
12
+ disabled: boolean;
13
+ placeholder: string;
14
+ enableMessageActions: boolean;
15
+ stopPropagationRoot?: HTMLElement;
16
+ refs: {
17
+ inputElement: HTMLInputElement | null;
18
+ sendButtonElement: HTMLButtonElement | null;
19
+ clearButtonElement: HTMLButtonElement | null;
20
+ retryButton: HTMLButtonElement | null;
21
+ };
22
+ isUpdatingInputProgrammatically: {
23
+ current: boolean;
24
+ };
25
+ updateSendButtonState: () => void;
26
+ eventHandlers: EventHandlerEntry[];
27
+ messageActions?: MessageActionHelpers;
28
+ }
29
+ export declare function attachEventHandlers(ctx: EventHandlerContext): void;
30
+ //# sourceMappingURL=event-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-handlers.d.ts","sourceRoot":"","sources":["../../../adapters/vanilla/event-handlers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAC;CAC7C,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,WAAW,CAAC;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,mBAAmB,CAAC,EAAE,WAAW,CAAC;IAClC,IAAI,EAAE;QACJ,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;QACtC,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;QAC5C,kBAAkB,EAAE,iBAAiB,GAAG,IAAI,CAAC;QAC7C,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;KACvC,CAAC;IACF,+BAA+B,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACtD,qBAAqB,EAAE,MAAM,IAAI,CAAC;IAClC,aAAa,EAAE,iBAAiB,EAAE,CAAC;IACnC,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,CAmSlE"}