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:
Adolfo Delorenzo 2025-06-02 22:29:45 -06:00
parent 08791d2fed
commit 829e8c3978
3 changed files with 374 additions and 47 deletions

View File

@ -15,6 +15,7 @@ import {
BeforeInstallPromptEvent
} from './types';
import { TranslationCache } from './translationCache';
import { RequestQueueManager } from './requestQueue';
document.addEventListener('DOMContentLoaded', function() {
// Register service worker
@ -103,6 +104,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() {
const newUrl = ttsServerUrl.value.trim();
@ -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();
}
}

View 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;
}
}
}

View File

@ -184,6 +184,13 @@
<i class="fas fa-microphone"></i>
</button>
<p class="status-indicator" id="statusIndicator">Click to start recording</p>
<!-- Queue Status Indicator -->
<div id="queueStatus" class="text-center mt-2" style="display: none;">
<small class="text-muted">
<i class="fas fa-list"></i> Queue: <span id="queueLength">0</span> |
<i class="fas fa-sync"></i> Active: <span id="activeRequests">0</span>
</small>
</div>
</div>
<div class="text-center mt-3">