// Connection status UI component import { ConnectionManager, ConnectionState } from './connectionManager'; import { RequestQueueManager } from './requestQueue'; export class ConnectionUI { private static instance: ConnectionUI; private connectionManager: ConnectionManager; private queueManager: RequestQueueManager; private statusElement: HTMLElement | null = null; private retryButton: HTMLButtonElement | null = null; private offlineMessage: HTMLElement | null = null; private constructor() { this.connectionManager = ConnectionManager.getInstance(); this.queueManager = RequestQueueManager.getInstance(); this.createUI(); this.subscribeToConnectionChanges(); } static getInstance(): ConnectionUI { if (!ConnectionUI.instance) { ConnectionUI.instance = new ConnectionUI(); } return ConnectionUI.instance; } private createUI(): void { // Create connection status indicator this.statusElement = document.createElement('div'); this.statusElement.id = 'connectionStatus'; this.statusElement.className = 'connection-status'; this.statusElement.innerHTML = ` Checking connection... `; // Create offline message banner this.offlineMessage = document.createElement('div'); this.offlineMessage.id = 'offlineMessage'; this.offlineMessage.className = 'offline-message'; this.offlineMessage.innerHTML = `
You're offline. Some features may be limited.
`; this.offlineMessage.style.display = 'none'; // Add to page document.body.appendChild(this.statusElement); document.body.appendChild(this.offlineMessage); // Get retry button reference this.retryButton = this.offlineMessage.querySelector('.retry-connection') as HTMLButtonElement; this.retryButton?.addEventListener('click', () => this.handleRetry()); // Add CSS if not already present if (!document.getElementById('connection-ui-styles')) { const style = document.createElement('style'); style.id = 'connection-ui-styles'; style.textContent = ` .connection-status { position: fixed; bottom: 20px; right: 20px; background: rgba(0, 0, 0, 0.8); color: white; padding: 8px 16px; border-radius: 20px; display: flex; align-items: center; gap: 8px; font-size: 14px; z-index: 1000; transition: all 0.3s ease; opacity: 0; transform: translateY(10px); } .connection-status.visible { opacity: 1; transform: translateY(0); } .connection-status.online { background: rgba(40, 167, 69, 0.9); } .connection-status.offline { background: rgba(220, 53, 69, 0.9); } .connection-status.connecting { background: rgba(255, 193, 7, 0.9); } .connection-icon::before { content: '●'; display: inline-block; animation: pulse 2s infinite; } .connection-status.connecting .connection-icon::before { animation: spin 1s linear infinite; content: '↻'; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .offline-message { position: fixed; top: 0; left: 0; right: 0; background: #dc3545; color: white; padding: 12px; text-align: center; z-index: 1001; transform: translateY(-100%); transition: transform 0.3s ease; } .offline-message.show { transform: translateY(0); } .offline-content { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; } .offline-content i { font-size: 20px; } .retry-connection { border-color: white; color: white; } .retry-connection:hover { background: white; color: #dc3545; } .queued-info { margin-left: 12px; } .queued-count { opacity: 0.9; } @media (max-width: 768px) { .connection-status { bottom: 10px; right: 10px; font-size: 12px; padding: 6px 12px; } .offline-content { font-size: 14px; } } `; document.head.appendChild(style); } } private subscribeToConnectionChanges(): void { this.connectionManager.subscribe('connection-ui', (state: ConnectionState) => { this.updateUI(state); }); } private updateUI(state: ConnectionState): void { if (!this.statusElement || !this.offlineMessage) return; const statusText = this.statusElement.querySelector('.connection-text') as HTMLElement; // Update status element this.statusElement.className = `connection-status visible ${state.status}`; switch (state.status) { case 'online': statusText.textContent = 'Connected'; this.hideOfflineMessage(); // Hide status after 3 seconds when online setTimeout(() => { if (this.connectionManager.getState().status === 'online') { this.statusElement?.classList.remove('visible'); } }, 3000); break; case 'offline': statusText.textContent = 'Offline'; this.showOfflineMessage(); this.updateQueuedInfo(); break; case 'connecting': statusText.textContent = 'Reconnecting...'; if (this.retryButton) { this.retryButton.disabled = true; this.retryButton.innerHTML = ' Connecting...'; } break; case 'error': statusText.textContent = `Connection error${state.retryCount > 0 ? ` (Retry ${state.retryCount})` : ''}`; this.showOfflineMessage(); this.updateQueuedInfo(); if (this.retryButton) { this.retryButton.disabled = false; this.retryButton.innerHTML = ' Retry'; } break; } } private showOfflineMessage(): void { if (this.offlineMessage) { this.offlineMessage.style.display = 'block'; setTimeout(() => { this.offlineMessage?.classList.add('show'); }, 10); } } private hideOfflineMessage(): void { if (this.offlineMessage) { this.offlineMessage.classList.remove('show'); setTimeout(() => { if (this.offlineMessage) { this.offlineMessage.style.display = 'none'; } }, 300); } } private updateQueuedInfo(): void { const queueStatus = this.queueManager.getStatus(); const queuedByType = this.queueManager.getQueuedByType(); const queuedInfo = this.offlineMessage?.querySelector('.queued-info') as HTMLElement; const queuedCount = this.offlineMessage?.querySelector('.queued-count') as HTMLElement; if (queuedInfo && queuedCount) { const totalQueued = queueStatus.queueLength + queueStatus.activeRequests; if (totalQueued > 0) { queuedInfo.style.display = 'block'; const parts = []; if (queuedByType.transcribe > 0) { parts.push(`${queuedByType.transcribe} transcription${queuedByType.transcribe > 1 ? 's' : ''}`); } if (queuedByType.translate > 0) { parts.push(`${queuedByType.translate} translation${queuedByType.translate > 1 ? 's' : ''}`); } if (queuedByType.tts > 0) { parts.push(`${queuedByType.tts} audio generation${queuedByType.tts > 1 ? 's' : ''}`); } queuedCount.textContent = `${totalQueued} request${totalQueued > 1 ? 's' : ''} queued${parts.length > 0 ? ': ' + parts.join(', ') : ''}`; } else { queuedInfo.style.display = 'none'; } } } private async handleRetry(): Promise { if (this.retryButton) { this.retryButton.disabled = true; this.retryButton.innerHTML = ' Connecting...'; } const success = await this.connectionManager.reconnect(); if (!success && this.retryButton) { this.retryButton.disabled = false; this.retryButton.innerHTML = ' Retry'; } } // Public method to show temporary connection message showTemporaryMessage(message: string, type: 'success' | 'error' | 'warning' = 'success'): void { if (!this.statusElement) return; const statusText = this.statusElement.querySelector('.connection-text') as HTMLElement; const originalClass = this.statusElement.className; const originalText = statusText.textContent; // Update appearance based on type this.statusElement.className = `connection-status visible ${type === 'success' ? 'online' : type === 'error' ? 'offline' : 'connecting'}`; statusText.textContent = message; // Reset after 3 seconds setTimeout(() => { if (this.statusElement && statusText) { this.statusElement.className = originalClass; statusText.textContent = originalText || ''; } }, 3000); } }