// 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);
}
}