/** * 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 `
${this.escapeHtml(task.file || '')} ${this.getStatusLabel(task.status)}
${this.escapeHtml(task.message || '')}
`; } getStatusLabel(status) { const labels = { 'queued': 'В очереди', 'processing': 'Обработка', 'completed': 'Готово', 'error': 'Ошибка', }; return labels[status] || status; } // ===== File Tree ===== renderFileTree(tree) { const container = document.getElementById('fileTree'); if (!tree || tree.length === 0) { container.innerHTML = '

Нет обработанных файлов

'; 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 `
${icon} ${this.escapeHtml(file.name)} ${this.formatBytes(file.size)}
`; }).join(''); return `
📁 ${this.escapeHtml(folder.name)} ${this.formatDate(folder.created)}
${files}
`; } setupFileTreeEvents() { const container = document.getElementById('fileTree'); container.addEventListener('click', (e) => { const folderHeader = e.target.closest('.folder-header'); if (folderHeader) { const folder = folderHeader.closest('.folder-item'); const files = folder.querySelector('.folder-files'); const toggle = folderHeader.querySelector('.folder-toggle'); if (files.style.display === 'none') { files.style.display = 'block'; toggle.textContent = '▼'; } else { files.style.display = 'none'; toggle.textContent = '▶'; } } const fileItem = e.target.closest('.file-item.clickable'); if (fileItem) { const path = fileItem.dataset.path; const ext = fileItem.dataset.ext; this.loadFileContent(path, ext); } }); } // ===== 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 = `
${this.escapeHtml(result.error)}
`; return; } // Render markdown const html = marked.parse(result.content); viewer.innerHTML = `
${this.escapeHtml(path)} ⬇️ Скачать
${html}
`; this.currentFile = path; } catch (error) { viewer.innerHTML = `
Ошибка загрузки: ${this.escapeHtml(error.message)}
`; } } else if (ext === '.docx') { viewer.innerHTML = `

Для просмотра DOCX скачайте файл:

⬇️ Скачать ${this.escapeHtml(path)}
`; } } // ===== Utilities ===== showToast(message, type = 'info') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.classList.add('toast-hide'); setTimeout(() => toast.remove(), 300); }, 4000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } formatDate(isoDate) { const date = new Date(isoDate); return date.toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } } // Initialize document.addEventListener('DOMContentLoaded', () => { const app = new TranscriptionApp(); app.setupFileTreeEvents(); });