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 <noreply@anthropic.com>
This commit is contained in:
parent
641b238de8
commit
8108924dcd
2 changed files with 366 additions and 0 deletions
195
hud/hud.js
195
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 = '<div class="chat-loading">Loading messages...</div>';
|
||||
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 = '<div class="chat-loading">No messages yet. Start the conversation!</div>';
|
||||
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 `
|
||||
<div class="chat-message ${isStaff ? 'staff' : 'customer'}">
|
||||
<div class="sender">${msg.SenderName || (isStaff ? 'Staff' : 'Customer')}</div>
|
||||
<div class="text">${this.escapeHtml(msg.MessageBody)}</div>
|
||||
<div class="time">${time}</div>
|
||||
</div>
|
||||
`;
|
||||
}).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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
171
hud/index.html
171
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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -385,6 +536,26 @@
|
|||
<div class="status-indicator" id="statusIndicator"></div>
|
||||
<button class="restore-btn" id="restoreBtn" onclick="toggleFullscreen()">✕</button>
|
||||
|
||||
<!-- Chat Modal -->
|
||||
<div class="chat-overlay" id="chatOverlay">
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<h3 id="chatTitle">Chat</h3>
|
||||
<div>
|
||||
<button class="chat-close-btn" onclick="HUD.closeChat()">Close</button>
|
||||
<button class="chat-end-btn" onclick="HUD.endChat()">End Chat</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="chat-loading">Loading messages...</div>
|
||||
</div>
|
||||
<div class="chat-input-area">
|
||||
<input type="text" class="chat-input" id="chatInput" placeholder="Type a message..." onkeypress="if(event.key==='Enter')HUD.sendMessage()">
|
||||
<button class="chat-send-btn" onclick="HUD.sendMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="hud.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Reference in a new issue