class DocumentTranslator { constructor() { this.currentFile = null; this.models = []; this.initializeElements(); this.setupEventListeners(); this.loadModels(); } initializeElements() { this.uploadArea = document.getElementById('uploadArea'); this.fileInput = document.getElementById('fileInput'); this.fileInfo = document.getElementById('fileInfo'); this.fileName = document.getElementById('fileName'); this.fileSize = document.getElementById('fileSize'); this.removeFile = document.getElementById('removeFile'); this.modelSelect = document.getElementById('modelSelect'); this.sourceLanguage = document.getElementById('sourceLanguage'); this.targetLanguage = document.getElementById('targetLanguage'); this.translateBtn = document.getElementById('translateBtn'); this.progressSection = document.getElementById('progressSection'); this.progressFill = document.getElementById('progressFill'); this.progressText = document.getElementById('progressText'); this.resultsSection = document.getElementById('resultsSection'); this.reportContent = document.getElementById('reportContent'); this.originalFileName = document.getElementById('originalFileName'); this.translatedFileName = document.getElementById('translatedFileName'); this.downloadOriginal = document.getElementById('downloadOriginal'); this.downloadTranslated = document.getElementById('downloadTranslated'); this.newTranslationBtn = document.getElementById('newTranslationBtn'); this.errorSection = document.getElementById('errorSection'); this.errorMessage = document.getElementById('errorMessage'); this.retryBtn = document.getElementById('retryBtn'); this.modelInfo = document.getElementById('modelInfo'); } setupEventListeners() { // File upload events this.uploadArea.addEventListener('click', () => this.fileInput.click()); this.uploadArea.addEventListener('dragover', this.handleDragOver.bind(this)); this.uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this)); this.uploadArea.addEventListener('drop', this.handleDrop.bind(this)); this.fileInput.addEventListener('change', this.handleFileSelect.bind(this)); // File removal this.removeFile.addEventListener('click', this.clearFile.bind(this)); // Model selection this.modelSelect.addEventListener('change', this.updateTranslateButton.bind(this)); // Translation this.translateBtn.addEventListener('click', this.startTranslation.bind(this)); // New translation this.newTranslationBtn.addEventListener('click', this.resetInterface.bind(this)); // Retry this.retryBtn.addEventListener('click', this.hideError.bind(this)); } async loadModels() { try { const response = await fetch('/models'); const data = await response.json(); this.models = data.models; this.modelSelect.innerHTML = ''; this.models.forEach((model, index) => { const option = document.createElement('option'); option.value = model.id; option.textContent = `${model.name}${model.description ? ' - ' + model.description : ''}`; option.selected = index === 0; // Auto-select the first model this.modelSelect.appendChild(option); }); // Show model selection since we have multiple models const modelGroup = this.modelSelect.closest('.form-group'); if (modelGroup) { modelGroup.style.display = 'block'; // Hide model info card since we're showing the selection this.modelInfo.style.display = 'none'; } this.updateTranslateButton(); } catch (error) { console.error('Error loading models:', error); // Fallback to the specified free models if API fails this.modelSelect.innerHTML = ''; const fallbackModels = [ { id: 'google/gemini-2.0-flash-exp:free', name: 'Google Gemini 2.0 Flash (Free)', description: 'Fast and free Google AI model' }, { id: 'tngtech/deepseek-r1t2-chimera:free', name: 'DeepSeek R1T2 Chimera (Free)', description: 'Free advanced reasoning model' } ]; fallbackModels.forEach((model, index) => { const option = document.createElement('option'); option.value = model.id; option.textContent = `${model.name} - ${model.description}`; option.selected = index === 0; this.modelSelect.appendChild(option); }); // Show model selection on fallback too const modelGroup = this.modelSelect.closest('.form-group'); if (modelGroup) { modelGroup.style.display = 'block'; this.modelInfo.style.display = 'none'; } this.updateTranslateButton(); } } handleDragOver(e) { e.preventDefault(); this.uploadArea.classList.add('dragover'); } handleDragLeave(e) { e.preventDefault(); this.uploadArea.classList.remove('dragover'); } handleDrop(e) { e.preventDefault(); this.uploadArea.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length > 0) { this.handleFile(files[0]); } } handleFileSelect(e) { const files = e.target.files; if (files.length > 0) { this.handleFile(files[0]); } } handleFile(file) { // Validate file type const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; const allowedExtensions = ['.pdf', '.docx']; const fileExtension = file.name.toLowerCase().slice(file.name.lastIndexOf('.')); if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) { this.showError('Please select a PDF or DOCX file.'); return; } // Validate file size (max 50MB) if (file.size > 50 * 1024 * 1024) { this.showError('File size must be less than 50MB.'); return; } this.currentFile = file; this.displayFileInfo(file); this.updateTranslateButton(); } displayFileInfo(file) { this.fileName.textContent = file.name; this.fileSize.textContent = this.formatFileSize(file.size); this.uploadArea.style.display = 'none'; this.fileInfo.style.display = 'flex'; } clearFile() { this.currentFile = null; this.fileInput.value = ''; this.uploadArea.style.display = 'block'; this.fileInfo.style.display = 'none'; this.updateTranslateButton(); } updateTranslateButton() { const hasFile = this.currentFile !== null; const hasModel = this.modelSelect.value !== ''; this.translateBtn.disabled = !hasFile; // Only check for file since model is auto-selected } async startTranslation() { if (!this.currentFile) { this.showError('Please select a file first.'); return; } // Ensure model is selected (should be auto-selected) if (!this.modelSelect.value && this.modelSelect.options.length > 0) { this.modelSelect.selectedIndex = 0; } this.hideError(); this.hideResults(); this.showProgress(); this.setTranslatingState(true); try { const formData = new FormData(); formData.append('file', this.currentFile); formData.append('model', this.modelSelect.value); formData.append('source_language', this.sourceLanguage.value); formData.append('target_language', this.targetLanguage.value); this.updateProgress(20, 'Uploading file...'); const response = await fetch('/translate', { method: 'POST', body: formData }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Translation failed'); } this.updateProgress(90, 'Processing translation...'); const result = await response.json(); this.updateProgress(100, 'Translation complete!'); setTimeout(() => { this.hideProgress(); this.showResults(result); this.setTranslatingState(false); }, 1000); } catch (error) { console.error('Translation error:', error); this.hideProgress(); this.setTranslatingState(false); // Handle rate limit errors specifically if (error.message && (error.message.includes('rate limit') || error.message.includes('429'))) { this.showError('Rate limit exceeded. Please try again later or switch to a different model.'); } else { this.showError(error.message || 'Translation failed. Please try again.'); } } } showProgress() { this.progressSection.style.display = 'block'; this.progressSection.classList.add('fade-in'); this.updateProgress(10, 'Starting translation...'); } hideProgress() { this.progressSection.style.display = 'none'; this.progressSection.classList.remove('fade-in'); } updateProgress(percentage, text) { this.progressFill.style.width = `${percentage}%`; this.progressText.textContent = text; } showResults(result) { this.resultsSection.style.display = 'block'; this.resultsSection.classList.add('fade-in'); // Populate report this.reportContent.innerHTML = `