- Implement ConnectionManager with exponential backoff retry strategy - Add automatic connection monitoring and health checks - Update RequestQueueManager to integrate with connection state - Create ConnectionUI component for visual connection status - Queue requests during offline periods and process when online - Add comprehensive error handling for network-related failures - Create detailed documentation for connection retry features - Support manual retry and automatic recovery Features: - Real-time connection status indicator - Offline banner with retry button - Request queue visualization - Priority-based request processing - Configurable retry parameters 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
325 lines
12 KiB
TypeScript
325 lines
12 KiB
TypeScript
// 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 = `
|
|
<span class="connection-icon"></span>
|
|
<span class="connection-text">Checking connection...</span>
|
|
`;
|
|
|
|
// Create offline message banner
|
|
this.offlineMessage = document.createElement('div');
|
|
this.offlineMessage.id = 'offlineMessage';
|
|
this.offlineMessage.className = 'offline-message';
|
|
this.offlineMessage.innerHTML = `
|
|
<div class="offline-content">
|
|
<i class="fas fa-wifi-slash"></i>
|
|
<span class="offline-text">You're offline. Some features may be limited.</span>
|
|
<button class="btn btn-sm btn-outline-light retry-connection">
|
|
<i class="fas fa-sync"></i> Retry
|
|
</button>
|
|
<div class="queued-info" style="display: none;">
|
|
<small class="queued-count"></small>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = '<i class="fas fa-spinner fa-spin"></i> 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 = '<i class="fas fa-sync"></i> 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<void> {
|
|
if (this.retryButton) {
|
|
this.retryButton.disabled = true;
|
|
this.retryButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Connecting...';
|
|
}
|
|
|
|
const success = await this.connectionManager.reconnect();
|
|
|
|
if (!success && this.retryButton) {
|
|
this.retryButton.disabled = false;
|
|
this.retryButton.innerHTML = '<i class="fas fa-sync"></i> 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);
|
|
}
|
|
} |