acecoderz-chat-ui 1.0.4 → 1.0.5

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.
@@ -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,CAgOxC,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,CAsPxC,CAAC"}
@@ -39,9 +39,24 @@ 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);
42
44
  // Add initial greeting message when component mounts
43
45
  React.useEffect(() => {
44
- if (initialGreeting && messages.length === 0) {
46
+ if (!initialGreeting) {
47
+ greetingAddedRef.current = null;
48
+ return;
49
+ }
50
+ // Check if there's already a greeting message with the same content
51
+ 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;
45
60
  const greetingMessage = {
46
61
  id: `greeting-${Date.now()}`,
47
62
  content: initialGreeting,
@@ -50,7 +65,7 @@ export const ChatUI = ({ config, theme = {}, className = '', placeholder = 'Type
50
65
  };
51
66
  addMessage(greetingMessage);
52
67
  }
53
- }, [initialGreeting, messages.length, addMessage]);
68
+ }, [initialGreeting, messages, addMessage]);
54
69
  const handleSubmit = (e) => {
55
70
  e.preventDefault();
56
71
  if (input.trim() && !isLoading && !disabled) {
@@ -18,6 +18,13 @@ export interface ChatUIOptions {
18
18
  loadingMessage?: string;
19
19
  customMessageRenderer?: (message: Message) => string;
20
20
  onInit?: (instance: ChatUIInstance) => void;
21
+ enableMessageActions?: boolean;
22
+ showCopyButton?: boolean;
23
+ showDeleteButton?: boolean;
24
+ showEditButton?: boolean;
25
+ onMessageCopy?: (message: Message) => void;
26
+ onMessageDelete?: (message: Message) => void;
27
+ onMessageEdit?: (message: Message, newContent: string) => void;
21
28
  }
22
29
  export interface ChatUIInstance {
23
30
  engine: ChatEngine;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../adapters/vanilla/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,WAAW,CAAC;CACjC;AA2DD,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,CAyUnE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../adapters/vanilla/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAE5C,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAChE;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,WAAW,CAAC;CACjC;AA2DD,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,CAwkBnE"}
@@ -47,7 +47,7 @@ function getContainerElement(container) {
47
47
  return container;
48
48
  }
49
49
  export function createChatUI(options) {
50
- 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, } = options;
50
+ 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, enableMessageActions = false, showCopyButton = true, showDeleteButton = true, showEditButton = true, onMessageCopy, onMessageDelete, onMessageEdit, } = options;
51
51
  // Get container element
52
52
  const containerElement = getContainerElement(container);
53
53
  const engine = new ChatEngine(config);
@@ -58,6 +58,8 @@ export function createChatUI(options) {
58
58
  let sendButtonElement = null;
59
59
  let clearButtonElement = null;
60
60
  let isUpdatingInputProgrammatically = false; // Flag to prevent input event loop
61
+ // Store event handlers for proper cleanup
62
+ const eventHandlers = [];
61
63
  // Apply CSS variables for theming
62
64
  const applyTheme = () => {
63
65
  const root = containerElement;
@@ -107,12 +109,59 @@ export function createChatUI(options) {
107
109
  const messageContent = enableMarkdown
108
110
  ? `<div class="chat-markdown">${formatText(message.content, enableMarkdown)}</div>`
109
111
  : `<div class="chat-message-text">${formatText(message.content, false)}</div>`;
112
+ const messageId = escapeHtml(message.id);
113
+ const messageActions = enableMessageActions ? `
114
+ <div class="chat-message-actions" role="group" aria-label="Message actions">
115
+ ${showCopyButton ? `
116
+ <button
117
+ class="chat-message-action chat-message-copy"
118
+ type="button"
119
+ aria-label="Copy message"
120
+ data-message-id="${messageId}"
121
+ data-action="copy"
122
+ title="Copy message"
123
+ >
124
+ <span aria-hidden="true">📋</span>
125
+ </button>
126
+ ` : ''}
127
+ ${showDeleteButton ? `
128
+ <button
129
+ class="chat-message-action chat-message-delete"
130
+ type="button"
131
+ aria-label="Delete message"
132
+ data-message-id="${messageId}"
133
+ data-action="delete"
134
+ title="Delete message"
135
+ >
136
+ <span aria-hidden="true">🗑️</span>
137
+ </button>
138
+ ` : ''}
139
+ ${showEditButton && isUser && !isSystem ? `
140
+ <button
141
+ class="chat-message-action chat-message-edit"
142
+ type="button"
143
+ aria-label="Edit message"
144
+ data-message-id="${messageId}"
145
+ data-action="edit"
146
+ title="Edit message"
147
+ >
148
+ <span aria-hidden="true">✏️</span>
149
+ </button>
150
+ ` : ''}
151
+ </div>
152
+ ` : '';
110
153
  return `
111
- <div class="chat-message ${isUser ? 'chat-message-user' : isSystem ? 'chat-message-system' : 'chat-message-assistant'}" data-message-id="${escapeHtml(message.id)}">
154
+ <div
155
+ class="chat-message ${isUser ? 'chat-message-user' : isSystem ? 'chat-message-system' : 'chat-message-assistant'}"
156
+ data-message-id="${messageId}"
157
+ role="article"
158
+ aria-label="${isUser ? 'Your message' : 'Assistant message'}"
159
+ >
112
160
  ${!isUser && !isSystem ? avatarHtml : ''}
113
161
  <div class="chat-message-content ${isUser ? 'chat-message-content-user' : isSystem ? 'chat-message-content-system' : 'chat-message-content-assistant'}">
114
162
  ${messageContent}
115
- ${showTimestamp ? `<div class="chat-message-timestamp">${formatTimestamp(message.timestamp)}</div>` : ''}
163
+ ${showTimestamp ? `<div class="chat-message-timestamp" aria-label="Sent at ${formatTimestamp(message.timestamp)}">${formatTimestamp(message.timestamp)}</div>` : ''}
164
+ ${messageActions}
116
165
  </div>
117
166
  ${isUser ? avatarHtml : ''}
118
167
  </div>
@@ -129,15 +178,24 @@ export function createChatUI(options) {
129
178
  applyTheme();
130
179
  containerElement.className = `chat-container ${containerElement.className || ''}`.trim();
131
180
  containerElement.setAttribute('data-chat-ui', 'true');
181
+ containerElement.setAttribute('role', 'log');
182
+ containerElement.setAttribute('aria-label', 'Chat conversation');
183
+ containerElement.setAttribute('aria-live', 'polite');
184
+ containerElement.setAttribute('aria-atomic', 'false');
132
185
  containerElement.innerHTML = `
133
- <div class="chat-messages-container">
186
+ <div class="chat-messages-container" role="log" aria-live="polite" aria-label="Chat messages">
134
187
  ${messages.length === 0 ? `<div class="chat-empty-state">${escapeHtml(emptyStateMessage)}</div>` : ''}
135
188
  ${messages.map(renderMessage).join('')}
136
189
  ${isLoading ? `
137
- <div class="chat-message chat-message-assistant chat-message-loading">
190
+ <div class="chat-message chat-message-assistant chat-message-loading" role="status" aria-live="polite" aria-label="Loading response">
138
191
  ${renderAvatar('assistant')}
139
192
  <div class="chat-message-content chat-message-content-assistant">
140
- <div class="chat-message-text">${escapeHtml(loadingMessage)}</div>
193
+ <div class="chat-message-text">
194
+ <span class="chat-loading-dots">
195
+ <span></span><span></span><span></span>
196
+ </span>
197
+ <span class="chat-loading-text">${escapeHtml(loadingMessage)}</span>
198
+ </div>
141
199
  </div>
142
200
  </div>
143
201
  ` : ''}
@@ -152,7 +210,15 @@ export function createChatUI(options) {
152
210
  </div>
153
211
  ` : ''}
154
212
  <div class="chat-input-container">
155
- <form class="chat-input-form">
213
+ <form
214
+ class="chat-input-form"
215
+ action="javascript:void(0)"
216
+ method="post"
217
+ novalidate
218
+ data-chat-form="true"
219
+ role="form"
220
+ aria-label="Chat input form"
221
+ >
156
222
  <input
157
223
  type="text"
158
224
  class="chat-input"
@@ -189,50 +255,202 @@ export function createChatUI(options) {
189
255
  }
190
256
  });
191
257
  }
192
- // Attach event listeners
258
+ // Attach event listeners with proper cleanup tracking
193
259
  if (formElement) {
194
- formElement.addEventListener('submit', (e) => {
195
- e.preventDefault();
260
+ // Use capture phase to ensure preventDefault runs before any other handlers
261
+ // This prevents page reloads even if parent elements have submit handlers
262
+ const handleSubmit = (e) => {
263
+ const submitEvent = e;
264
+ submitEvent.preventDefault();
265
+ submitEvent.stopPropagation();
196
266
  if (!disabled && !isLoading && input.trim()) {
197
267
  engine.sendMessage();
198
268
  }
199
- });
269
+ };
270
+ // Add handler in capture phase (runs first) to prevent any page reloads
271
+ formElement.addEventListener('submit', handleSubmit, true);
272
+ eventHandlers.push({ element: formElement, event: 'submit', handler: handleSubmit, options: true });
273
+ // Also add in bubble phase as fallback
274
+ formElement.addEventListener('submit', handleSubmit, false);
275
+ eventHandlers.push({ element: formElement, event: 'submit', handler: handleSubmit, options: false });
200
276
  }
201
277
  if (inputElement) {
202
278
  // Handle input changes
203
- inputElement.addEventListener('input', (e) => {
279
+ const handleInput = (e) => {
204
280
  // Skip if we're programmatically updating the input
205
281
  if (isUpdatingInputProgrammatically) {
206
282
  return;
207
283
  }
208
284
  const target = e.target;
209
285
  engine.setInput(target.value);
210
- });
286
+ };
287
+ inputElement.addEventListener('input', handleInput);
288
+ eventHandlers.push({ element: inputElement, event: 'input', handler: handleInput });
211
289
  // Keyboard shortcuts: Enter to send, Shift+Enter for newline
212
- inputElement.addEventListener('keydown', (e) => {
213
- if (e.key === 'Enter' && !e.shiftKey) {
214
- e.preventDefault();
290
+ const handleKeydown = (e) => {
291
+ const keyboardEvent = e;
292
+ if (keyboardEvent.key === 'Enter' && !keyboardEvent.shiftKey) {
293
+ keyboardEvent.preventDefault();
215
294
  if (!disabled && !isLoading && input.trim()) {
216
295
  engine.sendMessage();
217
296
  }
218
297
  }
219
298
  // Shift+Enter allows default behavior (newline if textarea, ignored if input)
220
- });
299
+ };
300
+ inputElement.addEventListener('keydown', handleKeydown);
301
+ eventHandlers.push({ element: inputElement, event: 'keydown', handler: handleKeydown });
221
302
  }
222
303
  // Clear button handler
223
304
  if (clearButtonElement) {
224
- clearButtonElement.addEventListener('click', () => {
305
+ const handleClearClick = () => {
225
306
  if (confirm('Are you sure you want to clear the conversation?')) {
226
307
  engine.clearMessages();
227
308
  }
228
- });
309
+ };
310
+ clearButtonElement.addEventListener('click', handleClearClick);
311
+ eventHandlers.push({ element: clearButtonElement, event: 'click', handler: handleClearClick });
229
312
  }
230
313
  // Retry button handler
231
314
  if (retryButton) {
232
- retryButton.addEventListener('click', () => {
315
+ const handleRetryClick = () => {
233
316
  engine.retryLastMessage();
234
- });
317
+ };
318
+ retryButton.addEventListener('click', handleRetryClick);
319
+ eventHandlers.push({ element: retryButton, event: 'click', handler: handleRetryClick });
320
+ }
321
+ // Message actions handlers (using event delegation)
322
+ if (enableMessageActions) {
323
+ const handleMessageAction = async (e) => {
324
+ const target = e.target;
325
+ const actionButton = target.closest('[data-action]');
326
+ if (!actionButton)
327
+ return;
328
+ e.preventDefault();
329
+ e.stopPropagation();
330
+ const action = actionButton.getAttribute('data-action');
331
+ const messageId = actionButton.getAttribute('data-message-id');
332
+ if (!messageId || !action)
333
+ return;
334
+ const message = engine.getMessages().find(m => m.id === messageId);
335
+ if (!message)
336
+ return;
337
+ switch (action) {
338
+ case 'copy':
339
+ await copyMessageToClipboard(message);
340
+ break;
341
+ case 'delete':
342
+ deleteMessage(message);
343
+ break;
344
+ case 'edit':
345
+ editMessage(message);
346
+ break;
347
+ }
348
+ };
349
+ containerElement.addEventListener('click', handleMessageAction);
350
+ eventHandlers.push({ element: containerElement, event: 'click', handler: handleMessageAction });
235
351
  }
352
+ // Toast notification helper
353
+ const showToast = (message, type = 'info') => {
354
+ const toast = document.createElement('div');
355
+ toast.className = `chat-toast chat-toast-${type}`;
356
+ toast.setAttribute('role', 'alert');
357
+ toast.setAttribute('aria-live', 'polite');
358
+ toast.textContent = message;
359
+ containerElement.appendChild(toast);
360
+ // Animate in
361
+ requestAnimationFrame(() => {
362
+ toast.classList.add('chat-toast-show');
363
+ });
364
+ // Remove after 3 seconds
365
+ setTimeout(() => {
366
+ toast.classList.remove('chat-toast-show');
367
+ setTimeout(() => toast.remove(), 300);
368
+ }, 3000);
369
+ };
370
+ const copyMessageToClipboard = async (message) => {
371
+ try {
372
+ await navigator.clipboard.writeText(message.content);
373
+ showToast('Message copied to clipboard', 'success');
374
+ onMessageCopy?.(message);
375
+ }
376
+ catch (err) {
377
+ console.error('Failed to copy:', err);
378
+ showToast('Failed to copy message', 'error');
379
+ }
380
+ };
381
+ const deleteMessage = (message) => {
382
+ if (confirm('Are you sure you want to delete this message?')) {
383
+ engine.deleteMessage(message.id);
384
+ showToast('Message deleted', 'info');
385
+ onMessageDelete?.(message);
386
+ }
387
+ };
388
+ const editMessage = (message) => {
389
+ const messageElement = containerElement.querySelector(`[data-message-id="${message.id}"]`);
390
+ if (!messageElement)
391
+ return;
392
+ const contentElement = messageElement.querySelector('.chat-message-text, .chat-markdown');
393
+ if (!contentElement)
394
+ return;
395
+ const originalContent = message.content;
396
+ const input = document.createElement('input');
397
+ input.type = 'text';
398
+ input.value = originalContent;
399
+ input.className = 'chat-message-edit-input';
400
+ input.setAttribute('aria-label', 'Edit message');
401
+ const buttonContainer = document.createElement('div');
402
+ buttonContainer.className = 'chat-message-edit-buttons';
403
+ const saveButton = document.createElement('button');
404
+ saveButton.textContent = 'Save';
405
+ saveButton.className = 'chat-message-edit-save';
406
+ saveButton.type = 'button';
407
+ saveButton.setAttribute('aria-label', 'Save changes');
408
+ const cancelButton = document.createElement('button');
409
+ cancelButton.textContent = 'Cancel';
410
+ cancelButton.className = 'chat-message-edit-cancel';
411
+ cancelButton.type = 'button';
412
+ cancelButton.setAttribute('aria-label', 'Cancel editing');
413
+ buttonContainer.appendChild(saveButton);
414
+ buttonContainer.appendChild(cancelButton);
415
+ const originalHTML = contentElement.innerHTML;
416
+ contentElement.innerHTML = '';
417
+ contentElement.appendChild(input);
418
+ contentElement.appendChild(buttonContainer);
419
+ input.focus();
420
+ input.select();
421
+ const handleSave = () => {
422
+ const newContent = input.value.trim();
423
+ if (newContent && newContent !== originalContent) {
424
+ try {
425
+ engine.editMessage(message.id, newContent);
426
+ showToast('Message updated', 'success');
427
+ onMessageEdit?.(message, newContent);
428
+ }
429
+ catch (error) {
430
+ showToast(error instanceof Error ? error.message : 'Failed to edit message', 'error');
431
+ contentElement.innerHTML = originalHTML;
432
+ }
433
+ }
434
+ else {
435
+ contentElement.innerHTML = originalHTML;
436
+ }
437
+ };
438
+ const handleCancel = () => {
439
+ contentElement.innerHTML = originalHTML;
440
+ };
441
+ saveButton.addEventListener('click', handleSave);
442
+ cancelButton.addEventListener('click', handleCancel);
443
+ input.addEventListener('keydown', (e) => {
444
+ if (e.key === 'Enter' && !e.shiftKey) {
445
+ e.preventDefault();
446
+ handleSave();
447
+ }
448
+ if (e.key === 'Escape') {
449
+ e.preventDefault();
450
+ handleCancel();
451
+ }
452
+ });
453
+ };
236
454
  // Auto-scroll to bottom after render
237
455
  // Use setTimeout to ensure DOM is updated
238
456
  setTimeout(() => {
@@ -301,6 +519,11 @@ export function createChatUI(options) {
301
519
  engine,
302
520
  render,
303
521
  destroy: () => {
522
+ // Remove all event listeners
523
+ eventHandlers.forEach(({ element, event, handler, options }) => {
524
+ element.removeEventListener(event, handler, options);
525
+ });
526
+ eventHandlers.length = 0;
304
527
  engine.destroy();
305
528
  containerElement.innerHTML = '';
306
529
  // Clear references
@@ -506,3 +506,195 @@
506
506
  /* This selector allows for custom styling via data attributes */
507
507
  }
508
508
 
509
+ /* Message Actions */
510
+ .chat-message-actions {
511
+ display: flex;
512
+ gap: 0.5rem;
513
+ margin-top: 0.5rem;
514
+ opacity: 0;
515
+ transition: opacity var(--chat-transition-duration, 0.2s);
516
+ }
517
+
518
+ .chat-message:hover .chat-message-actions {
519
+ opacity: 1;
520
+ }
521
+
522
+ .chat-message-action {
523
+ background: none;
524
+ border: none;
525
+ cursor: pointer;
526
+ padding: 0.25rem 0.5rem;
527
+ border-radius: var(--chat-border-radius, 0.25rem);
528
+ font-size: 0.875rem;
529
+ transition: background var(--chat-transition-duration, 0.2s);
530
+ display: inline-flex;
531
+ align-items: center;
532
+ justify-content: center;
533
+ min-width: 2rem;
534
+ min-height: 2rem;
535
+ }
536
+
537
+ .chat-message-action:hover {
538
+ background: color-mix(in srgb, var(--chat-secondary-color, #64748b) 10%, transparent);
539
+ }
540
+
541
+ .chat-message-action:focus {
542
+ outline: 2px solid var(--chat-primary-color, #3b82f6);
543
+ outline-offset: 2px;
544
+ }
545
+
546
+ .chat-message-action:active {
547
+ transform: scale(0.95);
548
+ }
549
+
550
+ /* Loading Animation */
551
+ .chat-loading-dots {
552
+ display: inline-flex;
553
+ gap: 0.25rem;
554
+ margin-right: 0.5rem;
555
+ }
556
+
557
+ .chat-loading-dots span {
558
+ width: 0.5rem;
559
+ height: 0.5rem;
560
+ border-radius: 50%;
561
+ background-color: var(--chat-secondary-color, #64748b);
562
+ animation: chat-loading-pulse 1.4s ease-in-out infinite;
563
+ }
564
+
565
+ .chat-loading-dots span:nth-child(1) {
566
+ animation-delay: 0s;
567
+ }
568
+
569
+ .chat-loading-dots span:nth-child(2) {
570
+ animation-delay: 0.2s;
571
+ }
572
+
573
+ .chat-loading-dots span:nth-child(3) {
574
+ animation-delay: 0.4s;
575
+ }
576
+
577
+ @keyframes chat-loading-pulse {
578
+ 0%, 80%, 100% {
579
+ opacity: 0.3;
580
+ transform: scale(0.8);
581
+ }
582
+ 40% {
583
+ opacity: 1;
584
+ transform: scale(1);
585
+ }
586
+ }
587
+
588
+ .chat-loading-text {
589
+ opacity: 0.7;
590
+ }
591
+
592
+ /* Toast Notifications */
593
+ .chat-toast {
594
+ position: fixed;
595
+ bottom: 1rem;
596
+ right: 1rem;
597
+ background: var(--chat-background-color, #ffffff);
598
+ color: var(--chat-text-color, #1e293b);
599
+ padding: 0.75rem 1rem;
600
+ border-radius: var(--chat-border-radius, 0.5rem);
601
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
602
+ z-index: 10000;
603
+ opacity: 0;
604
+ transform: translateY(1rem);
605
+ transition: opacity var(--chat-transition-duration, 0.2s), transform var(--chat-transition-duration, 0.2s);
606
+ max-width: 300px;
607
+ font-size: 0.875rem;
608
+ }
609
+
610
+ .chat-toast-show {
611
+ opacity: 1;
612
+ transform: translateY(0);
613
+ }
614
+
615
+ .chat-toast-success {
616
+ background: #d1fae5;
617
+ color: #065f46;
618
+ border-left: 3px solid #10b981;
619
+ }
620
+
621
+ .chat-toast-error {
622
+ background: #fee2e2;
623
+ color: #991b1b;
624
+ border-left: 3px solid #ef4444;
625
+ }
626
+
627
+ .chat-toast-info {
628
+ background: #dbeafe;
629
+ color: #1e40af;
630
+ border-left: 3px solid #3b82f6;
631
+ }
632
+
633
+ /* Message Edit Input */
634
+ .chat-message-edit-input {
635
+ width: 100%;
636
+ padding: 0.5rem;
637
+ border: 1px solid var(--chat-border-color, rgba(0, 0, 0, 0.1));
638
+ border-radius: var(--chat-border-radius, 0.5rem);
639
+ font-family: var(--chat-font-family, system-ui, sans-serif);
640
+ font-size: var(--chat-font-size, 1rem);
641
+ background: var(--chat-input-background-color, #f8fafc);
642
+ color: var(--chat-input-text-color, #1e293b);
643
+ margin-bottom: 0.5rem;
644
+ }
645
+
646
+ .chat-message-edit-input:focus {
647
+ outline: 2px solid var(--chat-primary-color, #3b82f6);
648
+ outline-offset: 2px;
649
+ }
650
+
651
+ .chat-message-edit-buttons {
652
+ display: flex;
653
+ gap: 0.5rem;
654
+ margin-top: 0.5rem;
655
+ }
656
+
657
+ .chat-message-edit-save,
658
+ .chat-message-edit-cancel {
659
+ padding: 0.375rem 0.75rem;
660
+ border-radius: var(--chat-border-radius, 0.5rem);
661
+ border: none;
662
+ cursor: pointer;
663
+ font-size: 0.875rem;
664
+ font-weight: 500;
665
+ transition: background var(--chat-transition-duration, 0.2s);
666
+ }
667
+
668
+ .chat-message-edit-save {
669
+ background: var(--chat-primary-color, #3b82f6);
670
+ color: white;
671
+ }
672
+
673
+ .chat-message-edit-save:hover {
674
+ background: color-mix(in srgb, var(--chat-primary-color, #3b82f6) 90%, black);
675
+ }
676
+
677
+ .chat-message-edit-cancel {
678
+ background: var(--chat-secondary-color, #64748b);
679
+ color: white;
680
+ }
681
+
682
+ .chat-message-edit-cancel:hover {
683
+ background: color-mix(in srgb, var(--chat-secondary-color, #64748b) 90%, black);
684
+ }
685
+
686
+ /* Accessibility Improvements */
687
+ .chat-container [role="status"] {
688
+ /* Ensure loading states are announced */
689
+ }
690
+
691
+ .chat-container [aria-live] {
692
+ /* Ensure dynamic content is announced */
693
+ }
694
+
695
+ .chat-message:focus-within {
696
+ outline: 2px solid var(--chat-primary-color, #3b82f6);
697
+ outline-offset: 2px;
698
+ border-radius: var(--chat-message-border-radius, var(--chat-border-radius, 0.5rem));
699
+ }
700
+