trabb / web /app.js
fokan's picture
first push
22473f8
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 = `
<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>
`;
// Setup download buttons
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');
// Add shake animation to upload area
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];
}
}
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new DocumentTranslator();
});