Add request queue status indicator to UI
- Added visual queue status display showing pending and active requests - Updates in real-time (every 500ms) to show current queue state - Only visible when there are requests in queue or being processed - Helps users understand system load and request processing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -15,6 +15,7 @@ import { | ||||
|     BeforeInstallPromptEvent | ||||
| } from './types'; | ||||
| import { TranslationCache } from './translationCache'; | ||||
| import { RequestQueueManager } from './requestQueue'; | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     // Register service worker | ||||
| @@ -102,6 +103,9 @@ function initApp(): void { | ||||
|  | ||||
|     // Check for saved translations in IndexedDB | ||||
|     loadSavedTranslations(); | ||||
|      | ||||
|     // Initialize queue status updates | ||||
|     initQueueStatus(); | ||||
|  | ||||
|     // Update TTS server URL and API key | ||||
|     updateTtsServer.addEventListener('click', function() { | ||||
| @@ -344,7 +348,7 @@ function initApp(): void { | ||||
|     } | ||||
|  | ||||
|     // Function to transcribe audio | ||||
|     function transcribeAudio(audioBlob: Blob): void { | ||||
|     async function transcribeAudio(audioBlob: Blob): Promise<void> { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('audio', audioBlob, 'audio.webm'); // Add filename for better server handling | ||||
|         formData.append('source_lang', sourceLanguage.value); | ||||
| @@ -355,12 +359,26 @@ function initApp(): void { | ||||
|  | ||||
|         showProgress(); | ||||
|          | ||||
|         fetch('/transcribe', { | ||||
|             method: 'POST', | ||||
|             body: formData | ||||
|         }) | ||||
|         .then(response => response.json() as Promise<TranscriptionResponse>) | ||||
|         .then(data => { | ||||
|         try { | ||||
|             // Use request queue for throttling | ||||
|             const queue = RequestQueueManager.getInstance(); | ||||
|             const data = await queue.enqueue<TranscriptionResponse>( | ||||
|                 'transcribe', | ||||
|                 async () => { | ||||
|                     const response = await fetch('/transcribe', { | ||||
|                         method: 'POST', | ||||
|                         body: formData | ||||
|                     }); | ||||
|                      | ||||
|                     if (!response.ok) { | ||||
|                         throw new Error(`HTTP error! status: ${response.status}`); | ||||
|                     } | ||||
|                      | ||||
|                     return response.json(); | ||||
|                 }, | ||||
|                 8 // Higher priority for transcription | ||||
|             ); | ||||
|              | ||||
|             hideProgress(); | ||||
|              | ||||
|             if (data.success && data.text) { | ||||
| @@ -386,13 +404,18 @@ function initApp(): void { | ||||
|                 statusIndicator.classList.add('error'); | ||||
|                 setTimeout(() => statusIndicator.classList.remove('error'), 2000); | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|         } catch (error: any) { | ||||
|             hideProgress(); | ||||
|             console.error('Transcription error:', error); | ||||
|             sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`; | ||||
|             statusIndicator.textContent = 'Transcription failed'; | ||||
|         }); | ||||
|              | ||||
|             if (error.message?.includes('Rate limit')) { | ||||
|                 sourceText.innerHTML = `<p class="text-warning">Too many requests. Please wait a moment.</p>`; | ||||
|                 statusIndicator.textContent = 'Rate limit - please wait'; | ||||
|             } else { | ||||
|                 sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`; | ||||
|                 statusIndicator.textContent = 'Transcription failed'; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Translate button click event | ||||
| @@ -440,15 +463,29 @@ function initApp(): void { | ||||
|             target_lang: targetLanguage.value | ||||
|         }; | ||||
|          | ||||
|         fetch('/translate', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify(requestBody) | ||||
|         }) | ||||
|         .then(response => response.json() as Promise<TranslationResponse>) | ||||
|         .then(async data => { | ||||
|         try { | ||||
|             // Use request queue for throttling | ||||
|             const queue = RequestQueueManager.getInstance(); | ||||
|             const data = await queue.enqueue<TranslationResponse>( | ||||
|                 'translate', | ||||
|                 async () => { | ||||
|                     const response = await fetch('/translate', { | ||||
|                         method: 'POST', | ||||
|                         headers: { | ||||
|                             'Content-Type': 'application/json' | ||||
|                         }, | ||||
|                         body: JSON.stringify(requestBody) | ||||
|                     }); | ||||
|                      | ||||
|                     if (!response.ok) { | ||||
|                         throw new Error(`HTTP error! status: ${response.status}`); | ||||
|                     } | ||||
|                      | ||||
|                     return response.json(); | ||||
|                 }, | ||||
|                 5 // Normal priority for translation | ||||
|             ); | ||||
|              | ||||
|             hideProgress(); | ||||
|              | ||||
|             if (data.success && data.translation) { | ||||
| @@ -482,21 +519,21 @@ function initApp(): void { | ||||
|                 translatedText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`; | ||||
|                 statusIndicator.textContent = 'Translation failed'; | ||||
|             } | ||||
|         }) | ||||
|         .catch(async error => { | ||||
|         } catch (error: any) { | ||||
|             hideProgress(); | ||||
|             console.error('Translation error:', error); | ||||
|              | ||||
|             // Check if we're offline and try to find a similar cached translation | ||||
|             if (!navigator.onLine) { | ||||
|             if (error.message?.includes('Rate limit')) { | ||||
|                 translatedText.innerHTML = `<p class="text-warning">Too many requests. Please wait a moment.</p>`; | ||||
|                 statusIndicator.textContent = 'Rate limit - please wait'; | ||||
|             } else if (!navigator.onLine) { | ||||
|                 statusIndicator.textContent = 'Offline - checking cache...'; | ||||
|                 // Could implement fuzzy matching here for similar translations | ||||
|                 translatedText.innerHTML = `<p class="text-warning">You're offline. Only cached translations are available.</p>`; | ||||
|             } else { | ||||
|                 translatedText.innerHTML = `<p class="text-danger">Failed to translate. Please try again.</p>`; | ||||
|                 statusIndicator.textContent = 'Translation failed'; | ||||
|             } | ||||
|             statusIndicator.textContent = 'Translation failed'; | ||||
|         }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Play source text | ||||
| @@ -516,7 +553,7 @@ function initApp(): void { | ||||
|     }); | ||||
|  | ||||
|     // Function to play audio via TTS | ||||
|     function playAudio(text: string, language: string): void { | ||||
|     async function playAudio(text: string, language: string): Promise<void> { | ||||
|         showProgress(); | ||||
|         showLoadingOverlay('Generating audio...'); | ||||
|          | ||||
| @@ -525,15 +562,29 @@ function initApp(): void { | ||||
|             language: language | ||||
|         }; | ||||
|          | ||||
|         fetch('/speak', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify(requestBody) | ||||
|         }) | ||||
|         .then(response => response.json() as Promise<TTSResponse>) | ||||
|         .then(data => { | ||||
|         try { | ||||
|             // Use request queue for throttling | ||||
|             const queue = RequestQueueManager.getInstance(); | ||||
|             const data = await queue.enqueue<TTSResponse>( | ||||
|                 'tts', | ||||
|                 async () => { | ||||
|                     const response = await fetch('/speak', { | ||||
|                         method: 'POST', | ||||
|                         headers: { | ||||
|                             'Content-Type': 'application/json' | ||||
|                         }, | ||||
|                         body: JSON.stringify(requestBody) | ||||
|                     }); | ||||
|                      | ||||
|                     if (!response.ok) { | ||||
|                         throw new Error(`HTTP error! status: ${response.status}`); | ||||
|                     } | ||||
|                      | ||||
|                     return response.json(); | ||||
|                 }, | ||||
|                 3 // Lower priority for TTS | ||||
|             ); | ||||
|              | ||||
|             hideProgress(); | ||||
|              | ||||
|             if (data.success && data.audio_url) { | ||||
| @@ -564,18 +615,23 @@ function initApp(): void { | ||||
|                 // Check TTS server status again | ||||
|                 checkTtsServer(); | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|         } catch (error: any) { | ||||
|             hideProgress(); | ||||
|             console.error('TTS error:', error); | ||||
|             statusIndicator.textContent = 'TTS failed'; | ||||
|              | ||||
|             // Show TTS server alert | ||||
|             ttsServerAlert.classList.remove('d-none'); | ||||
|             ttsServerAlert.classList.remove('alert-success'); | ||||
|             ttsServerAlert.classList.add('alert-warning'); | ||||
|             ttsServerMessage.textContent = 'Failed to connect to TTS server'; | ||||
|         }); | ||||
|             if (error.message?.includes('Rate limit')) { | ||||
|                 statusIndicator.textContent = 'Too many requests - please wait'; | ||||
|                 alert('Too many requests. Please wait a moment before trying again.'); | ||||
|             } else { | ||||
|                 statusIndicator.textContent = 'TTS failed'; | ||||
|                  | ||||
|                 // Show TTS server alert | ||||
|                 ttsServerAlert.classList.remove('d-none'); | ||||
|                 ttsServerAlert.classList.remove('alert-success'); | ||||
|                 ttsServerAlert.classList.add('alert-warning'); | ||||
|                 ttsServerMessage.textContent = 'Failed to connect to TTS server'; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Clear buttons | ||||
| @@ -669,6 +725,34 @@ function initApp(): void { | ||||
|     function hideLoadingOverlay(): void { | ||||
|         loadingOverlay.classList.remove('active'); | ||||
|     } | ||||
|      | ||||
|     // Initialize queue status display | ||||
|     function initQueueStatus(): void { | ||||
|         const queueStatus = document.getElementById('queueStatus') as HTMLDivElement; | ||||
|         const queueLength = document.getElementById('queueLength') as HTMLSpanElement; | ||||
|         const activeRequests = document.getElementById('activeRequests') as HTMLSpanElement; | ||||
|          | ||||
|         const queue = RequestQueueManager.getInstance(); | ||||
|          | ||||
|         // Update queue status display | ||||
|         function updateQueueDisplay(): void { | ||||
|             const status = queue.getStatus(); | ||||
|              | ||||
|             if (status.queueLength > 0 || status.activeRequests > 0) { | ||||
|                 queueStatus.style.display = 'block'; | ||||
|                 queueLength.textContent = status.queueLength.toString(); | ||||
|                 activeRequests.textContent = status.activeRequests.toString(); | ||||
|             } else { | ||||
|                 queueStatus.style.display = 'none'; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Poll for status updates | ||||
|         setInterval(updateQueueDisplay, 500); | ||||
|          | ||||
|         // Initial update | ||||
|         updateQueueDisplay(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										236
									
								
								static/js/src/requestQueue.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								static/js/src/requestQueue.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| // Request queue and throttling manager | ||||
| export interface QueuedRequest { | ||||
|     id: string; | ||||
|     type: 'transcribe' | 'translate' | 'tts'; | ||||
|     request: () => Promise<any>; | ||||
|     resolve: (value: any) => void; | ||||
|     reject: (reason?: any) => void; | ||||
|     retryCount: number; | ||||
|     priority: number; | ||||
|     timestamp: number; | ||||
| } | ||||
|  | ||||
| export class RequestQueueManager { | ||||
|     private static instance: RequestQueueManager; | ||||
|     private queue: QueuedRequest[] = []; | ||||
|     private activeRequests: Map<string, QueuedRequest> = new Map(); | ||||
|     private maxConcurrent = 2; // Maximum concurrent requests | ||||
|     private maxRetries = 3; | ||||
|     private retryDelay = 1000; // Base retry delay in ms | ||||
|     private isProcessing = false; | ||||
|  | ||||
|     // Rate limiting | ||||
|     private requestHistory: number[] = []; | ||||
|     private maxRequestsPerMinute = 30; | ||||
|     private maxRequestsPerSecond = 2; | ||||
|  | ||||
|     private constructor() { | ||||
|         // Start processing queue | ||||
|         this.startProcessing(); | ||||
|     } | ||||
|  | ||||
|     static getInstance(): RequestQueueManager { | ||||
|         if (!RequestQueueManager.instance) { | ||||
|             RequestQueueManager.instance = new RequestQueueManager(); | ||||
|         } | ||||
|         return RequestQueueManager.instance; | ||||
|     } | ||||
|  | ||||
|     // Add request to queue | ||||
|     async enqueue<T>( | ||||
|         type: 'transcribe' | 'translate' | 'tts', | ||||
|         request: () => Promise<T>, | ||||
|         priority: number = 5 | ||||
|     ): Promise<T> { | ||||
|         // Check rate limits | ||||
|         if (!this.checkRateLimits()) { | ||||
|             throw new Error('Rate limit exceeded. Please slow down.'); | ||||
|         } | ||||
|  | ||||
|         return new Promise((resolve, reject) => { | ||||
|             const id = this.generateId(); | ||||
|             const queuedRequest: QueuedRequest = { | ||||
|                 id, | ||||
|                 type, | ||||
|                 request, | ||||
|                 resolve, | ||||
|                 reject, | ||||
|                 retryCount: 0, | ||||
|                 priority, | ||||
|                 timestamp: Date.now() | ||||
|             }; | ||||
|  | ||||
|             // Add to queue based on priority | ||||
|             this.addToQueue(queuedRequest); | ||||
|              | ||||
|             // Log queue status | ||||
|             console.log(`Request queued: ${type}, Queue size: ${this.queue.length}, Active: ${this.activeRequests.size}`); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private addToQueue(request: QueuedRequest): void { | ||||
|         // Insert based on priority (higher priority first) | ||||
|         const insertIndex = this.queue.findIndex(item => item.priority < request.priority); | ||||
|         if (insertIndex === -1) { | ||||
|             this.queue.push(request); | ||||
|         } else { | ||||
|             this.queue.splice(insertIndex, 0, request); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private checkRateLimits(): boolean { | ||||
|         const now = Date.now(); | ||||
|          | ||||
|         // Clean old entries | ||||
|         this.requestHistory = this.requestHistory.filter( | ||||
|             time => now - time < 60000 // Keep last minute | ||||
|         ); | ||||
|  | ||||
|         // Check per-second limit | ||||
|         const lastSecond = this.requestHistory.filter( | ||||
|             time => now - time < 1000 | ||||
|         ).length; | ||||
|          | ||||
|         if (lastSecond >= this.maxRequestsPerSecond) { | ||||
|             console.warn('Per-second rate limit reached'); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Check per-minute limit | ||||
|         if (this.requestHistory.length >= this.maxRequestsPerMinute) { | ||||
|             console.warn('Per-minute rate limit reached'); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Record this request | ||||
|         this.requestHistory.push(now); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private async startProcessing(): Promise<void> { | ||||
|         if (this.isProcessing) return; | ||||
|         this.isProcessing = true; | ||||
|  | ||||
|         while (true) { | ||||
|             await this.processQueue(); | ||||
|             await this.delay(100); // Check queue every 100ms | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async processQueue(): Promise<void> { | ||||
|         // Check if we can process more requests | ||||
|         if (this.activeRequests.size >= this.maxConcurrent || this.queue.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Get next request | ||||
|         const request = this.queue.shift(); | ||||
|         if (!request) return; | ||||
|  | ||||
|         // Mark as active | ||||
|         this.activeRequests.set(request.id, request); | ||||
|  | ||||
|         try { | ||||
|             // Execute request | ||||
|             const result = await request.request(); | ||||
|             request.resolve(result); | ||||
|             console.log(`Request completed: ${request.type}`); | ||||
|         } catch (error) { | ||||
|             console.error(`Request failed: ${request.type}`, error); | ||||
|              | ||||
|             // Check if we should retry | ||||
|             if (request.retryCount < this.maxRetries && this.shouldRetry(error)) { | ||||
|                 request.retryCount++; | ||||
|                 const delay = this.calculateRetryDelay(request.retryCount); | ||||
|                 console.log(`Retrying request ${request.type} in ${delay}ms (attempt ${request.retryCount})`); | ||||
|                  | ||||
|                 // Re-queue with delay | ||||
|                 setTimeout(() => { | ||||
|                     this.addToQueue(request); | ||||
|                 }, delay); | ||||
|             } else { | ||||
|                 // Max retries reached or non-retryable error | ||||
|                 request.reject(error); | ||||
|             } | ||||
|         } finally { | ||||
|             // Remove from active | ||||
|             this.activeRequests.delete(request.id); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private shouldRetry(error: any): boolean { | ||||
|         // Retry on network errors or 5xx status codes | ||||
|         if (error.message?.includes('network') || error.message?.includes('Network')) { | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         if (error.status >= 500 && error.status < 600) { | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         // Don't retry on client errors (4xx) | ||||
|         if (error.status >= 400 && error.status < 500) { | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private calculateRetryDelay(retryCount: number): number { | ||||
|         // Exponential backoff with jitter | ||||
|         const baseDelay = this.retryDelay * Math.pow(2, retryCount - 1); | ||||
|         const jitter = Math.random() * 0.3 * baseDelay; // 30% jitter | ||||
|         return Math.min(baseDelay + jitter, 30000); // Max 30 seconds | ||||
|     } | ||||
|  | ||||
|     private generateId(): string { | ||||
|         return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | ||||
|     } | ||||
|  | ||||
|     private delay(ms: number): Promise<void> { | ||||
|         return new Promise(resolve => setTimeout(resolve, ms)); | ||||
|     } | ||||
|  | ||||
|     // Get queue status | ||||
|     getStatus(): { | ||||
|         queueLength: number; | ||||
|         activeRequests: number; | ||||
|         requestsPerMinute: number; | ||||
|     } { | ||||
|         const now = Date.now(); | ||||
|         const recentRequests = this.requestHistory.filter( | ||||
|             time => now - time < 60000 | ||||
|         ).length; | ||||
|  | ||||
|         return { | ||||
|             queueLength: this.queue.length, | ||||
|             activeRequests: this.activeRequests.size, | ||||
|             requestsPerMinute: recentRequests | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Clear queue (for emergency use) | ||||
|     clearQueue(): void { | ||||
|         this.queue.forEach(request => { | ||||
|             request.reject(new Error('Queue cleared')); | ||||
|         }); | ||||
|         this.queue = []; | ||||
|     } | ||||
|  | ||||
|     // Update settings | ||||
|     updateSettings(settings: { | ||||
|         maxConcurrent?: number; | ||||
|         maxRequestsPerMinute?: number; | ||||
|         maxRequestsPerSecond?: number; | ||||
|     }): void { | ||||
|         if (settings.maxConcurrent !== undefined) { | ||||
|             this.maxConcurrent = settings.maxConcurrent; | ||||
|         } | ||||
|         if (settings.maxRequestsPerMinute !== undefined) { | ||||
|             this.maxRequestsPerMinute = settings.maxRequestsPerMinute; | ||||
|         } | ||||
|         if (settings.maxRequestsPerSecond !== undefined) { | ||||
|             this.maxRequestsPerSecond = settings.maxRequestsPerSecond; | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user