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,
|
isConnected: true,
|
||||||
businessName: '',
|
businessName: '',
|
||||||
|
|
||||||
|
// Chat state
|
||||||
|
chatTaskId: null,
|
||||||
|
chatMessages: [],
|
||||||
|
chatPollInterval: null,
|
||||||
|
lastMessageId: 0,
|
||||||
|
|
||||||
// Category names (will be loaded from API)
|
// Category names (will be loaded from API)
|
||||||
categories: {
|
categories: {
|
||||||
1: { name: 'Orders', color: '#ef4444' },
|
1: { name: 'Orders', color: '#ef4444' },
|
||||||
|
|
@ -343,6 +349,11 @@ const HUD = {
|
||||||
this.tasks = this.tasks.filter(t => t.TaskID !== task.TaskID);
|
this.tasks = this.tasks.filter(t => t.TaskID !== task.TaskID);
|
||||||
this.renderTasks();
|
this.renderTasks();
|
||||||
this.showFeedback('Task accepted!', 'success');
|
this.showFeedback('Task accepted!', 'success');
|
||||||
|
|
||||||
|
// If it's a chat task, open the chat modal
|
||||||
|
if (this.isChatTask(task)) {
|
||||||
|
this.openChat(task);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showFeedback(data.ERROR || 'Failed to accept', 'error');
|
this.showFeedback(data.ERROR || 'Failed to accept', 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -368,6 +379,190 @@ const HUD = {
|
||||||
} else {
|
} else {
|
||||||
indicator.classList.add('disconnected');
|
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; }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.5; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -385,6 +536,26 @@
|
||||||
<div class="status-indicator" id="statusIndicator"></div>
|
<div class="status-indicator" id="statusIndicator"></div>
|
||||||
<button class="restore-btn" id="restoreBtn" onclick="toggleFullscreen()">✕</button>
|
<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>
|
<script src="hud.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Reference in a new issue