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:
parent
08791d2fed
commit
829e8c3978
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user