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.
- package/README.md +107 -0
- package/dist/adapters/react/ChatUI.d.ts.map +1 -1
- package/dist/adapters/react/ChatUI.js +29 -13
- package/dist/adapters/vanilla/createChatUI.d.ts +3 -0
- package/dist/adapters/vanilla/createChatUI.d.ts.map +1 -0
- package/dist/adapters/vanilla/createChatUI.js +292 -0
- package/dist/adapters/vanilla/event-handlers.d.ts +30 -0
- package/dist/adapters/vanilla/event-handlers.d.ts.map +1 -0
- package/dist/adapters/vanilla/event-handlers.js +251 -0
- package/dist/adapters/vanilla/index.d.ts +4 -37
- package/dist/adapters/vanilla/index.d.ts.map +1 -1
- package/dist/adapters/vanilla/index.js +2 -547
- package/dist/adapters/vanilla/message-actions.d.ts +17 -0
- package/dist/adapters/vanilla/message-actions.d.ts.map +1 -0
- package/dist/adapters/vanilla/message-actions.js +108 -0
- package/dist/adapters/vanilla/renderers.d.ts +17 -0
- package/dist/adapters/vanilla/renderers.d.ts.map +1 -0
- package/dist/adapters/vanilla/renderers.js +87 -0
- package/dist/adapters/vanilla/theme.d.ts +3 -0
- package/dist/adapters/vanilla/theme.d.ts.map +1 -0
- package/dist/adapters/vanilla/theme.js +14 -0
- package/dist/adapters/vanilla/types.d.ts +40 -0
- package/dist/adapters/vanilla/types.d.ts.map +1 -0
- package/dist/adapters/vanilla/types.js +1 -0
- package/dist/adapters/vanilla/utils.d.ts +13 -0
- package/dist/adapters/vanilla/utils.d.ts.map +1 -0
- package/dist/adapters/vanilla/utils.js +46 -0
- package/dist/browser/chatbot-ui.js +604 -282
- package/dist/browser/chatbot-ui.js.map +4 -4
- package/dist/browser/chatbot-ui.min.js +45 -49
- package/dist/src/LaravelIntegration.stories.d.ts +71 -0
- package/dist/src/LaravelIntegration.stories.d.ts.map +1 -0
- package/dist/src/LaravelIntegration.stories.js +335 -0
- 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,
|
|
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
|
|
43
|
-
const greetingAddedRef = React.useRef(
|
|
44
|
-
|
|
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
|
|
47
|
-
|
|
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
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
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 @@
|
|
|
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"}
|