From 8108924dcdf58c9d2ebb5dc66c52171cc499b145 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Sun, 8 Feb 2026 15:22:19 -0800 Subject: [PATCH] Add chat modal to HUD for responding to chat tasks - When accepting a chat task, automatically opens chat modal - Chat modal shows message history and allows sending replies - Auto-polls for new messages every 2 seconds - End Chat button to complete the chat task Co-Authored-By: Claude Opus 4.5 --- hud/hud.js | 195 +++++++++++++++++++++++++++++++++++++++++++++++++ hud/index.html | 171 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+) diff --git a/hud/hud.js b/hud/hud.js index 6ab56e3..93c7fd1 100644 --- a/hud/hud.js +++ b/hud/hud.js @@ -22,6 +22,12 @@ const HUD = { isConnected: true, businessName: '', + // Chat state + chatTaskId: null, + chatMessages: [], + chatPollInterval: null, + lastMessageId: 0, + // Category names (will be loaded from API) categories: { 1: { name: 'Orders', color: '#ef4444' }, @@ -343,6 +349,11 @@ const HUD = { this.tasks = this.tasks.filter(t => t.TaskID !== task.TaskID); this.renderTasks(); this.showFeedback('Task accepted!', 'success'); + + // If it's a chat task, open the chat modal + if (this.isChatTask(task)) { + this.openChat(task); + } } else { this.showFeedback(data.ERROR || 'Failed to accept', 'error'); } @@ -368,6 +379,190 @@ const HUD = { } else { indicator.classList.add('disconnected'); } + }, + + // Check if task is a chat task + isChatTask(task) { + if (!task) return false; + const name = (task.TaskTypeName || task.Title || '').toLowerCase(); + return name.includes('chat'); + }, + + // Open chat modal for a task + openChat(task) { + this.chatTaskId = task.TaskID; + this.chatMessages = []; + this.lastMessageId = 0; + + document.getElementById('chatTitle').textContent = task.Title || 'Chat'; + document.getElementById('chatMessages').innerHTML = '
Loading messages...
'; + document.getElementById('chatOverlay').classList.add('visible'); + document.getElementById('chatInput').focus(); + + // Load messages immediately and start polling + this.loadMessages(); + this.chatPollInterval = setInterval(() => this.loadMessages(), 2000); + }, + + // Close chat modal + closeChat() { + document.getElementById('chatOverlay').classList.remove('visible'); + if (this.chatPollInterval) { + clearInterval(this.chatPollInterval); + this.chatPollInterval = null; + } + this.chatTaskId = null; + this.chatMessages = []; + this.lastMessageId = 0; + }, + + // Load chat messages + async loadMessages() { + if (!this.chatTaskId) return; + + try { + const response = await fetch('/api/chat/getMessages.cfm', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + TaskID: this.chatTaskId, + AfterMessageID: this.lastMessageId + }) + }); + + const data = await response.json(); + + if (data.OK) { + const messages = data.MESSAGES || []; + + // If first load, replace all; otherwise append new messages + if (this.lastMessageId === 0) { + this.chatMessages = messages; + } else if (messages.length > 0) { + this.chatMessages = [...this.chatMessages, ...messages]; + } + + // Update last message ID + if (messages.length > 0) { + this.lastMessageId = messages[messages.length - 1].MessageID; + } + + this.renderMessages(); + + // Check if chat was closed + if (data.CHAT_CLOSED) { + this.showFeedback('Chat has ended', 'info'); + this.closeChat(); + } + } + } catch (err) { + console.error('[HUD] Load messages error:', err); + } + }, + + // Render chat messages + renderMessages() { + const container = document.getElementById('chatMessages'); + + if (this.chatMessages.length === 0) { + container.innerHTML = '
No messages yet. Start the conversation!
'; + return; + } + + container.innerHTML = this.chatMessages.map(msg => { + const isStaff = msg.SenderType === 'staff' || msg.SenderType === 'worker'; + const time = new Date(msg.CreatedOn).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + return ` +
+
${msg.SenderName || (isStaff ? 'Staff' : 'Customer')}
+
${this.escapeHtml(msg.MessageBody)}
+
${time}
+
+ `; + }).join(''); + + // Scroll to bottom + container.scrollTop = container.scrollHeight; + }, + + // Send a chat message + async sendMessage() { + const input = document.getElementById('chatInput'); + const message = input.value.trim(); + + if (!message || !this.chatTaskId) return; + + input.value = ''; + input.disabled = true; + + try { + const response = await fetch('/api/chat/sendMessage.cfm', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + TaskID: this.chatTaskId, + Message: message, + SenderType: 'staff' + }) + }); + + const data = await response.json(); + + if (data.OK) { + // Add message locally for immediate feedback + this.chatMessages.push({ + MessageID: data.MessageID || Date.now(), + MessageBody: message, + SenderType: 'staff', + SenderName: 'Staff', + CreatedOn: new Date().toISOString() + }); + this.renderMessages(); + } else { + this.showFeedback('Failed to send message', 'error'); + } + } catch (err) { + console.error('[HUD] Send message error:', err); + this.showFeedback('Failed to send message', 'error'); + } + + input.disabled = false; + input.focus(); + }, + + // End the chat (complete the task) + async endChat() { + if (!this.chatTaskId) return; + + if (!confirm('End this chat?')) return; + + try { + const response = await fetch('/api/tasks/completeChat.cfm', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ TaskID: this.chatTaskId }) + }); + + const data = await response.json(); + + if (data.OK) { + this.showFeedback('Chat ended', 'success'); + this.closeChat(); + } else { + this.showFeedback(data.ERROR || 'Failed to end chat', 'error'); + } + } catch (err) { + console.error('[HUD] End chat error:', err); + this.showFeedback('Failed to end chat', 'error'); + } + }, + + // Escape HTML for safe rendering + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } }; diff --git a/hud/index.html b/hud/index.html index 255bbee..a9faa04 100644 --- a/hud/index.html +++ b/hud/index.html @@ -334,6 +334,157 @@ 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } + + /* Chat modal */ + .chat-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.95); + z-index: 1100; + justify-content: center; + align-items: center; + padding: 20px; + } + + .chat-overlay.visible { + display: flex; + } + + .chat-container { + background: #1a1a1a; + border-radius: 12px; + width: 100%; + max-width: 500px; + height: 80vh; + max-height: 600px; + display: flex; + flex-direction: column; + border: 1px solid #333; + } + + .chat-header { + padding: 16px; + border-bottom: 1px solid #333; + display: flex; + justify-content: space-between; + align-items: center; + } + + .chat-header h3 { + margin: 0; + color: #fff; + font-size: 18px; + } + + .chat-close-btn { + background: #333; + border: none; + color: #fff; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + } + + .chat-messages { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + } + + .chat-message { + max-width: 80%; + padding: 10px 14px; + border-radius: 12px; + font-size: 14px; + line-height: 1.4; + } + + .chat-message.customer { + align-self: flex-start; + background: #333; + color: #fff; + } + + .chat-message.staff { + align-self: flex-end; + background: #2196f3; + color: #fff; + } + + .chat-message .sender { + font-size: 11px; + opacity: 0.7; + margin-bottom: 4px; + } + + .chat-message .time { + font-size: 10px; + opacity: 0.5; + margin-top: 4px; + text-align: right; + } + + .chat-input-area { + padding: 16px; + border-top: 1px solid #333; + display: flex; + gap: 12px; + } + + .chat-input { + flex: 1; + background: #333; + border: none; + border-radius: 8px; + padding: 12px 16px; + color: #fff; + font-size: 14px; + outline: none; + } + + .chat-input::placeholder { + color: #666; + } + + .chat-send-btn { + background: #22c55e; + border: none; + border-radius: 8px; + padding: 12px 20px; + color: #fff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + } + + .chat-send-btn:active { + transform: scale(0.98); + } + + .chat-loading { + text-align: center; + color: #666; + padding: 20px; + } + + .chat-end-btn { + background: #ef4444; + border: none; + color: #fff; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + margin-left: 8px; + } @@ -385,6 +536,26 @@
+ +
+
+
+

Chat

+
+ + +
+
+
+
Loading messages...
+
+
+ + +
+
+
+