/** * Reddit Video Maker Bot - Progress GUI JavaScript * Real-time progress tracking via WebSocket */ class ProgressTracker { constructor() { this.socket = null; this.connected = false; this.currentJob = null; this.jobHistory = []; // DOM elements this.elements = { noJob: document.getElementById('no-job'), jobInfo: document.getElementById('job-info'), jobSubreddit: document.getElementById('job-subreddit'), jobTitle: document.getElementById('job-title'), jobStatusBadge: document.getElementById('job-status-badge'), overallProgressFill: document.getElementById('overall-progress-fill'), overallProgressText: document.getElementById('overall-progress-text'), stepsContainer: document.getElementById('steps-container'), previewContainer: document.getElementById('preview-container'), historyList: document.getElementById('history-list'), connectionDot: document.getElementById('connection-dot'), connectionText: document.getElementById('connection-text'), }; this.stepIcons = { pending: '○', in_progress: '◐', completed: '✓', failed: '✗', skipped: '⊘', }; this.init(); } init() { this.connectWebSocket(); this.fetchInitialStatus(); } connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}`; this.socket = io(wsUrl + '/progress', { transports: ['websocket', 'polling'], reconnection: true, reconnectionDelay: 1000, reconnectionAttempts: Infinity, }); this.socket.on('connect', () => { this.connected = true; this.updateConnectionStatus(true); console.log('Connected to progress server'); }); this.socket.on('disconnect', () => { this.connected = false; this.updateConnectionStatus(false); console.log('Disconnected from progress server'); }); this.socket.on('progress_update', (data) => { this.handleProgressUpdate(data); }); this.socket.on('connect_error', (error) => { console.error('Connection error:', error); this.updateConnectionStatus(false); }); } fetchInitialStatus() { fetch('/api/status') .then(response => response.json()) .then(data => this.handleProgressUpdate(data)) .catch(error => console.error('Error fetching status:', error)); } updateConnectionStatus(connected) { const { connectionDot, connectionText } = this.elements; if (connected) { connectionDot.classList.add('connected'); connectionDot.classList.remove('disconnected'); connectionText.textContent = 'Connected'; } else { connectionDot.classList.remove('connected'); connectionDot.classList.add('disconnected'); connectionText.textContent = 'Disconnected - Reconnecting...'; } } handleProgressUpdate(data) { this.currentJob = data.current_job; this.jobHistory = data.job_history || []; this.renderCurrentJob(); this.renderHistory(); } renderCurrentJob() { const { noJob, jobInfo, jobSubreddit, jobTitle, jobStatusBadge, overallProgressFill, overallProgressText, stepsContainer, previewContainer } = this.elements; if (!this.currentJob) { noJob.classList.remove('hidden'); jobInfo.classList.add('hidden'); return; } noJob.classList.add('hidden'); jobInfo.classList.remove('hidden'); // Update job info jobSubreddit.textContent = `r/${this.currentJob.subreddit}`; jobTitle.textContent = this.currentJob.title; // Update status badge jobStatusBadge.textContent = this.formatStatus(this.currentJob.status); jobStatusBadge.className = `status-badge ${this.currentJob.status}`; // Update overall progress const progress = this.currentJob.overall_progress || 0; overallProgressFill.style.width = `${progress}%`; overallProgressText.textContent = `${Math.round(progress)}%`; // Render steps this.renderSteps(stepsContainer, this.currentJob.steps); // Update preview this.updatePreview(previewContainer, this.currentJob.steps); } renderSteps(container, steps) { container.innerHTML = ''; steps.forEach((step, index) => { const stepEl = document.createElement('div'); stepEl.className = `step ${this.getStepClass(step.status)}`; const icon = this.getStepIcon(step.status); const isActive = step.status === 'in_progress'; stepEl.innerHTML = `
${isActive ? '
' : icon}
${step.name}
${step.description}
${step.message ? `
${step.message}
` : ''}
${step.status === 'in_progress' ? `
${Math.round(step.progress)}%
` : ''} `; container.appendChild(stepEl); }); } getStepClass(status) { const classMap = { pending: 'pending', in_progress: 'active', completed: 'completed', failed: 'failed', skipped: 'skipped', }; return classMap[status] || 'pending'; } getStepIcon(status) { return this.stepIcons[status] || this.stepIcons.pending; } formatStatus(status) { const statusMap = { pending: 'Pending', in_progress: 'In Progress', completed: 'Completed', failed: 'Failed', skipped: 'Skipped', }; return statusMap[status] || status; } updatePreview(container, steps) { // Find the latest step with a preview let previewPath = null; for (let i = steps.length - 1; i >= 0; i--) { if (steps[i].preview_path) { previewPath = steps[i].preview_path; break; } } if (!previewPath) { container.innerHTML = `

Preview will appear here during processing

`; return; } // Determine if it's an image or video const extension = previewPath.split('.').pop().toLowerCase(); const isVideo = ['mp4', 'webm', 'mov'].includes(extension); if (isVideo) { container.innerHTML = ` `; } else { container.innerHTML = ` Preview `; } } renderHistory() { const { historyList } = this.elements; if (!this.jobHistory || this.jobHistory.length === 0) { historyList.innerHTML = '

No completed jobs yet

'; return; } historyList.innerHTML = ''; // Show most recent first const sortedHistory = [...this.jobHistory].reverse(); sortedHistory.forEach(job => { const item = document.createElement('div'); item.className = 'history-item'; const duration = job.completed_at && job.created_at ? this.formatDuration(job.completed_at - job.created_at) : 'N/A'; const statusClass = job.status === 'completed' ? 'completed' : 'failed'; item.innerHTML = `
${job.status === 'completed' ? '✓' : '✗'}
r/${job.subreddit}
${job.title}
${this.formatDate(job.created_at)} • ${duration} ${job.error ? `• ${job.error}` : ''}
${job.output_path ? ` View Video ` : ''}
`; historyList.appendChild(item); }); } formatDuration(seconds) { if (seconds < 60) { return `${Math.round(seconds)}s`; } else if (seconds < 3600) { const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return `${mins}m ${secs}s`; } else { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } } formatDate(timestamp) { const date = new Date(timestamp * 1000); return date.toLocaleString(); } } // Initialize the progress tracker when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.progressTracker = new ProgressTracker(); });