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.
- package/dist/adapters/react/ChatUI.d.ts.map +1 -1
- package/dist/adapters/react/ChatUI.js +17 -2
- package/dist/adapters/vanilla/index.d.ts +7 -0
- package/dist/adapters/vanilla/index.d.ts.map +1 -1
- package/dist/adapters/vanilla/index.js +244 -21
- package/dist/browser/chatbot-ui.css +192 -0
- package/dist/browser/chatbot-ui.js +302 -20
- package/dist/browser/chatbot-ui.js.map +3 -3
- package/dist/browser/chatbot-ui.min.js +94 -36
- package/dist/core/src/ChatEngine.d.ts +5 -0
- package/dist/core/src/ChatEngine.d.ts.map +1 -1
- package/dist/core/src/ChatEngine.js +72 -0
- package/dist/core/src/types.d.ts +10 -0
- package/dist/core/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/styles/chat.css +192 -0
|
@@ -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,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
|
|
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
|
|
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;
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|