diff --git a/TASKS.md b/TASKS.md index 2266669..ccbacf8 100644 --- a/TASKS.md +++ b/TASKS.md @@ -5,182 +5,55 @@ This document lists ALL HTML files in `botui/ui/suite` that contain inline `` and ` -``` - -### Phase 3: Verification - [ ] All HTML files have no inline ` - - + + diff --git a/ui/suite/analytics/analytics.html b/ui/suite/analytics/analytics.html index 42f2ba3..a114e9a 100644 --- a/ui/suite/analytics/analytics.html +++ b/ui/suite/analytics/analytics.html @@ -534,200 +534,6 @@ - - - + + diff --git a/ui/suite/attendant/attendant.css b/ui/suite/attendant/attendant.css index 68f1e17..5048c28 100644 --- a/ui/suite/attendant/attendant.css +++ b/ui/suite/attendant/attendant.css @@ -327,3 +327,1182 @@ display: none; } } + + /* Attendant uses global theme variables from base.css directly */ + + /* Main Layout */ + .attendant-layout { + display: grid; + grid-template-columns: 320px 1fr 380px; + height: calc(100vh - 64px); + background: var(--bg, #0f172a); + color: var(--text, #f1f5f9); + } + + /* CRM Disabled State */ + .crm-disabled { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + height: calc(100vh - 64px); + text-align: center; + padding: 40px; + background: var(--bg, #0f172a); + color: var(--text, #f1f5f9); + } + + .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, #94a3b8); + max-width: 500px; + line-height: 1.6; + margin-bottom: 24px; + } + + .crm-disabled code { + display: block; + background: var(--surface-hover, #334155); + padding: 16px 24px; + border-radius: 8px; + font-family: "Monaco", "Menlo", monospace; + font-size: 14px; + margin-top: 16px; + } + + /* Left Sidebar - Queue */ + .queue-sidebar { + background: var(--surface, #1e293b); + border-right: 1px solid var(--border, #334155); + display: flex; + flex-direction: column; + overflow: hidden; + } + + .queue-header { + padding: 20px; + border-bottom: 1px solid var(--border, #334155); + } + + .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(--surface-hover, #334155); + border-radius: 8px; + cursor: pointer; + position: relative; + } + + .status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--success, #10b981); + transition: background 0.3s; + } + + .status-indicator.online { + background: var(--success, #10b981); + } + .status-indicator.busy { + background: var(--warning, #f59e0b); + } + .status-indicator.away { + background: var(--text-tertiary, #64748b); + } + .status-indicator.offline { + background: var(--error, #ef4444); + } + + @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: 600; + font-size: 14px; + } + + .status-text { + font-size: 12px; + color: var(--text-secondary, #94a3b8); + } + + .status-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--surface, #1e293b); + border: 1px solid var(--border, #334155); + border-radius: 8px; + margin-top: 4px; + padding: 8px; + display: none; + z-index: 100; + } + + .status-dropdown.show { + display: block; + } + + .status-option { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; + } + + .status-option:hover { + background: var(--surface-hover, #334155); + } + + /* Queue Stats */ + .queue-stats { + display: flex; + gap: 8px; + padding: 16px 20px; + border-bottom: 1px solid var(--border, #334155); + } + + .stat-item { + flex: 1; + text-align: center; + padding: 8px; + background: var(--surface-hover, #334155); + border-radius: 8px; + } + + .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; + background: var(--surface-hover, #334155); + border: 1px solid transparent; + border-radius: 20px; + color: var(--text-secondary, #94a3b8); + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 6px; + } + + .filter-btn:hover { + background: var(--border-light, #475569); + } + + .filter-btn.active { + background: var(--primary, #3b82f6); + 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: 8px; + top: 50%; + transform: translateY(-50%); + width: 8px; + height: 8px; + background: var(--primary, #3b82f6); + border-radius: 50%; + } + + .conversation-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + } + + .customer-name { + font-weight: 500; + font-size: 14px; + } + + .conversation-time { + font-size: 11px; + color: var(--text-tertiary, #64748b); + } + + .conversation-preview { + font-size: 13px; + color: var(--text-secondary, #94a3b8); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 8px; + } + + .conversation-meta { + display: flex; + gap: 8px; + align-items: center; + } + + .channel-tag { + font-size: 10px; + padding: 2px 8px; + border-radius: 12px; + background: var(--border-light, #475569); + color: var(--text-secondary, #94a3b8); + } + + .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-tertiary, #64748b); + } + + .waiting-time.long { + color: var(--error, #ef4444); + } + + /* 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, #94a3b8); + } + + .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, #0f172a); + } + + .no-conversation { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-secondary, #94a3b8); + text-align: center; + } + + .no-conversation svg { + width: 80px; + height: 80px; + margin-bottom: 16px; + opacity: 0.3; + } + + .chat-header { + padding: 16px 20px; + background: var(--surface, #1e293b); + border-bottom: 1px solid var(--border, #334155); + display: flex; + justify-content: space-between; + align-items: center; + } + + .customer-info { + display: flex; + align-items: center; + gap: 12px; + } + + .customer-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--primary, #3b82f6); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 18px; + } + + .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: flex; + gap: 4px; + } + + .typing-indicator span { + width: 6px; + height: 6px; + background: var(--text-tertiary, #64748b); + border-radius: 50%; + animation: typing 1s 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 */ + /* Messages Area */ + .chat-messages { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; + background: var(--bg, #0f172a); + } + + .message { + display: flex; + gap: 12px; + max-width: 80%; + } + + .message.customer { + align-self: flex-start; + } + + .message.attendant { + flex-direction: row-reverse; + align-self: flex-end; + } + + .message.bot { + align-self: flex-start; + } + + .message.system { + align-self: center; + max-width: none; + } + + .message-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--surface-hover, #334155); + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + 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: 16px; + background: var(--surface-hover, #334155); + max-width: 100%; + } + + .message.customer .message-bubble { + background: var(--surface-hover, #334155); + } + + .message.attendant .message-bubble { + background: var(--primary, #3b82f6); + color: white; + } + + .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 */ + /* Input Area */ + .chat-input-area { + padding: 16px 20px; + background: var(--surface, #1e293b); + border-top: 1px solid var(--border, #334155); + } + + .quick-responses { + display: flex; + gap: 8px; + margin-bottom: 12px; + flex-wrap: wrap; + } + + .quick-response-btn { + padding: 8px 12px; + background: var(--surface-hover, #334155); + border: 1px solid var(--border, #334155); + border-radius: 8px; + color: var(--text-secondary, #94a3b8); + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + } + + .quick-response-btn:hover { + background: var(--border-light, #475569); + color: var(--text, #f1f5f9); + } + + .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(--surface-hover, #334155); + color: var(--text-secondary, #94a3b8); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + } + + .input-action-btn:hover { + background: var(--border-light, #475569); + color: var(--text, #f1f5f9); + } + + .chat-input { + flex: 1; + padding: 12px 16px; + border: 1px solid var(--border, #334155); + border-radius: 12px; + background: var(--surface-hover, #334155); + color: var(--text, #f1f5f9); + font-size: 14px; + resize: none; + min-height: 44px; + max-height: 120px; + font-family: inherit; + } + + .chat-input:focus { + outline: none; + border-color: var(--primary, #3b82f6); + } + + .chat-input::placeholder { + color: var(--text-tertiary, #64748b); + } + + .send-btn { + width: 44px; + height: 44px; + border: none; + border-radius: 12px; + background: var(--primary, #3b82f6); + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + } + + .send-btn:hover { + background: var(--primary-hover, #2563eb); + } + + .send-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + /* Right Sidebar - Insights */ + .insights-sidebar { + background: var(--surface, #1e293b); + border-left: 1px solid var(--border, #334155); + overflow-y: auto; + display: flex; + flex-direction: column; + } + + .sidebar-section { + padding: 20px; + border-bottom: 1px solid var(--border, #334155); + } + + .section-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary, #94a3b8); + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; + } + + /* AI Insights */ + .ai-insight { + background: var(--primary-light, rgba(59, 130, 246, 0.1)); + border: 1px solid var(--border, #334155); + 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, #94a3b8); + } + + .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(--border-light, #475569); + } + + .suggested-reply-text { + font-size: 13px; + line-height: 1.4; + margin-bottom: 6px; + } + + .suggestion-meta { + display: flex; + justify-content: space-between; + margin-top: 8px; + } + + .suggestion-confidence { + font-size: 11px; + color: var(--success, #10b981); + } + + .suggestion-source { + font-size: 11px; + color: var(--text-tertiary, #64748b); + } + + /* Customer Details */ + .customer-detail-item { + margin-bottom: 16px; + } + + .detail-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-tertiary, #64748b); + margin-bottom: 4px; + } + + .detail-value { + font-size: 14px; + color: var(--text-primary); + } + + .detail-value a { + color: var(--primary, #3b82f6); + text-decoration: none; + } + + .detail-value a:hover { + text-decoration: underline; + } + + /* Conversation History */ + .history-item { + padding: 12px; + background: var(--surface-hover, #334155); + border-radius: 8px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s; + } + + .history-item:hover { + background: var(--border-light, #475569); + } + + .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, #10b981); + 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, #10b981); + } + + .sentiment-neutral { + background: var(--border-light, #475569); + color: var(--text-secondary, #94a3b8); + } + + .sentiment-negative { + background: rgba(239, 68, 68, 0.2); + color: var(--error, #ef4444); + } + + /* Tags */ + .tags-container { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .tag { + font-size: 11px; + padding: 4px 8px; + background: var(--border-light, #475569); + border-radius: 4px; + color: var(--text-secondary, #94a3b8); + } + + /* 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(--surface, #1e293b); + border: 1px solid var(--border, #334155); + border-radius: 16px; + width: 100%; + max-width: 400px; + max-height: 80vh; + overflow: hidden; + } + + .modal-header { + padding: 20px; + border-bottom: 1px solid var(--border, #334155); + 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: 6px; + background: var(--surface-hover, #334155); + color: var(--text-secondary, #94a3b8); + 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); + } + + .attendant-list { + max-height: 200px; + overflow-y: auto; + } + + .attendant-option { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s; + } + + .attendant-option:hover { + background: var(--surface-hover, #334155); + } + + .attendant-option.selected { + background: var(--primary, #3b82f6); + } + + .attendant-option .status-indicator { + width: 10px; + height: 10px; + } + + .modal-footer { + padding: 16px 20px; + border-top: 1px solid var(--border, #334155); + display: flex; + justify-content: flex-end; + gap: 8px; + } + + /* Toast Notifications */ + .toast-container { + position: fixed; + bottom: 24px; + right: 24px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 1001; + } + + .toast { + padding: 12px 20px; + background: var(--surface, #1e293b); + border: 1px solid var(--border, #334155); + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + animation: slideIn 0.3s ease; + } + + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + .toast.success { + border-left: 3px solid var(--success, #10b981); + } + .toast.error { + border-left: 3px solid var(--error, #ef4444); + } + .toast.warning { + border-left: 3px solid var(--warning, #f59e0b); + } + .toast.info { + border-left: 3px solid var(--primary, #3b82f6); + } + + /* Loading State */ + .loading-spinner { + width: 24px; + height: 24px; + border: 3px solid var(--surface-hover, #334155); + border-top-color: var(--primary, #3b82f6); + 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; + } + } + diff --git a/ui/suite/attendant/attendant.js b/ui/suite/attendant/attendant.js index 2daedca..bae3a54 100644 --- a/ui/suite/attendant/attendant.js +++ b/ui/suite/attendant/attendant.js @@ -423,3 +423,1140 @@ insertQuickReply }; })(); + + + // ===================================================================== + // 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() { + // CRM is now enabled by default + 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) { + // Set current attendant (first one for now, should come from auth) + currentAttendantId = attendants[0].attendant_id; + document.getElementById( + "attendantName", + ).textContent = attendants[0].attendant_name; + } else { + // No attendants configured, use default + document.getElementById( + "attendantName", + ).textContent = "Agent"; + } + } else { + // API error, use default + document.getElementById( + "attendantName", + ).textContent = "Agent"; + } + + // Always load queue and connect WebSocket - CRM enabled by default + await loadQueue(); + connectWebSocket(); + } catch (error) { + console.error("Failed to load attendants:", error); + // Still enable the console with default settings + document.getElementById("attendantName").textContent = "Agent"; + await loadQueue(); + connectWebSocket(); + } + } + + function showCRMDisabled() { + // Kept for backwards compatibility but no longer used by default + document.getElementById("crmDisabled").classList.add("active"); + document.getElementById("crmDisabled").style.display = "flex"; + 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"); + } + } + + function renderConversations() { + const list = document.getElementById("conversationList"); + const emptyState = document.getElementById("emptyQueue"); + + if (conversations.length === 0) { + emptyState.style.display = "flex"; + return; + } + + 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) => ` +
+
+ ${escapeHtml(conv.user_name || "Anonymous")} + ${formatTime(conv.last_message_time)} +
+
${escapeHtml(conv.last_message || "No messages")}
+
+ ${conv.channel} + ${conv.priority >= 2 ? `🔥 ${conv.priority >= 3 ? "Urgent" : "High"}` : ""} + ${formatWaitTime(conv.waiting_time_seconds)} +
+
+ `, + ) + .join("") + + ``; + } + + 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"; + }); + } + + // ===================================================================== + // 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 = '
'; + + 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 = + '

Failed to load messages

'; + } + } + + 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 = ` +
+
${avatarContent}
+
+
${escapeHtml(content)}
+
+ ${timeStr} + ${type === "bot" ? 'Bot' : ""} +
+
+
+ `; + + container.insertAdjacentHTML("beforeend", messageHtml); + container.scrollTop = container.scrollHeight; + } + + function addSystemMessage(content) { + const container = document.getElementById("chatMessages"); + const messageHtml = ` +
+
+
${escapeHtml(content)}
+
+
+ `; + container.insertAdjacentHTML("beforeend", messageHtml); + } + + 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", + ); + } + } + + function useQuickResponse(text) { + document.getElementById("chatInput").value = text; + document.getElementById("chatInput").focus(); + } + + 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); + } + } + + function showTransferModal() { + if (!currentSessionId) return; + + const list = document.getElementById("attendantList"); + list.innerHTML = attendants + .filter((a) => a.attendant_id !== currentAttendantId) + .map( + (a) => ` +
+
+
+
${escapeHtml(a.attendant_name)}
+
${a.preferences} • ${a.channel}
+
+
+ `, + ) + .join(""); + + document.getElementById("transferModal").classList.add("show"); + } + + function closeTransferModal() { + document + .getElementById("transferModal") + .classList.remove("show"); + document.getElementById("transferReason").value = ""; + } + + let selectedTransferTarget = null; + + function selectTransferTarget(element, attendantId) { + document + .querySelectorAll(".attendant-option") + .forEach((el) => el.classList.remove("selected")); + element.classList.add("selected"); + selectedTransferTarget = attendantId; + } + + 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 = + `${s.emoji} ${s.overall.charAt(0).toUpperCase() + s.overall.slice(1)}`; + + // Show warning for high escalation risk + if (s.escalation_risk === 'high') { + showToast("⚠️ High escalation risk detected", "warning"); + } + } + } + } else { + document.getElementById("sentimentValue").innerHTML = + `😐 Neutral`; + } + + // 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 = + `😐 Neutral`; + 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 => ` +
+
${escapeHtml(reply.text)}
+
+ ${Math.round(reply.confidence * 100)}% match + ${reply.tone} • AI +
+
+ `).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 = ` +
+
Hello! Thank you for reaching out. How can I assist you today?
+
+ Template + Quick Reply +
+
+
+
I'd be happy to help you with that. Let me look into it.
+
+ Template + Quick Reply +
+
+
+
Is there anything else I can help you with?
+
+ Template + Quick Reply +
+
+ `; + } + + // 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 = ` +
+ 💡 + Tips +
+
+ `; + 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 `
${emoji} ${escapeHtml(tip.content)}
`; + }).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(); + break; + 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(); + break; + case "attendant_response": + // Response from another attendant + if ( + data.session_id === currentSessionId && + data.assigned_to !== currentAttendantId + ) { + addMessage( + "attendant", + data.content, + data.timestamp, + ); + } + break; + 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, + ); + } + } + + 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 = ` + ${escapeHtml(message)} + `; + 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)); + } + } + + // 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, + }); + } diff --git a/ui/suite/attendant/index.html b/ui/suite/attendant/index.html index 0813b04..dfd0846 100644 --- a/ui/suite/attendant/index.html +++ b/ui/suite/attendant/index.html @@ -1,1183 +1,5 @@ - + @@ -1750,1142 +572,4 @@ -
- - + diff --git a/ui/suite/docs/docs.js b/ui/suite/docs/docs.js index c219568..ce6f626 100644 --- a/ui/suite/docs/docs.js +++ b/ui/suite/docs/docs.js @@ -33,6 +33,7 @@ slashPosition: null, isAIPanelOpen: false, focusMode: false, + driveSource: null, }; // ============================================================================= @@ -798,13 +799,24 @@ const urlParams = new URLSearchParams(window.location.search); const hash = window.location.hash; let docId = urlParams.get("id"); + let bucket = urlParams.get("bucket"); + let path = urlParams.get("path"); - if (!docId && hash) { - const hashParams = new URLSearchParams(hash.substring(1)); - docId = hashParams.get("id"); + if (hash) { + const hashQueryIndex = hash.indexOf("?"); + if (hashQueryIndex !== -1) { + const hashParams = new URLSearchParams( + hash.substring(hashQueryIndex + 1), + ); + docId = docId || hashParams.get("id"); + bucket = bucket || hashParams.get("bucket"); + path = path || hashParams.get("path"); + } } - if (docId) { + if (bucket && path) { + await loadFromDrive(bucket, path); + } else if (docId) { try { const response = await fetch(`/api/ui/docs/${docId}`); if (response.ok) { @@ -830,6 +842,80 @@ } } + async function loadFromDrive(bucket, path) { + const fileName = path.split("/").pop() || "document"; + const ext = fileName.split(".").pop().toLowerCase(); + + state.driveSource = { bucket, path }; + state.docTitle = fileName; + + if (elements.editorTitle) { + elements.editorTitle.textContent = fileName; + } + if (elements.docTitleInput) { + elements.docTitleInput.value = fileName; + } + + try { + const response = await fetch("/api/files/read", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bucket, path }), + }); + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.status}`); + } + + const data = await response.json(); + const content = data.content || ""; + + if (ext === "md" || ext === "markdown") { + if (elements.editorContent) { + elements.editorContent.innerHTML = markdownToHtml(content); + } + } else if (ext === "txt") { + if (elements.editorContent) { + elements.editorContent.innerHTML = `

${escapeHtml(content).replace(/\n/g, "

")}

`; + } + } else if (ext === "html" || ext === "htm") { + if (elements.editorContent) { + elements.editorContent.innerHTML = content; + } + } else { + if (elements.editorContent) { + elements.editorContent.innerHTML = `

${escapeHtml(content)}

`; + } + } + + updateWordCount(); + state.isDirty = false; + } catch (err) { + console.error("Failed to load file from drive:", err); + alert(`Failed to load file: ${err.message}`); + } + } + + function markdownToHtml(md) { + return md + .replace(/^### (.*$)/gim, "

$1

") + .replace(/^## (.*$)/gim, "

$1

") + .replace(/^# (.*$)/gim, "

$1

") + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1") + .replace(/`(.+?)`/g, "$1") + .replace(/\[(.+?)\]\((.+?)\)/g, '$1') + .replace(/\n\n/g, "

") + .replace(/\n/g, "
") + .replace(/^(.+)$/gm, "

$1

"); + } + + function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + // ============================================================================= // EXPORT // ============================================================================= diff --git a/ui/suite/drive/drive.js b/ui/suite/drive/drive.js index 9b2ef09..9413b1d 100644 --- a/ui/suite/drive/drive.js +++ b/ui/suite/drive/drive.js @@ -394,7 +394,7 @@ if (type === "folder") { loadFiles(path, currentBucket); } else { - openInlineEditor(path); + openFile(path); } }); }); @@ -691,7 +691,6 @@ const isFolder = type === "folder"; const ep = escapeJs(path); - const canEdit = !isFolder && isEditableFile(path); const icons = { open: ``, @@ -709,9 +708,9 @@ ${ isFolder ? `
${icons.open}Open
` - : `
${icons.download}Download
` + : `
${icons.open}Open
+
${icons.download}Download
` } - ${canEdit ? `
${icons.edit}Edit
` : ""}
${icons.copy}Copy
${icons.cut}Cut
@@ -891,110 +890,28 @@ } } - function isBasicFile(path) { - const ext = "." + (path.split(".").pop() || "").toLowerCase(); - return ext === ".bas"; - } - - function openInDesigner(path) { - const params = new URLSearchParams({ - bucket: currentBucket, - path: path, - }); - - if (window.htmx) { - htmx.ajax("GET", `/designer.html?${params.toString()}`, { - target: "#main-content", - swap: "innerHTML", - }); - window.history.pushState({}, "", `/#designer?${params.toString()}`); - } else { - window.location.href = `/designer.html?${params.toString()}`; - } - } - - function isEditableFile(path) { - const editableExtensions = [ - ".txt", - ".md", - ".json", - ".js", - ".ts", - ".css", - ".html", - ".htm", - ".xml", - ".yaml", - ".yml", - ".csv", - ".vbs", - ".sql", - ".sh", - ".bat", - ".ps1", - ".py", - ".rb", - ".php", - ".java", - ".c", - ".cpp", - ".h", - ".rs", - ".go", - ".swift", - ".kt", - ".scala", - ".r", - ".lua", - ".pl", - ".ini", - ".conf", - ".config", - ".env", - ".gitignore", - ".dockerfile", - ".toml", - ".lock", - ".log", - ".markdown", - ".rst", - ".tex", - ".csv", - ]; - const ext = "." + (path.split(".").pop() || "").toLowerCase(); - return editableExtensions.includes(ext); - } - - async function openInlineEditor(path) { - const fileName = path.split("/").pop() || "file"; - console.log("openInlineEditor called with path:", path); - - const isBas = isBasicFile(path); - console.log("isBasicFile check:", path, "->", isBas); - - if (isBas) { - console.log("Opening .bas file in designer:", path); - openInDesigner(path); - return; - } - - if (!isEditableFile(path)) { - console.log("File not editable, downloading instead"); - downloadFile(path); - return; - } - + async function openFile(path) { try { - console.log("Fetching file content for:", path); - const response = await apiRequest("/read", { + const response = await apiRequest("/open", { method: "POST", body: JSON.stringify({ bucket: currentBucket, path: path }), }); - console.log("API response:", response); - const content = response.content || ""; - console.log("Content length:", content.length); - showEditorModal(path, fileName, content); + const { app, url } = response; + + if (window.htmx) { + htmx.ajax("GET", url, { + target: "#main-content", + swap: "innerHTML", + }); + window.history.pushState( + {}, + "", + `/#${app}?bucket=${encodeURIComponent(currentBucket)}&path=${encodeURIComponent(path)}`, + ); + } else { + window.location.href = url; + } } catch (err) { console.error("Failed to open file:", err); showNotification(`Failed to open file: ${err.message}`, "error"); @@ -1307,9 +1224,7 @@ selectAll, clearSelection, downloadFile, - openInlineEditor, - saveEditorContent, - closeEditor, + openFile, deleteItem, deleteSelected, renameItem, diff --git a/ui/suite/home.html b/ui/suite/home.html index 442f075..9383d47 100644 --- a/ui/suite/home.html +++ b/ui/suite/home.html @@ -1091,58 +1091,4 @@ - + diff --git a/ui/suite/js/home.js b/ui/suite/js/home.js index 64c744e..61719af 100644 --- a/ui/suite/js/home.js +++ b/ui/suite/js/home.js @@ -1,20 +1,158 @@ -/* Home page JavaScript */ +(function () { + "use strict"; -// Keyboard shortcuts -document.addEventListener('keydown', (e) => { - if (e.altKey && !e.ctrlKey && !e.shiftKey) { - const shortcuts = { - '1': '#chat', - '2': '#drive', - '3': '#tasks', - '4': '#mail', - '5': '#calendar', - '6': '#meet' - }; - if (shortcuts[e.key]) { - e.preventDefault(); - const link = document.querySelector(`a[href="${shortcuts[e.key]}"]`); - if (link) link.click(); - } + var ICON_SVG = { + doc: '', + sheet: + '', + slides: + '', + paper: + '', + }; + + var KEYBOARD_SHORTCUTS = { + 1: "#chat", + 2: "#drive", + 3: "#tasks", + 4: "#mail", + 5: "#calendar", + 6: "#meet", + }; + + function getIconForType(type) { + return ICON_SVG[type] || ICON_SVG.doc; + } + + function escapeHtml(text) { + var div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + function renderRecentDocuments(items) { + if (!items || items.length === 0) { + return; } -}); + + var container = document.getElementById("recent-documents"); + if (!container) { + return; + } + + var html = items + .slice(0, 4) + .map(function (item) { + var safeUrl = escapeHtml(item.url || ""); + var safeType = escapeHtml(item.type || "doc"); + var safeName = escapeHtml(item.name || ""); + var safeMeta = escapeHtml(item.meta || ""); + + return ( + '
' + + '
' + + getIconForType(item.type) + + "
" + + '
' + + '' + + safeName + + "" + + '' + + safeMeta + + "" + + "
" + + "
" + ); + }) + .join(""); + + container.innerHTML = html; + + container.querySelectorAll(".recent-card").forEach(function (card) { + card.addEventListener("click", function () { + var url = this.getAttribute("data-url"); + if (url) { + window.location.href = url; + } + }); + }); + } + + function loadRecentDocuments() { + fetch("/api/activity/recent") + .then(function (response) { + if (!response.ok) { + throw new Error("Failed to fetch recent documents"); + } + return response.json(); + }) + .then(function (items) { + renderRecentDocuments(items); + }) + .catch(function () { + console.log("Using placeholder recent documents"); + }); + } + + function setupHomeSearch() { + var homeSearch = document.getElementById("home-search"); + if (homeSearch) { + homeSearch.addEventListener("focus", function () { + var omnibox = document.getElementById("omniboxInput"); + if (omnibox) { + omnibox.focus(); + } + }); + } + } + + function setupKeyboardShortcuts() { + document.addEventListener("keydown", function (e) { + if (e.altKey && !e.ctrlKey && !e.shiftKey) { + var target = KEYBOARD_SHORTCUTS[e.key]; + if (target) { + e.preventDefault(); + var link = document.querySelector('a[href="' + target + '"]'); + if (link) { + link.click(); + } + } + } + }); + } + + function initHome() { + loadRecentDocuments(); + setupHomeSearch(); + } + + function isHomeVisible() { + return document.querySelector(".home-container") !== null; + } + + setupKeyboardShortcuts(); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", function () { + if (isHomeVisible()) { + initHome(); + } + }); + } else { + if (isHomeVisible()) { + initHome(); + } + } + + document.body.addEventListener("htmx:afterSwap", function (evt) { + if (evt.detail.target && evt.detail.target.id === "main-content") { + if (isHomeVisible()) { + initHome(); + } + } + }); +})(); diff --git a/ui/suite/mail/mail.html b/ui/suite/mail/mail.html index c9055f8..c6057c5 100644 --- a/ui/suite/mail/mail.html +++ b/ui/suite/mail/mail.html @@ -1340,1128 +1340,5 @@ - - - + + diff --git a/ui/suite/mail/mail.js b/ui/suite/mail/mail.js index b554c8d..8b57955 100644 --- a/ui/suite/mail/mail.js +++ b/ui/suite/mail/mail.js @@ -1,156 +1,478 @@ -function openCompose(replyTo = null, forward = null) { - const modal = document.getElementById("composeModal"); - if (modal) { - modal.classList.remove("hidden"); - modal.classList.remove("minimized"); - if (replyTo) { - document.getElementById("composeTo").value = replyTo; +(function () { + "use strict"; + + var selectedEmails = new Set(); + var currentFolder = "inbox"; + + function openCompose() { + var modal = document.getElementById("compose-modal"); + if (modal && modal.showModal) { + modal.showModal(); } } -} -function closeCompose() { - const modal = document.getElementById("composeModal"); - if (modal) { - modal.classList.add("hidden"); - document.getElementById("composeTo").value = ""; - document.getElementById("composeCc").value = ""; - document.getElementById("composeBcc").value = ""; - document.getElementById("composeSubject").value = ""; - document.getElementById("composeBody").value = ""; - } -} - -function minimizeCompose() { - const modal = document.getElementById("composeModal"); - if (modal) { - modal.classList.toggle("minimized"); - } -} - -function toggleCcBcc() { - const ccBcc = document.getElementById("ccBccFields"); - if (ccBcc) { - ccBcc.classList.toggle("hidden"); - } -} - -function toggleScheduleMenu() { - const menu = document.getElementById("scheduleMenu"); - if (menu) { - menu.classList.toggle("hidden"); - } -} - -function scheduleSend(when) { - console.log("Scheduling send for:", when); - toggleScheduleMenu(); -} - -function toggleSelectAll() { - const selectAll = document.getElementById("selectAll"); - const checkboxes = document.querySelectorAll(".email-checkbox"); - checkboxes.forEach((cb) => (cb.checked = selectAll.checked)); - updateBulkActions(); -} - -function updateBulkActions() { - const checked = document.querySelectorAll(".email-checkbox:checked"); - const bulkActions = document.getElementById("bulkActions"); - if (bulkActions) { - bulkActions.style.display = checked.length > 0 ? "flex" : "none"; - } -} - -function openTemplatesModal() { - const modal = document.getElementById("templatesModal"); - if (modal) modal.classList.remove("hidden"); -} - -function closeTemplatesModal() { - const modal = document.getElementById("templatesModal"); - if (modal) modal.classList.add("hidden"); -} - -function openSignaturesModal() { - const modal = document.getElementById("signaturesModal"); - if (modal) modal.classList.remove("hidden"); -} - -function closeSignaturesModal() { - const modal = document.getElementById("signaturesModal"); - if (modal) modal.classList.add("hidden"); -} - -function openRulesModal() { - const modal = document.getElementById("rulesModal"); - if (modal) modal.classList.remove("hidden"); -} - -function closeRulesModal() { - const modal = document.getElementById("rulesModal"); - if (modal) modal.classList.add("hidden"); -} - -function useTemplate(name) { - console.log("Using template:", name); - closeTemplatesModal(); -} - -function useSignature(name) { - console.log("Using signature:", name); - closeSignaturesModal(); -} - -function archiveSelected() { - const checked = document.querySelectorAll(".email-checkbox:checked"); - console.log("Archiving", checked.length, "emails"); -} - -function deleteSelected() { - const checked = document.querySelectorAll(".email-checkbox:checked"); - if (confirm(`Delete ${checked.length} email(s)?`)) { - console.log("Deleting", checked.length, "emails"); - } -} - -function markSelectedRead() { - const checked = document.querySelectorAll(".email-checkbox:checked"); - console.log("Marking", checked.length, "emails as read"); -} - -function handleAttachment(input) { - const files = input.files; - const attachmentList = document.getElementById("attachmentList"); - if (attachmentList && files.length > 0) { - for (const file of files) { - const item = document.createElement("div"); - item.className = "attachment-item"; - item.innerHTML = ` - ${file.name} - - `; - attachmentList.appendChild(item); + function closeCompose() { + var modal = document.getElementById("compose-modal"); + if (modal && modal.close) { + modal.close(); } } -} -document.addEventListener("keydown", function (e) { - if (e.key === "Escape") { + function minimizeCompose() { closeCompose(); - closeTemplatesModal(); - closeSignaturesModal(); - closeRulesModal(); } - if (e.ctrlKey && e.key === "n") { - e.preventDefault(); - openCompose(); + function toggleCcBcc() { + document.querySelectorAll(".cc-bcc").forEach(function (el) { + el.style.display = el.style.display === "none" ? "flex" : "none"; + }); } -}); -document.addEventListener("DOMContentLoaded", function () { - document.querySelectorAll(".email-checkbox").forEach((cb) => { - cb.addEventListener("change", updateBulkActions); + function toggleScheduleMenu() { + var menu = document.getElementById("schedule-menu"); + if (menu) { + menu.classList.toggle("show"); + } + } + + function scheduleSend(option) { + var date = new Date(); + switch (option) { + case "tomorrow-morning": + date.setDate(date.getDate() + 1); + date.setHours(8, 0, 0, 0); + break; + case "tomorrow-afternoon": + date.setDate(date.getDate() + 1); + date.setHours(13, 0, 0, 0); + break; + case "monday": + var daysUntilMonday = (8 - date.getDay()) % 7 || 7; + date.setDate(date.getDate() + daysUntilMonday); + date.setHours(8, 0, 0, 0); + break; + } + confirmScheduleSend(date); + toggleScheduleMenu(); + } + + function openCustomSchedule() { + toggleScheduleMenu(); + var today = new Date().toISOString().split("T")[0]; + var dateInput = document.getElementById("schedule-date"); + if (dateInput) { + dateInput.min = today; + dateInput.value = today; + } + var modal = document.getElementById("schedule-modal"); + if (modal && modal.showModal) { + modal.showModal(); + } + } + + function closeScheduleModal() { + var modal = document.getElementById("schedule-modal"); + if (modal && modal.close) { + modal.close(); + } + } + + function confirmSchedule() { + var dateInput = document.getElementById("schedule-date"); + var timeInput = document.getElementById("schedule-time"); + if (dateInput && timeInput) { + var scheduledDate = new Date(dateInput.value + "T" + timeInput.value); + confirmScheduleSend(scheduledDate); + } + closeScheduleModal(); + } + + function confirmScheduleSend(date) { + var form = document.getElementById("compose-form"); + if (form) { + var input = document.createElement("input"); + input.type = "hidden"; + input.name = "scheduled_at"; + input.value = date.toISOString(); + form.appendChild(input); + prepareSubmit(); + form.requestSubmit(); + } + } + + function prepareSubmit() { + var body = document.getElementById("compose-body"); + var hidden = document.getElementById("compose-body-hidden"); + if (body && hidden) { + hidden.value = body.innerHTML; + } + } + + function formatText(command) { + document.execCommand(command, false, null); + var body = document.getElementById("compose-body"); + if (body) { + body.focus(); + } + } + + function openTemplates() { + var modal = document.getElementById("templates-modal"); + if (modal && modal.showModal) { + modal.showModal(); + } + } + + function closeTemplates() { + var modal = document.getElementById("templates-modal"); + if (modal && modal.close) { + modal.close(); + } + } + + function openSignatures() { + var modal = document.getElementById("signatures-modal"); + if (modal && modal.showModal) { + modal.showModal(); + } + } + + function closeSignatures() { + var modal = document.getElementById("signatures-modal"); + if (modal && modal.close) { + modal.close(); + } + } + + function openRules() { + var modal = document.getElementById("rules-modal"); + if (modal && modal.showModal) { + modal.showModal(); + } + } + + function closeRules() { + var modal = document.getElementById("rules-modal"); + if (modal && modal.close) { + modal.close(); + } + } + + function openAutoResponder() { + var modal = document.getElementById("autoresponder-modal"); + if (modal && modal.showModal) { + modal.showModal(); + } + } + + function closeAutoResponder() { + var modal = document.getElementById("autoresponder-modal"); + if (modal && modal.close) { + modal.close(); + } + } + + function saveAutoResponder() { + var form = document.getElementById("autoresponder-form"); + if (form && typeof htmx !== "undefined") { + htmx.trigger(form, "submit"); + } + closeAutoResponder(); + if (typeof window.showNotification === "function") { + window.showNotification("Auto-reply settings saved", "success"); + } + } + + function openLabelManager() { + if (typeof window.showNotification === "function") { + window.showNotification("Label manager coming soon", "info"); + } + } + + function toggleSelectAll(checkbox) { + var items = document.querySelectorAll('.mail-item input[type="checkbox"]'); + items.forEach(function (item) { + item.checked = checkbox.checked; + if (checkbox.checked) { + selectedEmails.add(item.dataset.id); + } else { + selectedEmails.delete(item.dataset.id); + } + }); + updateBulkActions(); + } + + function updateBulkActions() { + var bulkBar = document.getElementById("bulk-actions"); + if (bulkBar) { + if (selectedEmails.size > 0) { + bulkBar.style.display = "flex"; + var countEl = bulkBar.querySelector(".selected-count"); + if (countEl) { + countEl.textContent = selectedEmails.size + " selected"; + } + } else { + bulkBar.style.display = "none"; + } + } + } + + function refreshMailList() { + var folderEl = document.querySelector( + '[data-folder="' + currentFolder + '"]', + ); + if (folderEl && typeof htmx !== "undefined") { + htmx.trigger(folderEl, "click"); + } + } + + function insertSignature() { + fetch("/api/email/signatures/default") + .then(function (r) { + return r.json(); + }) + .then(function (sig) { + if (sig.content_html) { + var body = document.getElementById("compose-body"); + if (body) { + body.innerHTML += "

" + sig.content_html; + } + } + }) + .catch(function (e) { + console.warn("Failed to load signature:", e); + }); + } + + function showTemplateSelector() { + openTemplates(); + } + + function attachFile() { + var input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.onchange = function (e) { + var files = e.target.files; + var container = document.getElementById("compose-attachments"); + if (container) { + Array.from(files).forEach(function (file) { + var chip = document.createElement("div"); + chip.className = "attachment-chip"; + chip.innerHTML = + "" + + escapeHtml(file.name) + + "" + + ''; + container.appendChild(chip); + }); + } + }; + input.click(); + } + + function insertLink() { + var url = prompt("Enter URL:"); + if (url) { + document.execCommand("createLink", false, url); + } + } + + function insertImage() { + var url = prompt("Enter image URL:"); + if (url) { + document.execCommand("insertImage", false, url); + } + } + + function saveDraft() { + prepareSubmit(); + var form = document.getElementById("compose-form"); + if (form) { + var formData = new FormData(form); + fetch("/api/email/draft", { + method: "POST", + body: formData, + }) + .then(function () { + if (typeof window.showNotification === "function") { + window.showNotification("Draft saved", "success"); + } + }) + .catch(function (e) { + console.warn("Failed to save draft:", e); + }); + } + } + + function createNewTemplate() { + if (typeof window.showNotification === "function") { + window.showNotification("Template editor coming soon", "info"); + } + } + + function createNewSignature() { + if (typeof window.showNotification === "function") { + window.showNotification("Signature editor coming soon", "info"); + } + } + + function createNewRule() { + if (typeof window.showNotification === "function") { + window.showNotification("Rule editor coming soon", "info"); + } + } + + function archiveSelected() { + if (typeof window.showNotification === "function") { + window.showNotification( + selectedEmails.size + " emails archived", + "success", + ); + } + selectedEmails.clear(); + updateBulkActions(); + refreshMailList(); + } + + function markAsRead() { + if (typeof window.showNotification === "function") { + window.showNotification( + selectedEmails.size + " emails marked as read", + "success", + ); + } + selectedEmails.clear(); + updateBulkActions(); + refreshMailList(); + } + + function addLabelToSelected() { + if (typeof window.showNotification === "function") { + window.showNotification("Label picker coming soon", "info"); + } + } + + function deleteSelected() { + if (confirm("Delete " + selectedEmails.size + " emails?")) { + if (typeof window.showNotification === "function") { + window.showNotification( + selectedEmails.size + " emails deleted", + "success", + ); + } + selectedEmails.clear(); + updateBulkActions(); + refreshMailList(); + } + } + + function openAddAccount() { + var modal = document.getElementById("account-modal"); + if (modal && modal.showModal) { + modal.showModal(); + } + } + + function closeAddAccount() { + var modal = document.getElementById("account-modal"); + if (modal && modal.close) { + modal.close(); + } + } + + function saveAccount() { + var form = document.getElementById("account-form"); + if (form && typeof htmx !== "undefined") { + htmx.trigger(form, "submit"); + } + closeAddAccount(); + if (typeof window.showNotification === "function") { + window.showNotification("Email account added", "success"); + } + } + + function escapeHtml(text) { + var div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + function initFolderHandlers() { + document + .querySelectorAll(".nav-item[data-folder]") + .forEach(function (item) { + item.addEventListener("click", function () { + document.querySelectorAll(".nav-item").forEach(function (i) { + i.classList.remove("active"); + }); + this.classList.add("active"); + currentFolder = this.dataset.folder; + }); + }); + } + + function initMail() { + initFolderHandlers(); + + var inboxItem = document.querySelector('.nav-item[data-folder="inbox"]'); + if (inboxItem && typeof htmx !== "undefined") { + htmx.trigger(inboxItem, "click"); + } + } + + window.openCompose = openCompose; + window.closeCompose = closeCompose; + window.minimizeCompose = minimizeCompose; + window.toggleCcBcc = toggleCcBcc; + window.toggleScheduleMenu = toggleScheduleMenu; + window.scheduleSend = scheduleSend; + window.openCustomSchedule = openCustomSchedule; + window.closeScheduleModal = closeScheduleModal; + window.confirmSchedule = confirmSchedule; + window.prepareSubmit = prepareSubmit; + window.formatText = formatText; + window.openTemplates = openTemplates; + window.closeTemplates = closeTemplates; + window.openSignatures = openSignatures; + window.closeSignatures = closeSignatures; + window.openRules = openRules; + window.closeRules = closeRules; + window.openAutoResponder = openAutoResponder; + window.closeAutoResponder = closeAutoResponder; + window.saveAutoResponder = saveAutoResponder; + window.openLabelManager = openLabelManager; + window.toggleSelectAll = toggleSelectAll; + window.updateBulkActions = updateBulkActions; + window.refreshMailList = refreshMailList; + window.insertSignature = insertSignature; + window.showTemplateSelector = showTemplateSelector; + window.attachFile = attachFile; + window.insertLink = insertLink; + window.insertImage = insertImage; + window.saveDraft = saveDraft; + window.createNewTemplate = createNewTemplate; + window.createNewSignature = createNewSignature; + window.createNewRule = createNewRule; + window.archiveSelected = archiveSelected; + window.markAsRead = markAsRead; + window.addLabelToSelected = addLabelToSelected; + window.deleteSelected = deleteSelected; + window.openAddAccount = openAddAccount; + window.closeAddAccount = closeAddAccount; + window.saveAccount = saveAccount; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initMail); + } else { + initMail(); + } + + document.body.addEventListener("htmx:afterSwap", function (evt) { + if (evt.detail.target && evt.detail.target.id === "main-content") { + if (document.querySelector(".mail-layout")) { + initMail(); + } + } }); -}); +})(); diff --git a/ui/suite/monitoring/monitoring.html b/ui/suite/monitoring/monitoring.html index 2078732..cb139d8 100644 --- a/ui/suite/monitoring/monitoring.html +++ b/ui/suite/monitoring/monitoring.html @@ -1374,349 +1374,7 @@ hx-swap="innerHTML" > - - - + + diff --git a/ui/suite/monitoring/monitoring.js b/ui/suite/monitoring/monitoring.js index c55aa06..e7ce376 100644 --- a/ui/suite/monitoring/monitoring.js +++ b/ui/suite/monitoring/monitoring.js @@ -1,75 +1,162 @@ /* Monitoring module - shared/base JavaScript */ -function setActiveNav(element) { - document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => { - item.classList.remove("active"); - }); - element.classList.add("active"); +(function () { + "use strict"; - // Update page title - const title = element.querySelector( - "span:not(.alert-badge):not(.health-indicator)", - ).textContent; - document.getElementById("page-title").textContent = title; -} + function setActiveNav(element) { + document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => { + item.classList.remove("active"); + }); + element.classList.add("active"); -function updateTimeRange(range) { - // Store selected time range - localStorage.setItem("monitoring-time-range", range); + // Update page title + const title = element.querySelector( + "span:not(.alert-badge):not(.health-indicator)", + ).textContent; + document.getElementById("page-title").textContent = title; + } - // Trigger refresh of current view - htmx.trigger("#monitoring-content", "refresh"); -} + function updateTimeRange(range) { + // Store selected time range + localStorage.setItem("monitoring-time-range", range); -function refreshMonitoring() { - htmx.trigger("#monitoring-content", "refresh"); - - // Visual feedback - const btn = event.currentTarget; - btn.classList.add("active"); - setTimeout(() => btn.classList.remove("active"), 500); -} - -// Guard against duplicate declarations on HTMX reload -if (typeof window.monitoringModuleInitialized === "undefined") { - window.monitoringModuleInitialized = true; - var autoRefresh = true; -} - -function toggleAutoRefresh() { - autoRefresh = !autoRefresh; - const btn = document.getElementById("auto-refresh-btn"); - btn.classList.toggle("active", autoRefresh); - - if (autoRefresh) { - // Re-enable polling by refreshing the page content + // Trigger refresh of current view htmx.trigger("#monitoring-content", "refresh"); } -} -function exportData() { - const timeRange = document.getElementById("time-range").value; - window.open(`/api/monitoring/export?range=${timeRange}`, "_blank"); -} + function refreshMonitoring() { + htmx.trigger("#monitoring-content", "refresh"); -// Initialize -document.addEventListener("DOMContentLoaded", function () { - // Restore time range preference - const savedRange = localStorage.getItem("monitoring-time-range"); - if (savedRange) { - const timeRangeEl = document.getElementById("time-range"); - if (timeRangeEl) timeRangeEl.value = savedRange; + // Visual feedback + const btn = event.currentTarget; + btn.classList.add("active"); + setTimeout(() => btn.classList.remove("active"), 500); } - // Set auto-refresh button state - const autoRefreshBtn = document.getElementById("auto-refresh-btn"); - if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh); -}); - -// Handle HTMX events for loading states -document.body.addEventListener("htmx:beforeRequest", function (evt) { - if (evt.target.id === "monitoring-content") { - evt.target.innerHTML = - '

Loading...

'; + // Guard against duplicate declarations on HTMX reload + if (typeof window.monitoringModuleInitialized === "undefined") { + window.monitoringModuleInitialized = true; + var autoRefresh = true; } -}); + + function toggleAutoRefresh() { + autoRefresh = !autoRefresh; + const btn = document.getElementById("auto-refresh-btn"); + btn.classList.toggle("active", autoRefresh); + + if (autoRefresh) { + // Re-enable polling by refreshing the page content + htmx.trigger("#monitoring-content", "refresh"); + } + } + + function exportData() { + const timeRange = document.getElementById("time-range").value; + window.open(`/api/monitoring/export?range=${timeRange}`, "_blank"); + } + + // Initialize + document.addEventListener("DOMContentLoaded", function () { + // Restore time range preference + const savedRange = localStorage.getItem("monitoring-time-range"); + if (savedRange) { + const timeRangeEl = document.getElementById("time-range"); + if (timeRangeEl) timeRangeEl.value = savedRange; + } + + // Set auto-refresh button state + const autoRefreshBtn = document.getElementById("auto-refresh-btn"); + if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh); + }); + + // Handle HTMX events for loading states + document.body.addEventListener("htmx:beforeRequest", function (evt) { + if (evt.target.id === "monitoring-content") { + evt.target.innerHTML = + '

Loading...

'; + } + }); + + function initViewToggle() { + var toggleBtn = document.getElementById("view-toggle"); + if (toggleBtn) { + toggleBtn.addEventListener("click", function () { + var liveView = document.getElementById("live-view"); + var gridView = document.getElementById("grid-view"); + + if (liveView && gridView) { + if (liveView.style.display === "none") { + liveView.style.display = "flex"; + gridView.style.display = "none"; + } else { + liveView.style.display = "none"; + gridView.style.display = "grid"; + } + } + }); + } + } + + function updateServiceStatusDots(event) { + if (event.detail.target.id === "service-status-container") { + try { + var data = JSON.parse(event.detail.target.textContent); + Object.entries(data).forEach(function (entry) { + var service = entry[0]; + var status = entry[1]; + var dot = document.querySelector('[data-status="' + service + '"]'); + if (dot) { + dot.classList.remove("running", "warning", "stopped"); + dot.classList.add(status); + } + }); + } catch (e) { + // Silent fail for non-JSON responses + } + } + } + + function handleKeyboardShortcuts(e) { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { + return; + } + + if (e.key === "r" && !e.ctrlKey && !e.metaKey) { + if (typeof htmx !== "undefined") { + htmx.trigger(document.body, "htmx:load"); + } + } + if (e.key === "v" && !e.ctrlKey && !e.metaKey) { + var toggleBtn = document.getElementById("view-toggle"); + if (toggleBtn) { + toggleBtn.click(); + } + } + } + + function initMonitoring() { + initViewToggle(); + document.body.addEventListener("htmx:afterSwap", updateServiceStatusDots); + document.addEventListener("keydown", handleKeyboardShortcuts); + } + + window.setActiveNav = setActiveNav; + window.updateTimeRange = updateTimeRange; + window.refreshMonitoring = refreshMonitoring; + window.toggleAutoRefresh = toggleAutoRefresh; + window.exportData = exportData; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initMonitoring); + } else { + initMonitoring(); + } + + document.body.addEventListener("htmx:afterSwap", function (evt) { + if (evt.detail.target && evt.detail.target.id === "main-content") { + if (document.querySelector(".monitoring-container")) { + initMonitoring(); + } + } + }); +})(); diff --git a/ui/suite/player/player.html b/ui/suite/player/player.html index 7aec989..7664857 100644 --- a/ui/suite/player/player.html +++ b/ui/suite/player/player.html @@ -38,11 +38,41 @@ @@ -109,7 +157,7 @@ @@ -166,23 +214,50 @@
- - -
- - +
- @@ -213,791 +288,885 @@
diff --git a/ui/suite/slides/slides.js b/ui/suite/slides/slides.js index 66a3da6..09220d6 100644 --- a/ui/suite/slides/slides.js +++ b/ui/suite/slides/slides.js @@ -36,6 +36,7 @@ autoSaveTimer: null, isPresenting: false, theme: null, + driveSource: null, }; const elements = { @@ -58,7 +59,97 @@ bindEvents(); loadFromUrlParams(); connectWebSocket(); - createNewPresentation(); + } + + async function loadFromUrlParams() { + const urlParams = new URLSearchParams(window.location.search); + const hash = window.location.hash; + let presentationId = urlParams.get("id"); + let bucket = urlParams.get("bucket"); + let path = urlParams.get("path"); + + if (hash) { + const hashQueryIndex = hash.indexOf("?"); + if (hashQueryIndex !== -1) { + const hashParams = new URLSearchParams( + hash.substring(hashQueryIndex + 1), + ); + presentationId = presentationId || hashParams.get("id"); + bucket = bucket || hashParams.get("bucket"); + path = path || hashParams.get("path"); + } + } + + if (bucket && path) { + await loadFromDrive(bucket, path); + } else if (presentationId) { + try { + const response = await fetch(`/api/slides/${presentationId}`); + if (response.ok) { + const data = await response.json(); + state.presentationId = presentationId; + state.presentationName = data.name || "Untitled Presentation"; + state.slides = data.slides || []; + + if (elements.presentationName) { + elements.presentationName.value = state.presentationName; + } + + renderThumbnails(); + renderCurrentSlide(); + updateSlideCounter(); + } + } catch (e) { + console.error("Load failed:", e); + createNewPresentation(); + } + } else { + createNewPresentation(); + } + } + + async function loadFromDrive(bucket, path) { + const fileName = path.split("/").pop() || "presentation"; + + state.driveSource = { bucket, path }; + state.presentationName = fileName; + + if (elements.presentationName) { + elements.presentationName.value = fileName; + } + + try { + const response = await fetch("/api/files/read", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bucket, path }), + }); + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.status}`); + } + + const data = await response.json(); + const content = data.content || ""; + + createNewPresentation(); + if (state.slides.length > 0 && state.slides[0].elements) { + const titleElement = state.slides[0].elements.find( + (el) => el.element_type === "text" && el.style?.fontSize >= 32, + ); + if (titleElement) { + titleElement.content = fileName.replace(/\.[^/.]+$/, ""); + } + } + + renderThumbnails(); + renderCurrentSlide(); + updateSlideCounter(); + state.isDirty = false; + } catch (err) { + console.error("Failed to load file from drive:", err); + createNewPresentation(); + } } function cacheElements() { diff --git a/ui/suite/tasks/tasks.html b/ui/suite/tasks/tasks.html index ea1a520..f0b5e99 100644 --- a/ui/suite/tasks/tasks.html +++ b/ui/suite/tasks/tasks.html @@ -310,317 +310,5 @@
- + + diff --git a/ui/suite/tasks/tasks.js b/ui/suite/tasks/tasks.js index 5ae9078..86c6773 100644 --- a/ui/suite/tasks/tasks.js +++ b/ui/suite/tasks/tasks.js @@ -3051,3 +3051,240 @@ function updateFilterCounts() { // Call updateFilterCounts on load document.addEventListener("DOMContentLoaded", updateFilterCounts); document.body.addEventListener("taskCreated", updateFilterCounts); + +// ============================================================================= +// MODAL FUNCTIONS +// ============================================================================= + +function showNewIntentModal() { + var modal = document.getElementById("new-intent-modal"); + if (modal) { + modal.style.display = "flex"; + } +} + +function closeNewIntentModal() { + var modal = document.getElementById("new-intent-modal"); + if (modal) { + modal.style.display = "none"; + } +} + +function showDecisionModal(decision) { + var questionEl = document.getElementById("decision-question"); + if (decision && questionEl) { + var title = decision.title || "Decision Required"; + var description = decision.description || ""; + questionEl.innerHTML = + "

" + + escapeHtml(title) + + "

" + + "

" + + escapeHtml(description) + + "

"; + } + var modal = document.getElementById("decision-modal"); + if (modal) { + modal.style.display = "flex"; + } +} + +function closeDecisionModal() { + var modal = document.getElementById("decision-modal"); + if (modal) { + modal.style.display = "none"; + } +} + +function submitNewIntent() { + var form = document.getElementById("new-intent-form"); + if (!form) return; + + var intentInput = form.querySelector('[name="intent"]'); + if (!intentInput) return; + + var intent = intentInput.value; + if (intent && intent.trim()) { + var quickInput = document.getElementById("quick-intent-input"); + if (quickInput) { + quickInput.value = intent; + } + var quickBtn = document.getElementById("quick-intent-btn"); + if (quickBtn && typeof htmx !== "undefined") { + htmx.trigger(quickBtn, "click"); + } + closeNewIntentModal(); + } +} + +function skipDecision() { + closeDecisionModal(); +} + +// ============================================================================= +// TASK STATS LOADING +// ============================================================================= + +function loadTaskStats() { + fetch("/api/tasks/stats/json") + .then(function (response) { + if (!response.ok) { + throw new Error("Failed to fetch stats"); + } + return response.json(); + }) + .then(function (stats) { + var mappings = [ + { key: "complete", id: "count-complete" }, + { key: "completed", id: "count-complete" }, + { key: "active", id: "count-active" }, + { key: "awaiting", id: "count-awaiting" }, + { key: "paused", id: "count-paused" }, + { key: "blocked", id: "count-blocked" }, + { key: "time_saved", id: "time-saved-value" }, + { key: "total", id: "count-all" }, + ]; + + mappings.forEach(function (mapping) { + if (stats[mapping.key] !== undefined) { + var el = document.getElementById(mapping.id); + if (el) { + el.textContent = stats[mapping.key]; + } + } + }); + }) + .catch(function (e) { + console.warn("Failed to load stats:", e); + }); +} + +// ============================================================================= +// SPLITTER DRAG FUNCTIONALITY +// ============================================================================= + +(function initSplitter() { + var splitter = document.getElementById("tasks-splitter"); + var main = document.querySelector(".tasks-main"); + var leftPanel = document.querySelector(".tasks-list-panel"); + + if (!splitter || !main || !leftPanel) return; + + var isDragging = false; + var startX = 0; + var startWidth = 0; + + splitter.addEventListener("mousedown", function (e) { + isDragging = true; + startX = e.clientX; + startWidth = leftPanel.offsetWidth; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + e.preventDefault(); + }); + + document.addEventListener("mousemove", function (e) { + if (!isDragging) return; + + var diff = e.clientX - startX; + var newWidth = Math.max(200, Math.min(600, startWidth + diff)); + leftPanel.style.flex = "0 0 " + newWidth + "px"; + leftPanel.style.width = newWidth + "px"; + }); + + document.addEventListener("mouseup", function () { + if (isDragging) { + isDragging = false; + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + }); +})(); + +// ============================================================================= +// HTMX TASK CREATION HANDLER +// ============================================================================= + +document.body.addEventListener("htmx:afterRequest", function (evt) { + if (!evt.detail.pathInfo) return; + if (evt.detail.pathInfo.requestPath !== "/api/autotask/create") return; + + var xhr = evt.detail.xhr; + var intentResult = document.getElementById("intent-result"); + if (!intentResult) return; + + if (xhr && xhr.status === 202) { + try { + var response = JSON.parse(xhr.responseText); + if (response.success && response.task_id) { + console.log("[TASK] Created task:", response.task_id); + + intentResult.innerHTML = + '✓ Task created - running...'; + intentResult.style.display = "block"; + + var quickInput = document.getElementById("quick-intent-input"); + if (quickInput) { + quickInput.value = ""; + } + + selectTask(response.task_id); + + setTimeout(function () { + intentResult.style.display = "none"; + }, 2000); + } else { + var msg = response.message || "Failed to create task"; + intentResult.innerHTML = + '✗ ' + escapeHtml(msg) + ""; + intentResult.style.display = "block"; + } + } catch (e) { + console.warn("Failed to parse create response:", e); + intentResult.innerHTML = + '✗ Failed to parse response'; + intentResult.style.display = "block"; + } + } else if (xhr && xhr.status >= 400) { + try { + var errorResponse = JSON.parse(xhr.responseText); + var errorMsg = + errorResponse.error || errorResponse.message || "Error creating task"; + intentResult.innerHTML = + '✗ ' + escapeHtml(errorMsg) + ""; + } catch (e) { + intentResult.innerHTML = + '✗ Error: ' + xhr.status + ""; + } + intentResult.style.display = "block"; + } +}); + +// ============================================================================= +// FILTER PILL CLICK HANDLER +// ============================================================================= + +document.querySelectorAll(".filter-pill").forEach(function (pill) { + pill.addEventListener("click", function () { + document.querySelectorAll(".filter-pill").forEach(function (p) { + p.classList.remove("active"); + }); + this.classList.add("active"); + }); +}); + +// ============================================================================= +// HTMX TASK LIST REFRESH HANDLER +// ============================================================================= + +document.body.addEventListener("htmx:afterSwap", function (e) { + if (e.detail.target && e.detail.target.id === "task-list") { + loadTaskStats(); + var taskList = document.getElementById("task-list"); + var emptyState = document.getElementById("empty-state"); + if (taskList && emptyState) { + var hasTasks = taskList.querySelector(".task-card"); + emptyState.style.display = hasTasks ? "none" : "flex"; + } + } +}); diff --git a/ui/suite/video/video.js b/ui/suite/video/video.js index 25d5e21..975f54a 100644 --- a/ui/suite/video/video.js +++ b/ui/suite/video/video.js @@ -13,6 +13,7 @@ class VideoEditor { this.pixelsPerMs = 0.1; this.undoStack = []; this.redoStack = []; + this.driveSource = null; this.init(); } @@ -20,9 +21,96 @@ class VideoEditor { async init() { this.bindEvents(); this.updateTimeRuler(); + await this.loadFromUrlParams(); await this.loadProjects(); } + async loadFromUrlParams() { + const urlParams = new URLSearchParams(window.location.search); + const hash = window.location.hash; + let bucket = urlParams.get("bucket"); + let path = urlParams.get("path"); + + if (hash) { + const hashQueryIndex = hash.indexOf("?"); + if (hashQueryIndex !== -1) { + const hashParams = new URLSearchParams( + hash.substring(hashQueryIndex + 1), + ); + bucket = bucket || hashParams.get("bucket"); + path = path || hashParams.get("path"); + } + } + + if (bucket && path) { + await this.loadFromDrive(bucket, path); + } + } + + async loadFromDrive(bucket, path) { + const fileName = path.split("/").pop() || "media"; + const ext = fileName.split(".").pop().toLowerCase(); + + this.driveSource = { bucket, path }; + + try { + const response = await fetch("/api/files/download", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bucket, path }), + }); + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.status}`); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + + const isImage = [ + "png", + "jpg", + "jpeg", + "gif", + "webp", + "svg", + "bmp", + "ico", + "tiff", + "tif", + "heic", + "heif", + ].includes(ext); + const isVideo = [ + "mp4", + "webm", + "mov", + "avi", + "mkv", + "wmv", + "flv", + "m4v", + ].includes(ext); + + const previewEl = document.getElementById("preview-video"); + if (previewEl) { + if (isImage) { + previewEl.outerHTML = `${fileName}`; + } else if (isVideo) { + previewEl.src = url; + previewEl.load(); + } + } + + const projectName = document.getElementById("current-project-name"); + if (projectName) { + projectName.textContent = fileName; + } + } catch (err) { + console.error("Failed to load file from drive:", err); + } + } + bindEvents() { document .getElementById("new-project-btn")