/** * Frontend application for Transcription Service */ class TranscriptionApp { constructor() { this.ws = null; this.tasks = new Map(); this.currentFile = null; this.init(); } init() { this.connectWebSocket(); this.setupUpload(); } // ===== WebSocket ===== connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; this.ws = new WebSocket(`${protocol}//${window.location.host}/ws`); this.ws.onopen = () => { console.log('WebSocket connected'); this.showToast('Подключено к серверу', 'success'); this.requestTasks(); this.requestTree(); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); }; this.ws.onclose = () => { console.log('WebSocket disconnected, reconnecting in 3s...'); setTimeout(() => this.connectWebSocket(), 3000); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; } sendWS(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } } requestTasks() { this.sendWS({ action: 'get_tasks' }); } requestTree() { this.sendWS({ action: 'get_tree' }); } handleWebSocketMessage(data) { if (data.type === 'tasks_list') { this.updateTasks(data.tasks); } else if (data.type === 'file_tree') { this.renderFileTree(data.tree); } else if (data.task_id) { // Прогресс обработки this.updateTaskProgress(data); } } // ===== Upload ===== setupUpload() { const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const browseLink = document.querySelector('.browse-link'); // Click on browse link (stop propagation to avoid double click) browseLink.addEventListener('click', (e) => { e.stopPropagation(); fileInput.click(); }); // Click on drop zone (but not on the file input itself) dropZone.addEventListener('click', (e) => { if (e.target === fileInput) return; fileInput.click(); }); // File input change - reset value after handling fileInput.addEventListener('change', (e) => { const files = e.target.files; if (files.length) { this.handleFiles(files); } // Reset so the same file can be selected again fileInput.value = ''; }); // Drag & drop dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); this.handleFiles(e.dataTransfer.files); }); } async handleFiles(files) { if (!files.length) return; const formData = new FormData(); for (const file of files) { formData.append('files', file); } try { this.showToast(`Загрузка ${files.length} файл(а)...`, 'info'); const response = await fetch('/upload-batch', { method: 'POST', body: formData, }); const result = await response.json(); if (result.error) { this.showToast(`Ошибка: ${result.error}`, 'error'); } else { this.showToast(`Загружено ${result.uploaded} файл(а). Начинается обработка...`, 'success'); result.tasks.forEach(task => { this.tasks.set(task.task_id, task); }); this.renderTasks(); } } catch (error) { this.showToast(`Ошибка загрузки: ${error.message}`, 'error'); } } // ===== Tasks / Progress ===== updateTasks(tasks) { tasks.forEach(task => { this.tasks.set(task.file + '_' + task.started, task); }); this.renderTasks(); } updateTaskProgress(data) { const existing = Array.from(this.tasks.values()).find(t => t.task_id === data.task_id); if (existing) { Object.assign(existing, data); } else { this.tasks.set(data.task_id, data); } this.renderTasks(); if (data.status === 'completed') { this.showToast(`Готово: ${data.message}`, 'success'); this.requestTree(); } else if (data.status === 'error') { this.showToast(`Ошибка: ${data.message}`, 'error'); } } renderTasks() { const container = document.getElementById('tasksList'); const tasks = Array.from(this.tasks.values()); if (tasks.length === 0) { container.innerHTML = '
Нет активных задач
'; return; } container.innerHTML = tasks.map(task => this.renderTaskItem(task)).join(''); } renderTaskItem(task) { const progress = task.progress || 0; const statusClass = task.status === 'completed' ? 'success' : task.status === 'error' ? 'error' : task.status === 'processing' ? 'processing' : 'queued'; return `Нет обработанных файлов
'; return; } container.innerHTML = tree.map(folder => this.renderFolder(folder)).join(''); } renderFolder(folder) { const files = folder.files.map(file => { const isMd = file.ext === '.md'; const isDocx = file.ext === '.docx'; const icon = isMd ? '📝' : isDocx ? '📄' : '📎'; const clickable = isMd ? 'clickable' : ''; return `Нет обработанных файлов
'; } } } catch (error) { this.showToast(`Ошибка удаления: ${error.message}`, 'error'); } } // ===== Viewer ===== async loadFileContent(path, ext) { const viewer = document.getElementById('viewer'); if (ext === '.md') { try { const response = await fetch(`/api/files/content?path=${encodeURIComponent(path)}`); const result = await response.json(); if (result.error) { viewer.innerHTML = `Для просмотра DOCX скачайте файл:
⬇️ Скачать ${this.escapeHtml(path)}