Created
October 2, 2025 10:16
-
-
Save defmans7/a3acb295c050f9aa35f6ee6fdd08c8f7 to your computer and use it in GitHub Desktop.
Improved Chat Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Chat Widget Script | |
| (function() { | |
| // Utility: Merge configs deeply | |
| function deepMerge(target, source) { | |
| for (const key in source) { | |
| if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { | |
| target[key] = deepMerge(target[key] || {}, source[key]); | |
| } else { | |
| target[key] = source[key]; | |
| } | |
| } | |
| return target; | |
| } | |
| // Utility: Create element with classes and attributes | |
| function createElement(tag, options = {}) { | |
| const el = document.createElement(tag); | |
| if (options.className) el.className = options.className; | |
| if (options.attrs) { | |
| Object.entries(options.attrs).forEach(([k, v]) => el.setAttribute(k, v)); | |
| } | |
| if (options.html) el.innerHTML = options.html; | |
| return el; | |
| } | |
| // Inject font and styles only once | |
| function injectFontAndStyles(styles) { | |
| if (!document.getElementById('n8n-chat-font')) { | |
| const fontLink = createElement('link', { | |
| attrs: { | |
| rel: 'stylesheet', | |
| href: 'https://cdn.jsdelivr.net/npm/geist@1.0.0/dist/fonts/geist-sans/style.css', | |
| id: 'n8n-chat-font' | |
| } | |
| }); | |
| document.head.appendChild(fontLink); | |
| } | |
| if (!document.getElementById('n8n-chat-style')) { | |
| const styleSheet = createElement('style', { | |
| attrs: { id: 'n8n-chat-style' }, | |
| html: styles | |
| }); | |
| document.head.appendChild(styleSheet); | |
| } | |
| } | |
| const styles = ` | |
| .n8n-chat-widget { | |
| --chat--color-primary: var(--n8n-chat-primary-color, #854fff); | |
| --chat--color-secondary: var(--n8n-chat-secondary-color, #6b3fd4); | |
| --chat--color-background: var(--n8n-chat-background-color, #ffffff); | |
| --chat--color-font: var(--n8n-chat-font-color, #333333); | |
| font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |
| } | |
| .n8n-chat-widget .chat-container { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 1000; | |
| display: none; | |
| width: 380px; | |
| height: 600px; | |
| background: var(--chat--color-background); | |
| border-radius: 12px; | |
| box-shadow: 0 8px 32px rgba(133, 79, 255, 0.15); | |
| border: 1px solid rgba(133, 79, 255, 0.2); | |
| overflow: hidden; | |
| font-family: inherit; | |
| } | |
| .n8n-chat-widget .chat-container.position-left { | |
| right: auto; | |
| left: 20px; | |
| } | |
| .n8n-chat-widget .chat-container.open { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .n8n-chat-widget .brand-header { | |
| padding: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| border-bottom: 1px solid rgba(133, 79, 255, 0.1); | |
| position: relative; | |
| } | |
| .n8n-chat-widget .close-button { | |
| position: absolute; | |
| right: 16px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| background: none; | |
| border: none; | |
| color: var(--chat--color-font); | |
| cursor: pointer; | |
| padding: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: color 0.2s; | |
| font-size: 20px; | |
| opacity: 0.6; | |
| } | |
| .n8n-chat-widget .close-button:hover { | |
| opacity: 1; | |
| } | |
| .n8n-chat-widget .brand-header img { | |
| width: 32px; | |
| height: 32px; | |
| } | |
| .n8n-chat-widget .brand-header span { | |
| font-size: 18px; | |
| font-weight: 500; | |
| color: var(--chat--color-font); | |
| } | |
| .n8n-chat-widget .new-conversation { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| padding: 20px; | |
| text-align: center; | |
| width: 100%; | |
| max-width: 300px; | |
| } | |
| .n8n-chat-widget .welcome-text { | |
| font-size: 24px; | |
| font-weight: 600; | |
| color: var(--chat--color-font); | |
| margin-bottom: 24px; | |
| line-height: 1.3; | |
| } | |
| .n8n-chat-widget .new-chat-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| width: 100%; | |
| padding: 16px 24px; | |
| background: linear-gradient(135deg, var(--chat--color-primary) 0%, var(--chat--color-secondary) 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: transform 0.3s; | |
| font-weight: 500; | |
| font-family: inherit; | |
| margin-bottom: 12px; | |
| } | |
| .n8n-chat-widget .new-chat-btn:hover { | |
| transform: scale(1.02); | |
| } | |
| .n8n-chat-widget .message-icon { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .n8n-chat-widget .response-text { | |
| font-size: 14px; | |
| color: var(--chat--color-font); | |
| opacity: 0.7; | |
| margin: 0; | |
| } | |
| .n8n-chat-widget .chat-interface { | |
| display: none; | |
| flex-direction: column; | |
| height: 100%; | |
| } | |
| .n8n-chat-widget .chat-interface.active { | |
| display: flex; | |
| } | |
| .n8n-chat-widget .chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| background: var(--chat--color-background); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .n8n-chat-widget .chat-message { | |
| padding: 12px 16px; | |
| margin: 8px 0; | |
| border-radius: 12px; | |
| max-width: 80%; | |
| word-wrap: break-word; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| .n8n-chat-widget .chat-message.user { | |
| background: linear-gradient(135deg, var(--chat--color-primary) 0%, var(--chat--color-secondary) 100%); | |
| color: white; | |
| align-self: flex-end; | |
| box-shadow: 0 4px 12px rgba(133, 79, 255, 0.2); | |
| border: none; | |
| } | |
| .n8n-chat-widget .chat-message.bot { | |
| background: var(--chat--color-background); | |
| border: 1px solid rgba(133, 79, 255, 0.2); | |
| color: var(--chat--color-font); | |
| align-self: flex-start; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
| } | |
| .n8n-chat-widget .chat-input { | |
| padding: 16px; | |
| background: var(--chat--color-background); | |
| border-top: 1px solid rgba(133, 79, 255, 0.1); | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .n8n-chat-widget .chat-input textarea { | |
| flex: 1; | |
| padding: 12px; | |
| border: 1px solid rgba(133, 79, 255, 0.2); | |
| border-radius: 8px; | |
| background: var(--chat--color-background); | |
| color: var(--chat--color-font); | |
| resize: none; | |
| font-family: inherit; | |
| font-size: 14px; | |
| } | |
| .n8n-chat-widget .chat-input textarea::placeholder { | |
| color: var(--chat--color-font); | |
| opacity: 0.6; | |
| } | |
| .n8n-chat-widget .chat-input button { | |
| background: linear-gradient(135deg, var(--chat--color-primary) 0%, var(--chat--color-secondary) 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| padding: 0 20px; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| font-family: inherit; | |
| font-weight: 500; | |
| } | |
| .n8n-chat-widget .chat-input button:hover { | |
| transform: scale(1.05); | |
| } | |
| .n8n-chat-widget .chat-toggle { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 30px; | |
| background: linear-gradient(135deg, var(--chat--color-primary) 0%, var(--chat--color-secondary) 100%); | |
| color: white; | |
| border: none; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(133, 79, 255, 0.3); | |
| z-index: 999; | |
| transition: transform 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .n8n-chat-widget .chat-toggle.position-left { | |
| right: auto; | |
| left: 20px; | |
| } | |
| .n8n-chat-widget .chat-toggle:hover { | |
| transform: scale(1.05); | |
| } | |
| .n8n-chat-widget .chat-toggle svg { | |
| width: 24px; | |
| height: 24px; | |
| fill: currentColor; | |
| } | |
| .n8n-chat-widget .chat-footer { | |
| padding: 8px; | |
| text-align: center; | |
| background: var(--chat--color-background); | |
| border-top: 1px solid rgba(133, 79, 255, 0.1); | |
| } | |
| .n8n-chat-widget .chat-footer a { | |
| color: var(--chat--color-primary); | |
| text-decoration: none; | |
| font-size: 12px; | |
| opacity: 0.8; | |
| transition: opacity 0.2s; | |
| font-family: inherit; | |
| } | |
| .n8n-chat-widget .chat-footer a:hover { | |
| opacity: 1; | |
| } | |
| `; | |
| injectFontAndStyles(styles + ` | |
| .thinking-indicator .dots { | |
| display: inline-block; | |
| font-size: 20px; | |
| letter-spacing: 2px; | |
| } | |
| .thinking-indicator .dots span { | |
| display: inline-block; | |
| animation: wave 1s infinite; | |
| transform-origin: bottom center; | |
| } | |
| .thinking-indicator .dots span:nth-child(1) { | |
| animation-delay: 0s; | |
| } | |
| .thinking-indicator .dots span:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .thinking-indicator .dots span:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes wave { | |
| 0%, 60%, 100% { | |
| transform: translateY(0); | |
| opacity: 0.5; | |
| } | |
| 30% { | |
| transform: translateY(-8px); | |
| opacity: 1; | |
| } | |
| } | |
| .thinking-word { | |
| font-style: italic; | |
| color: #854fff; | |
| opacity: 0.8; | |
| font-size: 16px; | |
| margin-bottom: 0; | |
| margin-top: 0; | |
| align-self: flex-start; | |
| background: none; | |
| border: none; | |
| box-shadow: none; | |
| } | |
| `); | |
| // Default configuration | |
| const defaultConfig = { | |
| webhook: { | |
| url: '', | |
| route: '' | |
| }, | |
| branding: { | |
| logo: '', | |
| name: '', | |
| welcomeText: '', | |
| responseTimeText: '', | |
| poweredBy: { | |
| text: 'Powered by n8n', | |
| link: 'https://n8n.partnerlinks.io/m8a94i19zhqq?utm_source=nocodecreative.io' | |
| } | |
| }, | |
| style: { | |
| primaryColor: '#854fff', | |
| secondaryColor: '#6b3fd4', | |
| position: 'right', | |
| backgroundColor: '#ffffff', | |
| fontColor: '#333333' | |
| } | |
| }; | |
| // Default thinking words | |
| const defaultThinkingWords = [ | |
| "hmmm...", | |
| "ah!", | |
| "wait...", | |
| "processing...", | |
| "let me think...", | |
| "aha!", | |
| "just a sec!", | |
| "🤔", | |
| "calculating...", | |
| "hocus pocus..." | |
| ]; | |
| // Merge user config with defaults | |
| const config = window.ChatWidgetConfig ? deepMerge(JSON.parse(JSON.stringify(defaultConfig)), window.ChatWidgetConfig) : defaultConfig; | |
| config.thinkingWords = config.thinkingWords || defaultThinkingWords; | |
| // Prevent multiple initializations | |
| if (window.N8NChatWidgetInitialized) return; | |
| window.N8NChatWidgetInitialized = true; | |
| let currentSessionId = ''; | |
| // Create widget container | |
| const widgetContainer = createElement('div', { className: 'n8n-chat-widget' }); | |
| // Set CSS variables for colors | |
| Object.entries({ | |
| '--n8n-chat-primary-color': config.style.primaryColor, | |
| '--n8n-chat-secondary-color': config.style.secondaryColor, | |
| '--n8n-chat-background-color': config.style.backgroundColor, | |
| '--n8n-chat-font-color': config.style.fontColor | |
| }).forEach(([k, v]) => widgetContainer.style.setProperty(k, v)); | |
| const chatContainer = createElement('div', { | |
| className: `chat-container${config.style.position === 'left' ? ' position-left' : ''}` | |
| }); | |
| const newConversationHTML = ` | |
| <div class="brand-header"> | |
| <img src="${config.branding.logo}" alt="${config.branding.name}"> | |
| <span>${config.branding.name}</span> | |
| <button class="close-button" aria-label="Close chat" tabindex="0">×</button> | |
| </div> | |
| <div class="new-conversation"> | |
| <h2 class="welcome-text">${config.branding.welcomeText}</h2> | |
| <button class="new-chat-btn" aria-label="Start new chat" tabindex="0"> | |
| <svg class="message-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> | |
| <path fill="currentColor" d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.2L4 17.2V4h16v12z"/> | |
| </svg> | |
| Send us a message | |
| </button> | |
| <p class="response-text">${config.branding.responseTimeText}</p> | |
| </div> | |
| `; | |
| const chatInterfaceHTML = ` | |
| <div class="chat-interface" aria-live="polite"> | |
| <div class="brand-header"> | |
| <img src="${config.branding.logo}" alt="${config.branding.name}"> | |
| <span>${config.branding.name}</span> | |
| <button class="close-button" aria-label="Close chat" tabindex="0">×</button> | |
| </div> | |
| <div class="chat-messages"></div> | |
| <div class="chat-input"> | |
| <textarea placeholder="Type your message here..." rows="1" aria-label="Chat input" tabindex="0"></textarea> | |
| <button type="submit" aria-label="Send message" tabindex="0">Send</button> | |
| </div> | |
| <div class="chat-footer"> | |
| <a href="${config.branding.poweredBy.link}" target="_blank">${config.branding.poweredBy.text}</a> | |
| </div> | |
| </div> | |
| `; | |
| chatContainer.innerHTML = newConversationHTML + chatInterfaceHTML; | |
| const toggleButton = createElement('button', { | |
| className: `chat-toggle${config.style.position === 'left' ? ' position-left' : ''}`, | |
| html: ` | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> | |
| <path d="M12 2C6.477 2 2 6.477 2 12c0 1.821.487 3.53 1.338 5L2.5 21.5l4.5-.838A9.955 9.955 0 0012 22c5.523 0 10-4.477 10-10S17.523 2 12 2zm0 18c-1.476 0-2.886-.313-4.156-.878l-3.156.586.586-3.156A7.962 7.962 0 014 12c0-4.411 3.589-8 8-8s8 3.589 8 8-3.589 8-8 8z"/> | |
| </svg>` | |
| }); | |
| widgetContainer.appendChild(chatContainer); | |
| widgetContainer.appendChild(toggleButton); | |
| document.body.appendChild(widgetContainer); | |
| // DOM refs | |
| const newChatBtn = chatContainer.querySelector('.new-chat-btn'); | |
| const chatInterface = chatContainer.querySelector('.chat-interface'); | |
| const messagesContainer = chatContainer.querySelector('.chat-messages'); | |
| const textarea = chatContainer.querySelector('textarea'); | |
| const sendButton = chatContainer.querySelector('button[type="submit"]'); | |
| const closeButtons = chatContainer.querySelectorAll('.close-button'); | |
| // Accessibility: Focus management | |
| function focusInput() { | |
| textarea.focus(); | |
| } | |
| // Utility: Generate UUID | |
| function generateUUID() { | |
| return (crypto && crypto.randomUUID) ? crypto.randomUUID() : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); | |
| } | |
| // Show error message in chat | |
| function showError(message) { | |
| const errorDiv = createElement('div', { className: 'chat-message bot', html: `<span style='color:red;'>${message}</span>` }); | |
| messagesContainer.appendChild(errorDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| } | |
| // Show/hide thinking indicator with two random thinking words and random timing | |
| let thinkingDotsDiv = null; | |
| let thinkingWordDiv1 = null; | |
| let thinkingWordDiv2 = null; | |
| let thinkingDotsTimeout = null; | |
| let thinkingWordTimeout1 = null; | |
| let thinkingWordTimeout2 = null; | |
| function getRandomDelay(min = 1500, max = 3000) { | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| } | |
| function getTwoDistinctWords(words) { | |
| if (words.length < 2) return [words[0], words[0]]; | |
| const idx1 = Math.floor(Math.random() * words.length); | |
| let idx2; | |
| do { | |
| idx2 = Math.floor(Math.random() * words.length); | |
| } while (idx2 === idx1); | |
| return [words[idx1], words[idx2]]; | |
| } | |
| function showThinkingIndicator() { | |
| hideThinkingIndicator(); // Clean up any previous indicators | |
| // Step 1: Show dots immediately | |
| thinkingDotsDiv = createElement('div', { | |
| className: 'chat-message bot thinking-indicator', | |
| html: `<span class="dots" aria-label="Bot is thinking" role="status"><span>.</span><span>.</span><span>.</span></span>` | |
| }); | |
| messagesContainer.appendChild(thinkingDotsDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| // Step 2: After random delay, show first random word | |
| const [word1, word2] = getTwoDistinctWords(config.thinkingWords); | |
| const delay1 = getRandomDelay(); | |
| thinkingDotsTimeout = setTimeout(() => { | |
| if (thinkingDotsDiv) { | |
| thinkingDotsDiv.remove(); | |
| thinkingDotsDiv = null; | |
| } | |
| thinkingWordDiv1 = createElement('div', { | |
| className: 'chat-message bot thinking-word', | |
| html: `<span aria-label="Bot is thinking" role="status">${word1}</span>` | |
| }); | |
| messagesContainer.appendChild(thinkingWordDiv1); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| // Step 3: After another random delay, show second random word | |
| const delay2 = getRandomDelay(); | |
| thinkingWordTimeout1 = setTimeout(() => { | |
| if (thinkingWordDiv1) { | |
| thinkingWordDiv1.remove(); | |
| thinkingWordDiv1 = null; | |
| } | |
| thinkingWordDiv2 = createElement('div', { | |
| className: 'chat-message bot thinking-word', | |
| html: `<span aria-label="Bot is thinking" role="status">${word2}</span>` | |
| }); | |
| messagesContainer.appendChild(thinkingWordDiv2); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| // Step 4: After another random delay, switch back to dots | |
| thinkingWordTimeout2 = setTimeout(() => { | |
| if (thinkingWordDiv2) { | |
| thinkingWordDiv2.remove(); | |
| thinkingWordDiv2 = null; | |
| } | |
| thinkingDotsDiv = createElement('div', { | |
| className: 'chat-message bot thinking-indicator', | |
| html: `<span class="dots" aria-label="Bot is thinking" role="status"><span>.</span><span>.</span><span>.</span></span>` | |
| }); | |
| messagesContainer.appendChild(thinkingDotsDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| }, getRandomDelay()); | |
| }, delay2); | |
| }, delay1); | |
| } | |
| function hideThinkingIndicator() { | |
| if (thinkingDotsTimeout) { | |
| clearTimeout(thinkingDotsTimeout); | |
| thinkingDotsTimeout = null; | |
| } | |
| if (thinkingWordTimeout1) { | |
| clearTimeout(thinkingWordTimeout1); | |
| thinkingWordTimeout1 = null; | |
| } | |
| if (thinkingWordTimeout2) { | |
| clearTimeout(thinkingWordTimeout2); | |
| thinkingWordTimeout2 = null; | |
| } | |
| if (thinkingDotsDiv) { | |
| thinkingDotsDiv.remove(); | |
| thinkingDotsDiv = null; | |
| } | |
| if (thinkingWordDiv1) { | |
| thinkingWordDiv1.remove(); | |
| thinkingWordDiv1 = null; | |
| } | |
| if (thinkingWordDiv2) { | |
| thinkingWordDiv2.remove(); | |
| thinkingWordDiv2 = null; | |
| } | |
| // Also remove any stray indicators | |
| const strayDots = messagesContainer.querySelector('.thinking-indicator'); | |
| if (strayDots) strayDots.remove(); | |
| const strayWord = messagesContainer.querySelectorAll('.thinking-word'); | |
| strayWord.forEach(w => w.remove()); | |
| } | |
| // Utility: Format URLs in text as clickable links | |
| function formatUrls(text) { | |
| // Regex matches http(s)://, www., and bare domains | |
| const urlRegex = /((https?:\/\/|www\.)[\w\-]+(\.[\w\-]+)+(:\d+)?(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi; | |
| return text.replace(urlRegex, function(url) { | |
| let href = url; | |
| if (!href.match(/^https?:\/\//)) { | |
| href = 'https://' + href; | |
| } | |
| return `<a href="${href}" target="_blank" rel="noopener noreferrer">${url}</a>`; | |
| }); | |
| } | |
| // Start new conversation | |
| async function startNewConversation() { | |
| currentSessionId = generateUUID(); | |
| const data = [{ | |
| action: "loadPreviousSession", | |
| sessionId: currentSessionId, | |
| route: config.webhook.route, | |
| metadata: { userId: "" } | |
| }]; | |
| try { | |
| const response = await fetch(config.webhook.url, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (!response.ok) throw new Error('Network error'); | |
| const responseData = await response.json(); | |
| chatContainer.querySelector('.brand-header').style.display = 'none'; | |
| chatContainer.querySelector('.new-conversation').style.display = 'none'; | |
| chatInterface.classList.add('active'); | |
| const botMessageDiv = createElement('div', { | |
| className: 'chat-message bot', | |
| html: formatUrls(Array.isArray(responseData) ? responseData[0].output : responseData.output) | |
| }); | |
| messagesContainer.appendChild(botMessageDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| focusInput(); | |
| } catch (error) { | |
| showError('Failed to start conversation. Please try again.'); | |
| console.error('Error:', error); | |
| } | |
| } | |
| // Send message | |
| async function sendMessage(message) { | |
| if (!message) return; | |
| const messageData = { | |
| action: "sendMessage", | |
| sessionId: currentSessionId, | |
| route: config.webhook.route, | |
| chatInput: message, | |
| metadata: { userId: "" } | |
| }; | |
| const userMessageDiv = createElement('div', { className: 'chat-message user', html: message }); | |
| messagesContainer.appendChild(userMessageDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| showThinkingIndicator(); | |
| try { | |
| const response = await fetch(config.webhook.url, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(messageData) | |
| }); | |
| if (!response.ok) throw new Error('Network error'); | |
| const data = await response.json(); | |
| hideThinkingIndicator(); | |
| const botMessageDiv = createElement('div', { | |
| className: 'chat-message bot', | |
| html: formatUrls(Array.isArray(data) ? data[0].output : data.output) | |
| }); | |
| messagesContainer.appendChild(botMessageDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| focusInput(); | |
| } catch (error) { | |
| hideThinkingIndicator(); | |
| showError('Failed to send message. Please try again.'); | |
| console.error('Error:', error); | |
| } | |
| } | |
| // Event listeners | |
| newChatBtn.addEventListener('click', startNewConversation); | |
| sendButton.addEventListener('click', () => { | |
| const message = textarea.value.trim(); | |
| if (message) { | |
| sendMessage(message); | |
| textarea.value = ''; | |
| } | |
| }); | |
| textarea.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| const message = textarea.value.trim(); | |
| if (message) { | |
| sendMessage(message); | |
| textarea.value = ''; | |
| } | |
| } | |
| }); | |
| toggleButton.addEventListener('click', () => { | |
| chatContainer.classList.toggle('open'); | |
| if (chatContainer.classList.contains('open')) focusInput(); | |
| }); | |
| closeButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| chatContainer.classList.remove('open'); | |
| }); | |
| }); | |
| // Accessibility: ESC closes chat | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && chatContainer.classList.contains('open')) { | |
| chatContainer.classList.remove('open'); | |
| toggleButton.focus(); | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment