2025-12-05 13:47:42 -03:00
|
|
|
<!doctype html>
|
2025-12-03 18:42:22 -03:00
|
|
|
<html lang="en">
|
2025-12-05 13:47:42 -03:00
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
<title>Attendant Console - General Bots</title>
|
|
|
|
|
<style>
|
|
|
|
|
:root {
|
|
|
|
|
--bg-primary: #0f172a;
|
|
|
|
|
--bg-secondary: #1e293b;
|
|
|
|
|
--bg-tertiary: #334155;
|
|
|
|
|
--bg-quaternary: #475569;
|
|
|
|
|
--text-primary: #f1f5f9;
|
|
|
|
|
--text-secondary: #94a3b8;
|
|
|
|
|
--text-muted: #64748b;
|
|
|
|
|
--border-color: #334155;
|
|
|
|
|
--accent-color: #3b82f6;
|
|
|
|
|
--accent-hover: #2563eb;
|
|
|
|
|
--success-color: #10b981;
|
|
|
|
|
--warning-color: #f59e0b;
|
|
|
|
|
--error-color: #ef4444;
|
|
|
|
|
--info-color: #06b6d4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
* {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
font-family:
|
|
|
|
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
|
|
|
Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
height: 100vh;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Main Layout */
|
|
|
|
|
.attendant-layout {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 320px 1fr 380px;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* CRM Disabled State */
|
|
|
|
|
.crm-disabled {
|
|
|
|
|
display: none;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 40px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.crm-disabled.active {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.crm-disabled-icon {
|
|
|
|
|
font-size: 64px;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.crm-disabled h2 {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.crm-disabled p {
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.crm-disabled code {
|
|
|
|
|
display: block;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-family: "Monaco", "Menlo", monospace;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Left Sidebar - Queue */
|
|
|
|
|
.queue-sidebar {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-right: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue-header {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue-title {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue-title svg {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Attendant Status Selector */
|
|
|
|
|
.attendant-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-indicator {
|
|
|
|
|
width: 12px;
|
|
|
|
|
height: 12px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--success-color);
|
|
|
|
|
transition: background 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-indicator.online {
|
|
|
|
|
background: var(--success-color);
|
|
|
|
|
}
|
|
|
|
|
.status-indicator.busy {
|
|
|
|
|
background: var(--warning-color);
|
|
|
|
|
}
|
|
|
|
|
.status-indicator.away {
|
|
|
|
|
background: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
.status-indicator.offline {
|
|
|
|
|
background: var(--error-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
0%,
|
|
|
|
|
100% {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
}
|
|
|
|
|
50% {
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-indicator.online {
|
|
|
|
|
animation: pulse 2s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-name {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-text {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dropdown {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 100%;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: none;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dropdown.show {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-option {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-option:hover {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Queue Stats */
|
|
|
|
|
.queue-stats {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-item {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--accent-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Queue Filters */
|
|
|
|
|
.queue-filters {
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn {
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn:hover {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn.active {
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn .badge {
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Conversation List */
|
|
|
|
|
.conversation-list {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-item {
|
|
|
|
|
padding: 14px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-item:hover {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-item.active {
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-item.active .conversation-time,
|
|
|
|
|
.conversation-item.active .conversation-preview,
|
|
|
|
|
.conversation-item.active .channel-tag {
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-item.unread::before {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 4px;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.customer-name {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-time {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-preview {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.conversation-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.channel-tag {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.channel-whatsapp {
|
|
|
|
|
background: #25d366;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.channel-teams {
|
|
|
|
|
background: #6264a7;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.channel-instagram {
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
45deg,
|
|
|
|
|
#f09433,
|
|
|
|
|
#e6683c,
|
|
|
|
|
#dc2743,
|
|
|
|
|
#cc2366,
|
|
|
|
|
#bc1888
|
|
|
|
|
);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.channel-web {
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.channel-telegram {
|
|
|
|
|
background: #0088cc;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.channel-email {
|
|
|
|
|
background: #ea4335;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.priority-tag {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.priority-high {
|
|
|
|
|
background: var(--error-color);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.priority-urgent {
|
|
|
|
|
background: #dc2626;
|
|
|
|
|
color: white;
|
|
|
|
|
animation: pulse 1s infinite;
|
|
|
|
|
}
|
|
|
|
|
.priority-normal {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.waiting-time {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.waiting-time.long {
|
|
|
|
|
color: var(--warning-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Empty Queue State */
|
|
|
|
|
.empty-queue {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-queue svg {
|
|
|
|
|
width: 64px;
|
|
|
|
|
height: 64px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Chat Area */
|
|
|
|
|
.chat-area {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-conversation {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 100%;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-conversation svg {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-header {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.customer-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.customer-avatar {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.customer-details h3 {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.customer-status-line {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-indicator {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
gap: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-indicator span {
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
background: var(--text-secondary);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: typing 1.4s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-indicator span:nth-child(2) {
|
|
|
|
|
animation-delay: 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.typing-indicator span:nth-child(3) {
|
|
|
|
|
animation-delay: 0.4s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes typing {
|
|
|
|
|
0%,
|
|
|
|
|
60%,
|
|
|
|
|
100% {
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
}
|
|
|
|
|
30% {
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:hover {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary {
|
|
|
|
|
background: var(--success-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary:hover {
|
|
|
|
|
background: #059669;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.danger {
|
|
|
|
|
background: var(--error-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.danger:hover {
|
|
|
|
|
background: #dc2626;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Chat Messages */
|
|
|
|
|
.chat-messages {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
max-width: 80%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.customer {
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.attendant {
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
flex-direction: row-reverse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.bot {
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.system {
|
|
|
|
|
align-self: center;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-avatar {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.bot .message-avatar {
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-bubble {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.customer .message-bubble {
|
|
|
|
|
border-bottom-left-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.attendant .message-bubble {
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
border-bottom-right-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.bot .message-bubble {
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
135deg,
|
|
|
|
|
rgba(102, 126, 234, 0.2),
|
|
|
|
|
rgba(118, 75, 162, 0.2)
|
|
|
|
|
);
|
|
|
|
|
border: 1px solid rgba(102, 126, 234, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.system .message-bubble {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message.attendant .message-meta {
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bot-badge {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Chat Input */
|
|
|
|
|
.chat-input-area {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-top: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-responses {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-response-btn {
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.quick-response-btn:hover {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-action-btn {
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-action-btn:hover {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
resize: none;
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
max-height: 120px;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--accent-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-input::placeholder {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-btn {
|
|
|
|
|
width: 44px;
|
|
|
|
|
height: 44px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
color: white;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-btn:hover {
|
|
|
|
|
background: var(--accent-hover);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.send-btn:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Right Sidebar - Insights */
|
|
|
|
|
.insights-sidebar {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-left: 1px solid var(--border-color);
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sidebar-section {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* AI Insights */
|
|
|
|
|
.ai-insight {
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
135deg,
|
|
|
|
|
rgba(102, 126, 234, 0.1),
|
|
|
|
|
rgba(118, 75, 162, 0.1)
|
|
|
|
|
);
|
|
|
|
|
border: 1px solid rgba(102, 126, 234, 0.2);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.insight-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.insight-icon {
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.insight-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.insight-value {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Suggested Replies */
|
|
|
|
|
.suggested-reply {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.suggested-reply:hover {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.suggested-reply-text {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.suggestion-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.suggestion-confidence {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--success-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.suggestion-source {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Customer Details */
|
|
|
|
|
.customer-detail-item {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-label {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-value {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-value a {
|
|
|
|
|
color: var(--accent-color);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-value a:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Conversation History */
|
|
|
|
|
.history-item {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-item:hover {
|
|
|
|
|
background: var(--bg-quaternary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-date {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-status {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: var(--success-color);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-summary {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Sentiment */
|
|
|
|
|
.sentiment-indicator {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sentiment-positive {
|
|
|
|
|
background: rgba(16, 185, 129, 0.2);
|
|
|
|
|
color: var(--success-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sentiment-neutral {
|
|
|
|
|
background: rgba(148, 163, 184, 0.2);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sentiment-negative {
|
|
|
|
|
background: rgba(239, 68, 68, 0.2);
|
|
|
|
|
color: var(--error-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Tags */
|
|
|
|
|
.tags-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tag {
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Transfer Modal */
|
|
|
|
|
.modal-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
display: none;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-overlay.show {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 480px;
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-header h3 {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-close {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-body {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input,
|
|
|
|
|
.form-select {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input:focus,
|
|
|
|
|
.form-select:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--accent-color);
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
.attendant-list {
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
.attendant-option {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
.attendant-option:hover {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.attendant-option.selected {
|
|
|
|
|
background: var(--accent-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.attendant-option .status-indicator {
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-top: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Toast Notifications */
|
|
|
|
|
.toast-container {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 20px;
|
|
|
|
|
right: 20px;
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toast {
|
|
|
|
|
padding: 14px 20px;
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
animation: slideIn 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes slideIn {
|
|
|
|
|
from {
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toast.success {
|
|
|
|
|
border-left: 4px solid var(--success-color);
|
|
|
|
|
}
|
|
|
|
|
.toast.error {
|
|
|
|
|
border-left: 4px solid var(--error-color);
|
|
|
|
|
}
|
|
|
|
|
.toast.warning {
|
|
|
|
|
border-left: 4px solid var(--warning-color);
|
|
|
|
|
}
|
|
|
|
|
.toast.info {
|
|
|
|
|
border-left: 4px solid var(--info-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Loading State */
|
|
|
|
|
.loading-spinner {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border: 3px solid var(--bg-tertiary);
|
|
|
|
|
border-top-color: var(--accent-color);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
to {
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Responsive */
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
.attendant-layout {
|
|
|
|
|
grid-template-columns: 280px 1fr 320px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 992px) {
|
|
|
|
|
.attendant-layout {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue-sidebar,
|
|
|
|
|
.insights-sidebar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue-sidebar.mobile-show,
|
|
|
|
|
.insights-sidebar.mobile-show {
|
|
|
|
|
display: flex;
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
width: 320px;
|
|
|
|
|
z-index: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.queue-sidebar.mobile-show {
|
|
|
|
|
left: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.insights-sidebar.mobile-show {
|
|
|
|
|
right: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<!-- CRM Disabled State -->
|
|
|
|
|
<div class="crm-disabled" id="crmDisabled">
|
|
|
|
|
<div class="crm-disabled-icon">🚫</div>
|
|
|
|
|
<h2>CRM Features Not Enabled</h2>
|
|
|
|
|
<p>
|
|
|
|
|
The Attendant Console requires CRM features to be enabled for
|
|
|
|
|
this bot. This allows human agents to receive and respond to
|
|
|
|
|
conversations transferred from the bot.
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
To enable CRM features, add this line to your bot's
|
|
|
|
|
<strong>config.csv</strong>:
|
|
|
|
|
</p>
|
|
|
|
|
<code>crm-enabled,true</code>
|
|
|
|
|
<p style="margin-top: 24px">
|
|
|
|
|
Then create an <strong>attendant.csv</strong> file to configure
|
|
|
|
|
your team:
|
|
|
|
|
</p>
|
|
|
|
|
<code
|
|
|
|
|
>id,name,channel,preferences,department,aliases att-001,John
|
|
|
|
|
Smith,all,sales,commercial,john;johnny att-002,Jane
|
|
|
|
|
Doe,web,support,customer-service,jane</code
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Main Layout -->
|
|
|
|
|
<div class="attendant-layout" id="mainLayout" style="display: none">
|
|
|
|
|
<!-- Left Sidebar - Queue -->
|
|
|
|
|
<aside class="queue-sidebar">
|
|
|
|
|
<div class="queue-header">
|
|
|
|
|
<div class="queue-title">
|
|
|
|
|
<svg
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
|
|
|
|
|
/>
|
|
|
|
|
<circle cx="9" cy="7" r="4" />
|
|
|
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
|
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
|
|
|
</svg>
|
|
|
|
|
Attendant Console
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div
|
|
|
|
|
class="attendant-status"
|
|
|
|
|
id="attendantStatus"
|
|
|
|
|
onclick="toggleStatusDropdown()"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
class="status-indicator online"
|
|
|
|
|
id="statusIndicator"
|
|
|
|
|
></div>
|
|
|
|
|
<div class="status-info">
|
|
|
|
|
<div class="status-name" id="attendantName">
|
|
|
|
|
Loading...
|
|
|
|
|
</div>
|
|
|
|
|
<div class="status-text" id="statusText">
|
|
|
|
|
Online - Ready for conversations
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path d="M6 9l6 6 6-6" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="status-dropdown" id="statusDropdown">
|
|
|
|
|
<div
|
|
|
|
|
class="status-option"
|
|
|
|
|
onclick="setStatus('online')"
|
|
|
|
|
>
|
|
|
|
|
<div class="status-indicator online"></div>
|
|
|
|
|
<span>Online</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="status-option"
|
|
|
|
|
onclick="setStatus('busy')"
|
|
|
|
|
>
|
|
|
|
|
<div class="status-indicator busy"></div>
|
|
|
|
|
<span>Busy</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="status-option"
|
|
|
|
|
onclick="setStatus('away')"
|
|
|
|
|
>
|
|
|
|
|
<div class="status-indicator away"></div>
|
|
|
|
|
<span>Away</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="status-option"
|
|
|
|
|
onclick="setStatus('offline')"
|
|
|
|
|
>
|
|
|
|
|
<div class="status-indicator offline"></div>
|
|
|
|
|
<span>Offline</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="queue-stats">
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<div class="stat-value" id="waitingCount">0</div>
|
|
|
|
|
<div class="stat-label">Waiting</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="stat-item">
|
|
|
|
|
<div class="stat-value" id="activeCount">0</div>
|
|
|
|
|
<div class="stat-label">Active</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="stat-item">
|
|
|
|
|
<div class="stat-value" id="resolvedCount">0</div>
|
|
|
|
|
<div class="stat-label">Resolved</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="queue-filters">
|
|
|
|
|
<button
|
|
|
|
|
class="filter-btn active"
|
|
|
|
|
data-filter="all"
|
|
|
|
|
onclick="filterQueue('all')"
|
|
|
|
|
>
|
|
|
|
|
All <span class="badge" id="allBadge">0</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="filter-btn"
|
|
|
|
|
data-filter="waiting"
|
|
|
|
|
onclick="filterQueue('waiting')"
|
|
|
|
|
>
|
|
|
|
|
Waiting <span class="badge" id="waitingBadge">0</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="filter-btn"
|
|
|
|
|
data-filter="mine"
|
|
|
|
|
onclick="filterQueue('mine')"
|
|
|
|
|
>
|
|
|
|
|
Mine <span class="badge" id="mineBadge">0</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="filter-btn"
|
|
|
|
|
data-filter="high"
|
|
|
|
|
onclick="filterQueue('high')"
|
|
|
|
|
>
|
|
|
|
|
🔥 Priority
|
|
|
|
|
</button>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="conversation-list" id="conversationList">
|
|
|
|
|
<div class="empty-queue" id="emptyQueue">
|
|
|
|
|
<svg
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="1.5"
|
|
|
|
|
>
|
|
|
|
|
<path d="M20 6L9 17l-5-5" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p>No conversations in queue</p>
|
|
|
|
|
<small>New conversations will appear here</small>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Chat Area -->
|
|
|
|
|
<main class="chat-area" id="chatArea">
|
|
|
|
|
<div class="no-conversation" id="noConversation">
|
|
|
|
|
<svg
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="1.5"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
<h3>Select a conversation</h3>
|
|
|
|
|
<p>
|
|
|
|
|
Choose a conversation from the queue to start responding
|
|
|
|
|
</p>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div
|
|
|
|
|
id="activeChat"
|
|
|
|
|
style="display: none; flex-direction: column; height: 100%"
|
|
|
|
|
>
|
|
|
|
|
<div class="chat-header">
|
|
|
|
|
<div class="customer-info">
|
|
|
|
|
<div class="customer-avatar" id="customerAvatar">
|
|
|
|
|
?
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customer-details">
|
|
|
|
|
<h3 id="customerName">Customer Name</h3>
|
|
|
|
|
<div class="customer-status-line">
|
|
|
|
|
<span
|
|
|
|
|
class="channel-tag"
|
|
|
|
|
id="customerChannel"
|
|
|
|
|
>web</span
|
|
|
|
|
>
|
|
|
|
|
<span id="customerStatusText"
|
|
|
|
|
>Active now</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="chat-actions">
|
|
|
|
|
<button
|
|
|
|
|
class="action-btn"
|
|
|
|
|
onclick="showTransferModal()"
|
|
|
|
|
title="Transfer"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path d="M17 1l4 4-4 4" />
|
|
|
|
|
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
|
|
|
|
<path d="M7 23l-4-4 4-4" />
|
|
|
|
|
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
|
|
|
|
</svg>
|
|
|
|
|
Transfer
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="action-btn primary"
|
|
|
|
|
onclick="resolveConversation()"
|
|
|
|
|
title="Resolve"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path d="M20 6L9 17l-5-5" />
|
|
|
|
|
</svg>
|
|
|
|
|
Resolve
|
|
|
|
|
</button>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="chat-messages" id="chatMessages">
|
|
|
|
|
<!-- Messages loaded dynamically -->
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="chat-input-area">
|
|
|
|
|
<div class="quick-responses" id="quickResponses">
|
|
|
|
|
<button
|
|
|
|
|
class="quick-response-btn"
|
|
|
|
|
onclick="useQuickResponse('Hello! How can I help you today?')"
|
|
|
|
|
>
|
|
|
|
|
👋 Greeting
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="quick-response-btn"
|
|
|
|
|
onclick="useQuickResponse('Thank you for your patience.')"
|
|
|
|
|
>
|
|
|
|
|
🙏 Thanks
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="quick-response-btn"
|
|
|
|
|
onclick="useQuickResponse('Let me look into that for you.')"
|
|
|
|
|
>
|
|
|
|
|
🔍 Looking into it
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="quick-response-btn"
|
|
|
|
|
onclick="useQuickResponse('Is there anything else I can help you with?')"
|
|
|
|
|
>
|
|
|
|
|
❓ Anything else
|
|
|
|
|
</button>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="input-wrapper">
|
|
|
|
|
<div class="input-actions">
|
|
|
|
|
<button
|
|
|
|
|
class="input-action-btn"
|
|
|
|
|
onclick="attachFile()"
|
|
|
|
|
title="Attach file"
|
|
|
|
|
>
|
|
|
|
|
<button class="input-action-btn" onclick="attachFile()" title="Attach file">
|
|
|
|
|
<svg
|
|
|
|
|
width="18"
|
|
|
|
|
height="18"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="input-action-btn"
|
|
|
|
|
onclick="polishMessage()"
|
|
|
|
|
title="✨ Polish message with AI"
|
|
|
|
|
style="color: var(--primary);"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="18"
|
|
|
|
|
height="18"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z"/>
|
|
|
|
|
<path d="M19 13l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="input-action-btn"
|
|
|
|
|
onclick="insertEmoji()"
|
|
|
|
|
title="Insert emoji"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="18"
|
|
|
|
|
height="18"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<circle cx="12" cy="12" r="10" />
|
|
|
|
|
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
|
|
|
|
<line x1="9" y1="9" x2="9.01" y2="9" />
|
|
|
|
|
<line x1="15" y1="9" x2="15.01" y2="9" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<textarea
|
|
|
|
|
class="chat-input"
|
|
|
|
|
id="chatInput"
|
|
|
|
|
placeholder="Type your message..."
|
|
|
|
|
rows="1"
|
|
|
|
|
></textarea>
|
|
|
|
|
<button
|
|
|
|
|
class="send-btn"
|
|
|
|
|
id="sendBtn"
|
|
|
|
|
onclick="sendMessage()"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="20"
|
|
|
|
|
height="20"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<line x1="22" y1="2" x2="11" y2="13" />
|
|
|
|
|
<polygon
|
|
|
|
|
points="22 2 15 22 11 13 2 9 22 2"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<!-- Right Sidebar - Insights -->
|
|
|
|
|
<aside class="insights-sidebar">
|
|
|
|
|
<div class="sidebar-section">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"
|
|
|
|
|
/>
|
|
|
|
|
<path d="M12 6v6l4 2" />
|
|
|
|
|
</svg>
|
|
|
|
|
AI Insights
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-insight" id="sentimentInsight">
|
|
|
|
|
<div class="insight-header">
|
|
|
|
|
<span class="insight-label"
|
|
|
|
|
>Customer Sentiment</span
|
|
|
|
|
>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="insight-value">
|
|
|
|
|
<span
|
|
|
|
|
class="sentiment-indicator sentiment-neutral"
|
|
|
|
|
id="sentimentValue"
|
|
|
|
|
>😐 Neutral</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-insight" id="intentInsight">
|
|
|
|
|
<div class="insight-header">
|
|
|
|
|
<span class="insight-label">Detected Intent</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="insight-value" id="intentValue">
|
|
|
|
|
Awaiting messages...
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-insight" id="summaryInsight">
|
|
|
|
|
<div class="insight-header">
|
|
|
|
|
<span class="insight-label"
|
|
|
|
|
>Conversation Summary</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="insight-value" id="summaryValue">
|
|
|
|
|
No conversation selected
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="sidebar-section">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
Suggested Replies
|
|
|
|
|
</div>
|
|
|
|
|
<div id="suggestedReplies">
|
|
|
|
|
<div
|
|
|
|
|
class="suggested-reply"
|
|
|
|
|
onclick="useSuggestion(this)"
|
|
|
|
|
>
|
|
|
|
|
<div class="suggested-reply-text">
|
|
|
|
|
Select a conversation to see AI-suggested
|
|
|
|
|
replies
|
|
|
|
|
</div>
|
|
|
|
|
<div class="suggestion-meta">
|
|
|
|
|
<span class="suggestion-source"
|
|
|
|
|
>AI Assistant</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<div
|
|
|
|
|
class="sidebar-section"
|
|
|
|
|
id="customerDetails"
|
|
|
|
|
style="display: none"
|
|
|
|
|
>
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
|
|
|
|
|
/>
|
|
|
|
|
<circle cx="12" cy="7" r="4" />
|
|
|
|
|
</svg>
|
|
|
|
|
Customer Details
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customer-detail-item">
|
|
|
|
|
<div class="detail-label">Email</div>
|
|
|
|
|
<div class="detail-value" id="detailEmail">-</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customer-detail-item">
|
|
|
|
|
<div class="detail-label">Phone</div>
|
|
|
|
|
<div class="detail-value" id="detailPhone">-</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customer-detail-item">
|
|
|
|
|
<div class="detail-label">Location</div>
|
|
|
|
|
<div class="detail-value" id="detailLocation">-</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="customer-detail-item">
|
|
|
|
|
<div class="detail-label">Tags</div>
|
|
|
|
|
<div class="tags-container" id="detailTags">
|
|
|
|
|
<span class="tag">new-customer</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
|
|
|
|
|
<div class="sidebar-section">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<circle cx="12" cy="12" r="10" />
|
|
|
|
|
<polyline points="12 6 12 12 16 14" />
|
|
|
|
|
</svg>
|
|
|
|
|
Previous Conversations
|
|
|
|
|
</div>
|
|
|
|
|
<div id="conversationHistory">
|
|
|
|
|
<div
|
|
|
|
|
class="history-item"
|
|
|
|
|
onclick="loadHistoricalConversation('123')"
|
|
|
|
|
>
|
|
|
|
|
<div class="history-header">
|
|
|
|
|
<span class="history-date"
|
|
|
|
|
>No history available</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="history-summary">
|
|
|
|
|
Select a customer to view their history
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
</aside>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
<!-- Transfer Modal -->
|
|
|
|
|
<div class="modal-overlay" id="transferModal">
|
|
|
|
|
<div class="modal">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h3>Transfer Conversation</h3>
|
|
|
|
|
<button class="modal-close" onclick="closeTransferModal()">
|
|
|
|
|
<svg
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
>
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Transfer to</label>
|
|
|
|
|
<div class="attendant-list" id="attendantList">
|
|
|
|
|
<!-- Loaded dynamically -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Reason (optional)</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class="form-input"
|
|
|
|
|
id="transferReason"
|
|
|
|
|
placeholder="e.g., Technical issue, needs specialist"
|
|
|
|
|
/>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="action-btn" onclick="closeTransferModal()">
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="action-btn primary"
|
|
|
|
|
onclick="confirmTransfer()"
|
|
|
|
|
>
|
|
|
|
|
Transfer
|
|
|
|
|
</button>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Toast Container -->
|
|
|
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// Configuration
|
|
|
|
|
// =====================================================================
|
|
|
|
|
const API_BASE = window.location.origin;
|
|
|
|
|
let currentSessionId = null;
|
|
|
|
|
let currentAttendantId = null;
|
|
|
|
|
let currentAttendantStatus = "online";
|
|
|
|
|
let conversations = [];
|
|
|
|
|
let attendants = [];
|
|
|
|
|
let ws = null;
|
|
|
|
|
let reconnectAttempts = 0;
|
|
|
|
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
|
|
|
|
|
|
|
|
// LLM Assist configuration
|
|
|
|
|
let llmAssistConfig = {
|
|
|
|
|
tips_enabled: false,
|
|
|
|
|
polish_enabled: false,
|
|
|
|
|
smart_replies_enabled: false,
|
|
|
|
|
auto_summary_enabled: false,
|
|
|
|
|
sentiment_enabled: false
|
|
|
|
|
};
|
|
|
|
|
let conversationHistory = [];
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// Initialization
|
|
|
|
|
// =====================================================================
|
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
|
|
|
await checkCRMEnabled();
|
|
|
|
|
setupEventListeners();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function checkCRMEnabled() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/attendants`,
|
|
|
|
|
);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (response.ok && Array.isArray(data)) {
|
|
|
|
|
attendants = data;
|
|
|
|
|
if (attendants.length > 0) {
|
|
|
|
|
// CRM is enabled
|
|
|
|
|
document.getElementById(
|
|
|
|
|
"crmDisabled",
|
|
|
|
|
).style.display = "none";
|
|
|
|
|
document.getElementById(
|
|
|
|
|
"mainLayout",
|
|
|
|
|
).style.display = "grid";
|
|
|
|
|
|
|
|
|
|
// Set current attendant (first one for now, should come from auth)
|
|
|
|
|
currentAttendantId = attendants[0].attendant_id;
|
|
|
|
|
document.getElementById(
|
|
|
|
|
"attendantName",
|
|
|
|
|
).textContent = attendants[0].attendant_name;
|
|
|
|
|
|
|
|
|
|
await loadQueue();
|
|
|
|
|
connectWebSocket();
|
|
|
|
|
} else {
|
|
|
|
|
showCRMDisabled();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
showCRMDisabled();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to check CRM status:", error);
|
|
|
|
|
showCRMDisabled();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showCRMDisabled() {
|
|
|
|
|
document.getElementById("crmDisabled").classList.add("active");
|
|
|
|
|
document.getElementById("mainLayout").style.display = "none";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupEventListeners() {
|
|
|
|
|
// Chat input auto-resize
|
|
|
|
|
const chatInput = document.getElementById("chatInput");
|
|
|
|
|
chatInput.addEventListener("input", function () {
|
|
|
|
|
this.style.height = "auto";
|
|
|
|
|
this.style.height = Math.min(this.scrollHeight, 120) + "px";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send on Enter (without Shift)
|
|
|
|
|
chatInput.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
sendMessage();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close dropdown on outside click
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
if (!e.target.closest("#attendantStatus")) {
|
|
|
|
|
document
|
|
|
|
|
.getElementById("statusDropdown")
|
|
|
|
|
.classList.remove("show");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// Queue Management
|
|
|
|
|
// =====================================================================
|
|
|
|
|
async function loadQueue() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/queue`,
|
|
|
|
|
);
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
conversations = await response.json();
|
|
|
|
|
renderConversations();
|
|
|
|
|
updateStats();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load queue:", error);
|
|
|
|
|
showToast("Failed to load queue", "error");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function renderConversations() {
|
|
|
|
|
const list = document.getElementById("conversationList");
|
|
|
|
|
const emptyState = document.getElementById("emptyQueue");
|
|
|
|
|
|
|
|
|
|
if (conversations.length === 0) {
|
|
|
|
|
emptyState.style.display = "flex";
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
emptyState.style.display = "none";
|
|
|
|
|
|
|
|
|
|
// Sort by priority and waiting time
|
|
|
|
|
conversations.sort((a, b) => {
|
|
|
|
|
if (b.priority !== a.priority)
|
|
|
|
|
return b.priority - a.priority;
|
|
|
|
|
return b.waiting_time_seconds - a.waiting_time_seconds;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
list.innerHTML =
|
|
|
|
|
conversations
|
|
|
|
|
.map(
|
|
|
|
|
(conv) => `
|
|
|
|
|
<div class="conversation-item ${conv.session_id === currentSessionId ? "active" : ""} ${conv.status === "waiting" ? "unread" : ""}"
|
|
|
|
|
onclick="selectConversation('${conv.session_id}')"
|
|
|
|
|
data-session-id="${conv.session_id}">
|
|
|
|
|
<div class="conversation-header">
|
|
|
|
|
<span class="customer-name">${escapeHtml(conv.user_name || "Anonymous")}</span>
|
|
|
|
|
<span class="conversation-time">${formatTime(conv.last_message_time)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="conversation-preview">${escapeHtml(conv.last_message || "No messages")}</div>
|
|
|
|
|
<div class="conversation-meta">
|
|
|
|
|
<span class="channel-tag channel-${conv.channel.toLowerCase()}">${conv.channel}</span>
|
|
|
|
|
${conv.priority >= 2 ? `<span class="priority-tag priority-${conv.priority >= 3 ? "urgent" : "high"}">🔥 ${conv.priority >= 3 ? "Urgent" : "High"}</span>` : ""}
|
|
|
|
|
<span class="waiting-time ${conv.waiting_time_seconds > 300 ? "long" : ""}">${formatWaitTime(conv.waiting_time_seconds)}</span>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.join("") +
|
|
|
|
|
`<div class="empty-queue" id="emptyQueue" style="display: none;">
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
<path d="M20 6L9 17l-5-5"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<p>No conversations in queue</p>
|
|
|
|
|
<small>New conversations will appear here</small>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateStats() {
|
|
|
|
|
const waiting = conversations.filter(
|
|
|
|
|
(c) => c.status === "waiting",
|
|
|
|
|
).length;
|
|
|
|
|
const active = conversations.filter(
|
|
|
|
|
(c) => c.status === "active",
|
|
|
|
|
).length;
|
|
|
|
|
const resolved = conversations.filter(
|
|
|
|
|
(c) => c.status === "resolved",
|
|
|
|
|
).length;
|
|
|
|
|
const mine = conversations.filter(
|
|
|
|
|
(c) => c.assigned_to === currentAttendantId,
|
|
|
|
|
).length;
|
|
|
|
|
|
|
|
|
|
document.getElementById("waitingCount").textContent = waiting;
|
|
|
|
|
document.getElementById("activeCount").textContent = active;
|
|
|
|
|
document.getElementById("resolvedCount").textContent = resolved;
|
|
|
|
|
|
|
|
|
|
document.getElementById("allBadge").textContent =
|
|
|
|
|
conversations.length;
|
|
|
|
|
document.getElementById("waitingBadge").textContent = waiting;
|
|
|
|
|
document.getElementById("mineBadge").textContent = mine;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterQueue(filter) {
|
|
|
|
|
document.querySelectorAll(".filter-btn").forEach((btn) => {
|
|
|
|
|
btn.classList.toggle(
|
|
|
|
|
"active",
|
|
|
|
|
btn.dataset.filter === filter,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const items = document.querySelectorAll(".conversation-item");
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const sessionId = item.dataset.sessionId;
|
|
|
|
|
const conv = conversations.find(
|
|
|
|
|
(c) => c.session_id === sessionId,
|
|
|
|
|
);
|
|
|
|
|
if (!conv) return;
|
|
|
|
|
|
|
|
|
|
let show = true;
|
|
|
|
|
switch (filter) {
|
|
|
|
|
case "waiting":
|
|
|
|
|
show = conv.status === "waiting";
|
|
|
|
|
break;
|
|
|
|
|
case "mine":
|
|
|
|
|
show = conv.assigned_to === currentAttendantId;
|
|
|
|
|
break;
|
|
|
|
|
case "high":
|
|
|
|
|
show = conv.priority >= 2;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
item.style.display = show ? "block" : "none";
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
// =====================================================================
|
|
|
|
|
// Conversation Selection & Chat
|
|
|
|
|
// =====================================================================
|
|
|
|
|
async function selectConversation(sessionId) {
|
|
|
|
|
currentSessionId = sessionId;
|
|
|
|
|
conversationHistory = []; // Reset history for new conversation
|
|
|
|
|
const conv = conversations.find(
|
|
|
|
|
(c) => c.session_id === sessionId,
|
|
|
|
|
);
|
|
|
|
|
if (!conv) return;
|
|
|
|
|
|
|
|
|
|
// Update UI
|
|
|
|
|
document
|
|
|
|
|
.querySelectorAll(".conversation-item")
|
|
|
|
|
.forEach((item) => {
|
|
|
|
|
item.classList.toggle(
|
|
|
|
|
"active",
|
|
|
|
|
item.dataset.sessionId === sessionId,
|
|
|
|
|
);
|
|
|
|
|
if (item.dataset.sessionId === sessionId) {
|
|
|
|
|
item.classList.remove("unread");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("noConversation").style.display =
|
|
|
|
|
"none";
|
|
|
|
|
document.getElementById("activeChat").style.display = "flex";
|
|
|
|
|
|
|
|
|
|
// Update header
|
|
|
|
|
document.getElementById("customerAvatar").textContent =
|
|
|
|
|
(conv.user_name || "A")[0].toUpperCase();
|
|
|
|
|
document.getElementById("customerName").textContent =
|
|
|
|
|
conv.user_name || "Anonymous";
|
|
|
|
|
document.getElementById("customerChannel").textContent =
|
|
|
|
|
conv.channel;
|
|
|
|
|
document.getElementById("customerChannel").className =
|
|
|
|
|
`channel-tag channel-${conv.channel.toLowerCase()}`;
|
|
|
|
|
|
|
|
|
|
// Show customer details
|
|
|
|
|
document.getElementById("customerDetails").style.display =
|
|
|
|
|
"block";
|
|
|
|
|
document.getElementById("detailEmail").textContent =
|
|
|
|
|
conv.user_email || "-";
|
|
|
|
|
|
|
|
|
|
// Load messages
|
|
|
|
|
await loadMessages(sessionId);
|
|
|
|
|
|
|
|
|
|
// Load AI insights
|
|
|
|
|
await loadInsights(sessionId);
|
|
|
|
|
|
|
|
|
|
// Assign to self if unassigned
|
|
|
|
|
if (!conv.assigned_to && currentAttendantId) {
|
|
|
|
|
await assignConversation(sessionId, currentAttendantId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadMessages(sessionId) {
|
|
|
|
|
const container = document.getElementById("chatMessages");
|
|
|
|
|
container.innerHTML = '<div class="loading-spinner"></div>';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// For now, show the last message from queue data
|
|
|
|
|
const conv = conversations.find(
|
|
|
|
|
(c) => c.session_id === sessionId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// In real implementation, fetch from /api/sessions/{id}/messages
|
|
|
|
|
container.innerHTML = "";
|
|
|
|
|
|
|
|
|
|
if (conv && conv.last_message) {
|
|
|
|
|
addMessage(
|
|
|
|
|
"customer",
|
|
|
|
|
conv.last_message,
|
|
|
|
|
conv.last_message_time,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add system message for transfer
|
|
|
|
|
if (conv && conv.assigned_to_name) {
|
|
|
|
|
addSystemMessage(
|
|
|
|
|
`Assigned to ${conv.assigned_to_name}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load messages:", error);
|
|
|
|
|
container.innerHTML =
|
|
|
|
|
'<p style="text-align: center; color: var(--text-muted);">Failed to load messages</p>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addMessage(type, content, time = null) {
|
|
|
|
|
const container = document.getElementById("chatMessages");
|
|
|
|
|
const timeStr = time
|
|
|
|
|
? formatTime(time)
|
|
|
|
|
: new Date().toLocaleTimeString([], {
|
|
|
|
|
hour: "2-digit",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const avatarContent =
|
|
|
|
|
type === "customer" ? "C" : type === "bot" ? "🤖" : "You";
|
|
|
|
|
const avatarClass = type === "bot" ? "bot" : "";
|
|
|
|
|
|
|
|
|
|
const messageHtml = `
|
|
|
|
|
<div class="message ${type}">
|
|
|
|
|
<div class="message-avatar ${avatarClass}">${avatarContent}</div>
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
<div class="message-bubble">${escapeHtml(content)}</div>
|
|
|
|
|
<div class="message-meta">
|
|
|
|
|
<span>${timeStr}</span>
|
|
|
|
|
${type === "bot" ? '<span class="bot-badge">Bot</span>' : ""}
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
`;
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
container.insertAdjacentHTML("beforeend", messageHtml);
|
|
|
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function addSystemMessage(content) {
|
|
|
|
|
const container = document.getElementById("chatMessages");
|
|
|
|
|
const messageHtml = `
|
|
|
|
|
<div class="message system">
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
<div class="message-bubble">${escapeHtml(content)}</div>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
`;
|
|
|
|
|
container.insertAdjacentHTML("beforeend", messageHtml);
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
async function sendMessage() {
|
|
|
|
|
const input = document.getElementById("chatInput");
|
|
|
|
|
const message = input.value.trim();
|
|
|
|
|
|
|
|
|
|
if (!message || !currentSessionId) return;
|
|
|
|
|
|
|
|
|
|
input.value = "";
|
|
|
|
|
input.style.height = "auto";
|
|
|
|
|
|
|
|
|
|
// Add to UI immediately
|
|
|
|
|
addMessage("attendant", message);
|
|
|
|
|
|
|
|
|
|
// Add to conversation history
|
|
|
|
|
conversationHistory.push({
|
|
|
|
|
role: "attendant",
|
|
|
|
|
content: message,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Send to attendance respond API
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/respond`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: currentSessionId,
|
|
|
|
|
message: message,
|
|
|
|
|
attendant_id: currentAttendantId,
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
result.error || "Failed to send message",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showToast(result.message, "success");
|
|
|
|
|
|
|
|
|
|
// Refresh smart replies after sending
|
|
|
|
|
if (llmAssistConfig.smart_replies_enabled) {
|
|
|
|
|
loadSmartReplies(currentSessionId);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to send message:", error);
|
|
|
|
|
showToast(
|
|
|
|
|
"Failed to send message: " + error.message,
|
|
|
|
|
"error",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function useQuickResponse(text) {
|
|
|
|
|
document.getElementById("chatInput").value = text;
|
|
|
|
|
document.getElementById("chatInput").focus();
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function useSuggestion(element) {
|
|
|
|
|
const text = element
|
|
|
|
|
.querySelector(".suggested-reply-text")
|
|
|
|
|
.textContent.trim();
|
|
|
|
|
document.getElementById("chatInput").value = text;
|
|
|
|
|
document.getElementById("chatInput").focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// Transfer & Assignment
|
|
|
|
|
// =====================================================================
|
|
|
|
|
async function assignConversation(sessionId, attendantId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/assign`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
attendant_id: attendantId,
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
showToast("Conversation assigned", "success");
|
|
|
|
|
await loadQueue();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to assign conversation:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function showTransferModal() {
|
|
|
|
|
if (!currentSessionId) return;
|
|
|
|
|
|
|
|
|
|
const list = document.getElementById("attendantList");
|
|
|
|
|
list.innerHTML = attendants
|
|
|
|
|
.filter((a) => a.attendant_id !== currentAttendantId)
|
|
|
|
|
.map(
|
|
|
|
|
(a) => `
|
|
|
|
|
<div class="attendant-option" onclick="selectTransferTarget(this, '${a.attendant_id}')">
|
|
|
|
|
<div class="status-indicator ${a.status.toLowerCase()}"></div>
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-weight: 500;">${escapeHtml(a.attendant_name)}</div>
|
|
|
|
|
<div style="font-size: 12px; color: var(--text-secondary);">${a.preferences} • ${a.channel}</div>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.join("");
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
document.getElementById("transferModal").classList.add("show");
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function closeTransferModal() {
|
|
|
|
|
document
|
|
|
|
|
.getElementById("transferModal")
|
|
|
|
|
.classList.remove("show");
|
|
|
|
|
document.getElementById("transferReason").value = "";
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
let selectedTransferTarget = null;
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
function selectTransferTarget(element, attendantId) {
|
|
|
|
|
document
|
|
|
|
|
.querySelectorAll(".attendant-option")
|
|
|
|
|
.forEach((el) => el.classList.remove("selected"));
|
|
|
|
|
element.classList.add("selected");
|
|
|
|
|
selectedTransferTarget = attendantId;
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
async function confirmTransfer() {
|
|
|
|
|
if (!selectedTransferTarget || !currentSessionId) {
|
|
|
|
|
showToast("Please select an attendant", "warning");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reason = document.getElementById("transferReason").value;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/transfer`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: currentSessionId,
|
|
|
|
|
from_attendant_id: currentAttendantId,
|
|
|
|
|
to_attendant_id: selectedTransferTarget,
|
|
|
|
|
reason: reason,
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
showToast("Conversation transferred", "success");
|
|
|
|
|
closeTransferModal();
|
|
|
|
|
currentSessionId = null;
|
|
|
|
|
document.getElementById(
|
|
|
|
|
"noConversation",
|
|
|
|
|
).style.display = "flex";
|
|
|
|
|
document.getElementById("activeChat").style.display =
|
|
|
|
|
"none";
|
|
|
|
|
await loadQueue();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Transfer failed");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to transfer:", error);
|
|
|
|
|
showToast("Failed to transfer conversation", "error");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveConversation() {
|
|
|
|
|
if (!currentSessionId) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/resolve/${currentSessionId}`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
showToast("Conversation resolved", "success");
|
|
|
|
|
currentSessionId = null;
|
|
|
|
|
document.getElementById(
|
|
|
|
|
"noConversation",
|
|
|
|
|
).style.display = "flex";
|
|
|
|
|
document.getElementById("activeChat").style.display =
|
|
|
|
|
"none";
|
|
|
|
|
await loadQueue();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Failed to resolve");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to resolve:", error);
|
|
|
|
|
showToast("Failed to resolve conversation", "error");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// Status Management
|
|
|
|
|
// =====================================================================
|
|
|
|
|
function toggleStatusDropdown() {
|
|
|
|
|
document
|
|
|
|
|
.getElementById("statusDropdown")
|
|
|
|
|
.classList.toggle("show");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function setStatus(status) {
|
|
|
|
|
currentAttendantStatus = status;
|
|
|
|
|
document.getElementById("statusIndicator").className =
|
|
|
|
|
`status-indicator ${status}`;
|
|
|
|
|
document
|
|
|
|
|
.getElementById("statusDropdown")
|
|
|
|
|
.classList.remove("show");
|
|
|
|
|
|
|
|
|
|
const statusTexts = {
|
|
|
|
|
online: "Online - Ready for conversations",
|
|
|
|
|
busy: "Busy - Handling conversations",
|
|
|
|
|
away: "Away - Temporarily unavailable",
|
|
|
|
|
offline: "Offline - Not accepting conversations",
|
|
|
|
|
};
|
|
|
|
|
document.getElementById("statusText").textContent =
|
|
|
|
|
statusTexts[status];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await fetch(
|
|
|
|
|
`${API_BASE}/api/attendance/attendants/${currentAttendantId}/status`,
|
|
|
|
|
{
|
|
|
|
|
method: "PUT",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ status: status }),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to update status:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// AI Insights
|
|
|
|
|
// =====================================================================
|
|
|
|
|
async function loadInsights(sessionId) {
|
|
|
|
|
// Update sentiment (loading state)
|
|
|
|
|
document.getElementById("sentimentValue").innerHTML =
|
|
|
|
|
"😐 Analyzing...";
|
|
|
|
|
document.getElementById("intentValue").textContent =
|
|
|
|
|
"Analyzing conversation...";
|
|
|
|
|
document.getElementById("summaryValue").textContent =
|
|
|
|
|
"Loading summary...";
|
|
|
|
|
|
|
|
|
|
const conv = conversations.find(c => c.session_id === sessionId);
|
|
|
|
|
|
|
|
|
|
// Load LLM Assist config for this bot
|
|
|
|
|
try {
|
|
|
|
|
const configResponse = await fetch(`${API_BASE}/api/attendance/llm/config/${conv?.bot_id || 'default'}`);
|
|
|
|
|
if (configResponse.ok) {
|
|
|
|
|
llmAssistConfig = await configResponse.json();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log("LLM config not available, using defaults");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load real insights using LLM Assist APIs
|
|
|
|
|
try {
|
|
|
|
|
// Generate summary if enabled
|
|
|
|
|
if (llmAssistConfig.auto_summary_enabled) {
|
|
|
|
|
const summaryResponse = await fetch(`${API_BASE}/api/attendance/llm/summary/${sessionId}`);
|
|
|
|
|
if (summaryResponse.ok) {
|
|
|
|
|
const summaryData = await summaryResponse.json();
|
|
|
|
|
if (summaryData.success) {
|
|
|
|
|
document.getElementById("summaryValue").textContent = summaryData.summary.brief || "No summary available";
|
|
|
|
|
document.getElementById("intentValue").textContent =
|
|
|
|
|
summaryData.summary.customer_needs?.join(", ") || "General inquiry";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById("summaryValue").textContent =
|
|
|
|
|
`Customer ${conv?.user_name || "Anonymous"} via ${conv?.channel || "web"}`;
|
|
|
|
|
document.getElementById("intentValue").textContent = "General inquiry";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Analyze sentiment if we have the last message
|
|
|
|
|
if (llmAssistConfig.sentiment_enabled && conv?.last_message) {
|
|
|
|
|
const sentimentResponse = await fetch(`${API_BASE}/api/attendance/llm/sentiment`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
message: conv.last_message,
|
|
|
|
|
history: conversationHistory
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
if (sentimentResponse.ok) {
|
|
|
|
|
const sentimentData = await sentimentResponse.json();
|
|
|
|
|
if (sentimentData.success) {
|
|
|
|
|
const s = sentimentData.sentiment;
|
|
|
|
|
const sentimentClass = s.overall === 'positive' ? 'sentiment-positive' :
|
|
|
|
|
s.overall === 'negative' ? 'sentiment-negative' : 'sentiment-neutral';
|
|
|
|
|
document.getElementById("sentimentValue").innerHTML =
|
|
|
|
|
`<span class="sentiment-indicator ${sentimentClass}">${s.emoji} ${s.overall.charAt(0).toUpperCase() + s.overall.slice(1)}</span>`;
|
|
|
|
|
|
|
|
|
|
// Show warning for high escalation risk
|
|
|
|
|
if (s.escalation_risk === 'high') {
|
|
|
|
|
showToast("⚠️ High escalation risk detected", "warning");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById("sentimentValue").innerHTML =
|
|
|
|
|
`<span class="sentiment-indicator sentiment-neutral">😐 Neutral</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate smart replies if enabled
|
|
|
|
|
if (llmAssistConfig.smart_replies_enabled) {
|
|
|
|
|
await loadSmartReplies(sessionId);
|
|
|
|
|
} else {
|
|
|
|
|
loadDefaultReplies();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load insights:", error);
|
|
|
|
|
// Show fallback data
|
|
|
|
|
document.getElementById("sentimentValue").innerHTML =
|
|
|
|
|
`<span class="sentiment-indicator sentiment-neutral">😐 Neutral</span>`;
|
|
|
|
|
document.getElementById("summaryValue").textContent =
|
|
|
|
|
`Customer ${conv?.user_name || "Anonymous"} via ${conv?.channel || "web"}`;
|
|
|
|
|
loadDefaultReplies();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load smart replies from LLM
|
|
|
|
|
async function loadSmartReplies(sessionId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/attendance/llm/smart-replies`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
history: conversationHistory
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success && data.replies.length > 0) {
|
|
|
|
|
const repliesHtml = data.replies.map(reply => `
|
|
|
|
|
<div class="suggested-reply" onclick="useSuggestion(this)">
|
|
|
|
|
<div class="suggested-reply-text">${escapeHtml(reply.text)}</div>
|
|
|
|
|
<div class="suggestion-meta">
|
|
|
|
|
<span class="suggestion-confidence">${Math.round(reply.confidence * 100)}% match</span>
|
|
|
|
|
<span class="suggestion-source">${reply.tone} • AI</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
document.getElementById("suggestedReplies").innerHTML = repliesHtml;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Failed to load smart replies:", e);
|
|
|
|
|
}
|
|
|
|
|
loadDefaultReplies();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load default replies when LLM is unavailable
|
|
|
|
|
function loadDefaultReplies() {
|
|
|
|
|
document.getElementById("suggestedReplies").innerHTML = `
|
|
|
|
|
<div class="suggested-reply" onclick="useSuggestion(this)">
|
|
|
|
|
<div class="suggested-reply-text">Hello! Thank you for reaching out. How can I assist you today?</div>
|
|
|
|
|
<div class="suggestion-meta">
|
|
|
|
|
<span class="suggestion-confidence">Template</span>
|
|
|
|
|
<span class="suggestion-source">Quick Reply</span>
|
|
|
|
|
</div>
|
2025-12-03 18:42:22 -03:00
|
|
|
</div>
|
2025-12-05 13:47:42 -03:00
|
|
|
<div class="suggested-reply" onclick="useSuggestion(this)">
|
|
|
|
|
<div class="suggested-reply-text">I'd be happy to help you with that. Let me look into it.</div>
|
|
|
|
|
<div class="suggestion-meta">
|
|
|
|
|
<span class="suggestion-confidence">Template</span>
|
|
|
|
|
<span class="suggestion-source">Quick Reply</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="suggested-reply" onclick="useSuggestion(this)">
|
|
|
|
|
<div class="suggested-reply-text">Is there anything else I can help you with?</div>
|
|
|
|
|
<div class="suggestion-meta">
|
|
|
|
|
<span class="suggestion-confidence">Template</span>
|
|
|
|
|
<span class="suggestion-source">Quick Reply</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate tips when customer message arrives
|
|
|
|
|
async function generateTips(sessionId, customerMessage) {
|
|
|
|
|
if (!llmAssistConfig.tips_enabled) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/attendance/llm/tips`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
customer_message: customerMessage,
|
|
|
|
|
history: conversationHistory
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success && data.tips.length > 0) {
|
|
|
|
|
displayTips(data.tips);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Failed to generate tips:", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Display tips in the UI
|
|
|
|
|
function displayTips(tips) {
|
|
|
|
|
const tipsContainer = document.getElementById("tipsContainer");
|
|
|
|
|
if (!tipsContainer) {
|
|
|
|
|
// Create tips container if it doesn't exist
|
|
|
|
|
const insightsSection = document.querySelector(".insights-sidebar .sidebar-section");
|
|
|
|
|
if (insightsSection) {
|
|
|
|
|
const tipsDiv = document.createElement("div");
|
|
|
|
|
tipsDiv.id = "tipsContainer";
|
|
|
|
|
tipsDiv.className = "ai-insight";
|
|
|
|
|
tipsDiv.innerHTML = `
|
|
|
|
|
<div class="insight-header">
|
|
|
|
|
<span class="insight-icon">💡</span>
|
|
|
|
|
<span class="insight-label">Tips</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="insight-value" id="tipsValue"></div>
|
|
|
|
|
`;
|
|
|
|
|
insightsSection.insertBefore(tipsDiv, insightsSection.firstChild);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tipsValue = document.getElementById("tipsValue");
|
|
|
|
|
if (tipsValue) {
|
|
|
|
|
const tipsHtml = tips.map(tip => {
|
|
|
|
|
const emoji = tip.tip_type === 'warning' ? '⚠️' :
|
|
|
|
|
tip.tip_type === 'intent' ? '🎯' :
|
|
|
|
|
tip.tip_type === 'action' ? '✅' : '💡';
|
|
|
|
|
return `<div style="margin-bottom: 8px;">${emoji} ${escapeHtml(tip.content)}</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
tipsValue.innerHTML = tipsHtml;
|
|
|
|
|
|
|
|
|
|
// Show toast for high priority tips
|
|
|
|
|
const highPriorityTip = tips.find(t => t.priority === 1);
|
|
|
|
|
if (highPriorityTip) {
|
|
|
|
|
showToast(`💡 ${highPriorityTip.content}`, "info");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Polish message before sending
|
|
|
|
|
async function polishMessage() {
|
|
|
|
|
if (!llmAssistConfig.polish_enabled) {
|
|
|
|
|
showToast("Message polish feature is disabled", "info");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const input = document.getElementById("chatInput");
|
|
|
|
|
const message = input.value.trim();
|
|
|
|
|
|
|
|
|
|
if (!message || !currentSessionId) {
|
|
|
|
|
showToast("Enter a message first", "info");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showToast("✨ Polishing message...", "info");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/attendance/llm/polish`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: currentSessionId,
|
|
|
|
|
message: message,
|
|
|
|
|
tone: "professional"
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success && data.polished !== message) {
|
|
|
|
|
input.value = data.polished;
|
|
|
|
|
input.style.height = "auto";
|
|
|
|
|
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
|
|
|
|
|
|
|
|
|
if (data.changes.length > 0) {
|
|
|
|
|
showToast(`✨ Message polished: ${data.changes.join(", ")}`, "success");
|
|
|
|
|
} else {
|
|
|
|
|
showToast("✨ Message polished!", "success");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
showToast("Message looks good already!", "success");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Failed to polish message:", e);
|
|
|
|
|
showToast("Failed to polish message", "error");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// WebSocket
|
|
|
|
|
// =====================================================================
|
|
|
|
|
function connectWebSocket() {
|
|
|
|
|
if (!currentAttendantId) {
|
|
|
|
|
console.warn(
|
|
|
|
|
"No attendant ID, skipping WebSocket connection",
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const protocol =
|
|
|
|
|
window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
|
|
|
ws = new WebSocket(
|
|
|
|
|
`${protocol}//${window.location.host}/ws/attendant?attendant_id=${encodeURIComponent(currentAttendantId)}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
console.log(
|
|
|
|
|
"WebSocket connected for attendant:",
|
|
|
|
|
currentAttendantId,
|
|
|
|
|
);
|
|
|
|
|
reconnectAttempts = 0;
|
|
|
|
|
showToast(
|
|
|
|
|
"Connected to notification service",
|
|
|
|
|
"success",
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onmessage = (event) => {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
console.log("WebSocket message received:", data);
|
|
|
|
|
handleWebSocketMessage(data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onclose = () => {
|
|
|
|
|
console.log("WebSocket disconnected");
|
|
|
|
|
attemptReconnect();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onerror = (error) => {
|
|
|
|
|
console.error("WebSocket error:", error);
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to connect WebSocket:", error);
|
|
|
|
|
attemptReconnect();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function attemptReconnect() {
|
|
|
|
|
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
|
|
|
reconnectAttempts++;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
console.log(
|
|
|
|
|
`Reconnecting... attempt ${reconnectAttempts}`,
|
|
|
|
|
);
|
|
|
|
|
connectWebSocket();
|
|
|
|
|
}, 2000 * reconnectAttempts);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleWebSocketMessage(data) {
|
|
|
|
|
const msgType = data.type || data.notification_type;
|
|
|
|
|
|
|
|
|
|
switch (msgType) {
|
|
|
|
|
case "connected":
|
|
|
|
|
console.log("WebSocket connected:", data.message);
|
|
|
|
|
break;
|
|
|
|
|
case "new_conversation":
|
|
|
|
|
showToast("New conversation in queue", "info");
|
|
|
|
|
loadQueue();
|
|
|
|
|
// Play notification sound
|
|
|
|
|
playNotificationSound();
|
2025-12-03 18:42:22 -03:00
|
|
|
break;
|
2025-12-05 13:47:42 -03:00
|
|
|
case "new_message":
|
|
|
|
|
// Message from customer
|
|
|
|
|
showToast(
|
|
|
|
|
`New message from ${data.user_name || "Customer"}`,
|
|
|
|
|
"info",
|
|
|
|
|
);
|
|
|
|
|
if (data.session_id === currentSessionId) {
|
|
|
|
|
addMessage(
|
|
|
|
|
"customer",
|
|
|
|
|
data.content,
|
|
|
|
|
data.timestamp,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Add to conversation history for context
|
|
|
|
|
conversationHistory.push({
|
|
|
|
|
role: "customer",
|
|
|
|
|
content: data.content,
|
|
|
|
|
timestamp: data.timestamp || new Date().toISOString()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Generate tips for this new message
|
|
|
|
|
generateTips(data.session_id, data.content);
|
|
|
|
|
|
|
|
|
|
// Refresh sentiment analysis
|
|
|
|
|
if (llmAssistConfig.sentiment_enabled) {
|
|
|
|
|
loadInsights(data.session_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
loadQueue();
|
|
|
|
|
playNotificationSound();
|
2025-12-03 18:42:22 -03:00
|
|
|
break;
|
2025-12-05 13:47:42 -03:00
|
|
|
case "attendant_response":
|
|
|
|
|
// Response from another attendant
|
|
|
|
|
if (
|
|
|
|
|
data.session_id === currentSessionId &&
|
|
|
|
|
data.assigned_to !== currentAttendantId
|
|
|
|
|
) {
|
|
|
|
|
addMessage(
|
|
|
|
|
"attendant",
|
|
|
|
|
data.content,
|
|
|
|
|
data.timestamp,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
break;
|
2025-12-05 13:47:42 -03:00
|
|
|
case "queue_update":
|
|
|
|
|
loadQueue();
|
|
|
|
|
break;
|
|
|
|
|
case "transfer":
|
|
|
|
|
if (data.assigned_to === currentAttendantId) {
|
|
|
|
|
showToast(
|
|
|
|
|
`Conversation transferred to you`,
|
|
|
|
|
"info",
|
|
|
|
|
);
|
|
|
|
|
loadQueue();
|
|
|
|
|
playNotificationSound();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.log(
|
|
|
|
|
"Unknown WebSocket message type:",
|
|
|
|
|
msgType,
|
|
|
|
|
data,
|
|
|
|
|
);
|
2025-12-03 18:42:22 -03:00
|
|
|
}
|
2025-12-05 13:47:42 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function playNotificationSound() {
|
|
|
|
|
// Create a simple beep sound
|
|
|
|
|
try {
|
|
|
|
|
const audioContext = new (window.AudioContext ||
|
|
|
|
|
window.webkitAudioContext)();
|
|
|
|
|
const oscillator = audioContext.createOscillator();
|
|
|
|
|
const gainNode = audioContext.createGain();
|
|
|
|
|
|
|
|
|
|
oscillator.connect(gainNode);
|
|
|
|
|
gainNode.connect(audioContext.destination);
|
|
|
|
|
|
|
|
|
|
oscillator.frequency.value = 800;
|
|
|
|
|
oscillator.type = "sine";
|
|
|
|
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
|
|
|
|
gainNode.gain.exponentialRampToValueAtTime(
|
|
|
|
|
0.01,
|
|
|
|
|
audioContext.currentTime + 0.3,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
oscillator.start(audioContext.currentTime);
|
|
|
|
|
oscillator.stop(audioContext.currentTime + 0.3);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Audio not available
|
|
|
|
|
console.log("Could not play notification sound");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
// Utility Functions
|
|
|
|
|
// =====================================================================
|
|
|
|
|
function escapeHtml(text) {
|
|
|
|
|
const div = document.createElement("div");
|
|
|
|
|
div.textContent = text || "";
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTime(timestamp) {
|
|
|
|
|
if (!timestamp) return "";
|
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const diff = (now - date) / 1000;
|
|
|
|
|
|
|
|
|
|
if (diff < 60) return "Just now";
|
|
|
|
|
if (diff < 3600) return `${Math.floor(diff / 60)} min`;
|
|
|
|
|
if (diff < 86400)
|
|
|
|
|
return date.toLocaleTimeString([], {
|
|
|
|
|
hour: "2-digit",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
});
|
|
|
|
|
return date.toLocaleDateString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatWaitTime(seconds) {
|
|
|
|
|
if (!seconds || seconds < 0) return "";
|
|
|
|
|
if (seconds < 60) return `${seconds}s`;
|
|
|
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
|
|
|
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showToast(message, type = "info") {
|
|
|
|
|
const container = document.getElementById("toastContainer");
|
|
|
|
|
const toast = document.createElement("div");
|
|
|
|
|
toast.className = `toast ${type}`;
|
|
|
|
|
toast.innerHTML = `
|
|
|
|
|
<span>${escapeHtml(message)}</span>
|
|
|
|
|
`;
|
|
|
|
|
container.appendChild(toast);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
toast.style.opacity = "0";
|
|
|
|
|
setTimeout(() => toast.remove(), 300);
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function attachFile() {
|
|
|
|
|
showToast("File attachment coming soon", "info");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function insertEmoji() {
|
|
|
|
|
showToast("Emoji picker coming soon", "info");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadHistoricalConversation(id) {
|
|
|
|
|
showToast("Loading conversation history...", "info");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Periodic refresh (every 30 seconds if WebSocket not connected)
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
if (currentAttendantStatus === "online") {
|
|
|
|
|
// Only refresh if WebSocket is not connected
|
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
|
|
|
loadQueue();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
// Send status updates via WebSocket
|
|
|
|
|
function sendWebSocketMessage(data) {
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
ws.send(JSON.stringify(data));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
|
2025-12-05 13:47:42 -03:00
|
|
|
// Send typing indicator
|
|
|
|
|
function sendTypingIndicator() {
|
|
|
|
|
if (currentSessionId) {
|
|
|
|
|
sendWebSocketMessage({
|
|
|
|
|
type: "typing",
|
|
|
|
|
session_id: currentSessionId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark messages as read
|
|
|
|
|
function markAsRead(sessionId) {
|
|
|
|
|
sendWebSocketMessage({
|
|
|
|
|
type: "read",
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
2025-12-03 18:42:22 -03:00
|
|
|
</html>
|