|
|
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() { |
|
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
this.removeFile.addEventListener('click', this.clearFile.bind(this)); |
|
|
|
|
|
|
|
|
this.modelSelect.addEventListener('change', this.updateTranslateButton.bind(this)); |
|
|
|
|
|
|
|
|
this.translateBtn.addEventListener('click', this.startTranslation.bind(this)); |
|
|
|
|
|
|
|
|
this.newTranslationBtn.addEventListener('click', this.resetInterface.bind(this)); |
|
|
|
|
|
|
|
|
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; |
|
|
this.modelSelect.appendChild(option); |
|
|
}); |
|
|
|
|
|
|
|
|
const modelGroup = this.modelSelect.closest('.form-group'); |
|
|
if (modelGroup) { |
|
|
modelGroup.style.display = 'block'; |
|
|
|
|
|
this.modelInfo.style.display = 'none'; |
|
|
} |
|
|
|
|
|
this.updateTranslateButton(); |
|
|
} catch (error) { |
|
|
console.error('Error loading models:', error); |
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
async startTranslation() { |
|
|
if (!this.currentFile) { |
|
|
this.showError('Please select a file first.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
this.reportContent.innerHTML = ` |
|
|
<div class="report-item"> |
|
|
<span class="report-label">Original File:</span> |
|
|
<span class="report-value">${result.original_filename}</span> |
|
|
</div> |
|
|
<div class="report-item"> |
|
|
<span class="report-label">Translated File:</span> |
|
|
<span class="report-value">${result.translated_filename}</span> |
|
|
</div> |
|
|
<div class="report-item"> |
|
|
<span class="report-label">Pages Processed:</span> |
|
|
<span class="report-value">${result.pages_translated}</span> |
|
|
</div> |
|
|
<div class="report-item"> |
|
|
<span class="report-label">Paragraphs Translated:</span> |
|
|
<span class="report-value">${result.paragraphs_translated}</span> |
|
|
</div> |
|
|
<div class="report-item"> |
|
|
<span class="report-label">Model Used:</span> |
|
|
<span class="report-value">${result.model_used}</span> |
|
|
</div> |
|
|
<div class="report-item"> |
|
|
<span class="report-label">Languages:</span> |
|
|
<span class="report-value">${result.source_language} → ${result.target_language}</span> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
|
|
|
this.originalFileName.textContent = result.original_filename; |
|
|
this.translatedFileName.textContent = result.translated_filename; |
|
|
|
|
|
if (result.files.original) { |
|
|
this.downloadOriginal.onclick = () => this.downloadFile(result.files.original, result.original_filename); |
|
|
} |
|
|
|
|
|
if (result.files.translated) { |
|
|
this.downloadTranslated.onclick = () => this.downloadFile(result.files.translated, result.translated_filename); |
|
|
} |
|
|
} |
|
|
|
|
|
hideResults() { |
|
|
this.resultsSection.style.display = 'none'; |
|
|
this.resultsSection.classList.remove('fade-in'); |
|
|
} |
|
|
|
|
|
showError(message) { |
|
|
this.errorMessage.textContent = message; |
|
|
this.errorSection.style.display = 'block'; |
|
|
this.errorSection.classList.add('fade-in'); |
|
|
|
|
|
|
|
|
this.uploadArea.classList.add('shake'); |
|
|
setTimeout(() => { |
|
|
this.uploadArea.classList.remove('shake'); |
|
|
}, 500); |
|
|
} |
|
|
|
|
|
hideError() { |
|
|
this.errorSection.style.display = 'none'; |
|
|
this.errorSection.classList.remove('fade-in'); |
|
|
} |
|
|
|
|
|
setTranslatingState(isTranslating) { |
|
|
if (isTranslating) { |
|
|
this.translateBtn.querySelector('.btn-text').style.display = 'none'; |
|
|
this.translateBtn.querySelector('.btn-spinner').style.display = 'inline'; |
|
|
this.translateBtn.disabled = true; |
|
|
} else { |
|
|
this.translateBtn.querySelector('.btn-text').style.display = 'inline'; |
|
|
this.translateBtn.querySelector('.btn-spinner').style.display = 'none'; |
|
|
this.updateTranslateButton(); |
|
|
} |
|
|
} |
|
|
|
|
|
async downloadFile(filePath, filename) { |
|
|
try { |
|
|
const response = await fetch(`/download/${filePath}`); |
|
|
if (!response.ok) { |
|
|
throw new Error('Download failed'); |
|
|
} |
|
|
|
|
|
const blob = await response.blob(); |
|
|
const url = window.URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.style.display = 'none'; |
|
|
a.href = url; |
|
|
a.download = filename; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
window.URL.revokeObjectURL(url); |
|
|
document.body.removeChild(a); |
|
|
} catch (error) { |
|
|
console.error('Download error:', error); |
|
|
this.showError('Failed to download file. Please try again.'); |
|
|
} |
|
|
} |
|
|
|
|
|
resetInterface() { |
|
|
this.clearFile(); |
|
|
this.hideResults(); |
|
|
this.hideError(); |
|
|
this.hideProgress(); |
|
|
this.modelSelect.selectedIndex = 0; |
|
|
this.sourceLanguage.selectedIndex = 0; |
|
|
this.targetLanguage.selectedIndex = 0; |
|
|
this.updateTranslateButton(); |
|
|
} |
|
|
|
|
|
formatFileSize(bytes) { |
|
|
if (bytes === 0) return '0 Bytes'; |
|
|
const k = 1024; |
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
new DocumentTranslator(); |
|
|
}); |