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.last_message || "No messages")}
+
+ ${conv.channel}
+ ${conv.priority >= 2 ? `🔥 ${conv.priority >= 3 ? "Urgent" : "High"}` : ""}
+ ${formatWaitTime(conv.waiting_time_seconds)}
+
+
+ `,
+ )
+ .join("") +
+ `
+
+
No conversations in queue
+
New conversations will appear here
+
`;
+ }
+
+ 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 = `
+
+
+ `;
+ 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
? ``
- : ``
+ : `
+ `
}
- ${canEdit ? `` : ""}
@@ -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 =
- '';
+ // 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 =
+ '';
+ }
+ });
+
+ 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 @@
Files
-
-
-
-
-
+
+
+
+
+
-
@@ -86,7 +134,7 @@
@@ -97,7 +145,7 @@
🎵
@@ -109,7 +157,7 @@
@@ -166,23 +214,50 @@
-
+
+
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 = `
`;
+ } 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")