/** * Deep-Dive Video Note Taker — script.js * Frontend logic: upload, polling, results rendering, RAG search */ // ── State ───────────────────────────────────────────────────────────────────── let currentJobId = null; let pollingTimer = null; let selectedFile = null; let currentNotes = null; // ── DOM refs ────────────────────────────────────────────────────────────────── const fileInput = document.getElementById('file-input'); const dropZone = document.getElementById('drop-zone'); const filePreview = document.getElementById('file-preview'); const previewName = document.getElementById('preview-name'); const previewSize = document.getElementById('preview-size'); const processBtn = document.getElementById('process-btn'); const progressSection = document.getElementById('progress-section'); const progressBar = document.getElementById('progress-bar'); const progressLabel = document.getElementById('progress-label'); const progressPct = document.getElementById('progress-pct'); const resultsSection = document.getElementById('results-section'); const notesOutput = document.getElementById('notes-output'); const actionsOutput = document.getElementById('actions-output'); const transcriptOutput = document.getElementById('transcript-output'); const qaOutput = document.getElementById('qa-output'); // ── File Input ──────────────────────────────────────────────────────────────── fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); }); // 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'); const f = e.dataTransfer.files[0]; if (f) handleFile(f); }); function handleFile(file) { const ALLOWED = ['video/mp4','video/avi','video/quicktime','video/x-matroska', 'video/webm','audio/mpeg','audio/wav','audio/mp4','audio/x-m4a']; const ext = file.name.split('.').pop().toLowerCase(); const ALLOWED_EXT = ['mp4','avi','mov','mkv','webm','mp3','wav','m4a']; if (!ALLOWED_EXT.includes(ext)) { showToast('⚠️ Unsupported file type. Please upload a video or audio file.', 'error'); return; } if (file.size > 500 * 1024 * 1024) { showToast('⚠️ File too large. Maximum 500 MB.', 'error'); return; } selectedFile = file; previewName.textContent = file.name; previewSize.textContent = formatBytes(file.size); filePreview.classList.remove('hidden'); processBtn.disabled = false; showToast('✅ File selected: ' + file.name); } function clearFile() { selectedFile = null; fileInput.value = ''; filePreview.classList.add('hidden'); processBtn.disabled = true; } // ── Processing ──────────────────────────────────────────────────────────────── async function startProcessing() { if (!selectedFile) return; const openaiKey = document.getElementById('openai-key').value.trim(); const formData = new FormData(); formData.append('file', selectedFile); if (openaiKey) formData.append('openai_key', openaiKey); // UI: show progress progressSection.classList.remove('hidden'); processBtn.disabled = true; processBtn.classList.add('loading'); updateProgress(0, 'Uploading video…'); resetProgressSteps(); try { const res = await fetch('/api/v1/process/upload', { method: 'POST', body: formData, }); if (!res.ok) { const err = await res.json(); throw new Error(err.message || 'Upload failed'); } const data = await res.json(); currentJobId = data.data.job_id; showToast('🚀 Processing started! Job: ' + currentJobId.slice(0, 8) + '…'); // Start polling startPolling(); } catch (err) { showToast('❌ Error: ' + err.message, 'error'); processBtn.disabled = false; processBtn.classList.remove('loading'); updateProgress(0, 'Upload failed.'); } } function startPolling() { if (pollingTimer) clearInterval(pollingTimer); pollingTimer = setInterval(pollStatus, 2500); } async function pollStatus() { if (!currentJobId) return; try { const res = await fetch(`/api/v1/status/${currentJobId}`); const data = await res.json(); const job = data.data; updateProgress(job.progress, job.message); syncProgressSteps(job.progress); if (job.status === 'complete') { clearInterval(pollingTimer); await loadResults(currentJobId); processBtn.classList.remove('loading'); } else if (job.status === 'error') { clearInterval(pollingTimer); showToast('❌ Processing error: ' + job.message, 'error'); processBtn.disabled = false; processBtn.classList.remove('loading'); } } catch (err) { console.error('Polling error:', err); } } function updateProgress(pct, label) { progressBar.style.width = pct + '%'; progressPct.textContent = pct + '%'; progressLabel.textContent = label; } function resetProgressSteps() { document.querySelectorAll('.progress-step').forEach(s => { s.classList.remove('active', 'done'); }); } function syncProgressSteps(pct) { const steps = [ { el: document.querySelector('[data-step="audio"]'), threshold: 5 }, { el: document.querySelector('[data-step="asr"]'), threshold: 20 }, { el: document.querySelector('[data-step="llm"]'), threshold: 50 }, { el: document.querySelector('[data-step="rag"]'), threshold: 75 }, { el: document.querySelector('[data-step="notes"]'), threshold: 90 }, ]; steps.forEach(({ el, threshold }, i) => { if (!el) return; el.classList.remove('active', 'done'); if (pct >= threshold && (i === steps.length - 1 || pct < steps[i + 1]?.threshold)) { el.classList.add('active'); } else if (pct >= threshold) { el.classList.add('done'); } }); } // ── Load & Render Results ───────────────────────────────────────────────────── async function loadResults(jobId) { try { const res = await fetch(`/api/v1/notes/${jobId}`); const data = await res.json(); currentNotes = data.data; renderNotes(currentNotes.notes, currentNotes.markdown); renderQuiz(currentNotes.notes.quiz || []); renderTopics(currentNotes.notes.topics || []); renderQA(currentNotes.notes.qa_pairs || []); renderTranscript(currentNotes.notes.transcript_segments || []); resultsSection.classList.remove('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); showToast('✅ Notes ready! Scroll down to view.', 'success'); updateProgress(100, '✅ Complete!'); syncProgressSteps(100); } catch (err) { showToast('❌ Failed to load results: ' + err.message, 'error'); } } // ── Render: Notes (Markdown → HTML) ────────────────────────────────────────── function renderNotes(notes, markdown) { if (!markdown) { notesOutput.innerHTML = '
No notes generated.
'; return; } notesOutput.innerHTML = markdownToHtml(markdown); } function markdownToHtml(md) { // Headings md = md.replace(/^### (.+)$/gm, '$1');
// Code blocks
md = md.replace(/```[\w]*\n([\s\S]*?)```/g, '$1');
// Horizontal rule
md = md.replace(/^---$/gm, '$1'); // Unordered list items md = md.replace(/^\s*[-•*] (.+)$/gm, '
${line}
`; }); return md; } // ── Translate Notes ─────────────────────────────────────────────────────────── async function translateNotes() { if (!currentJobId) { showToast('⚠️ Process a video first to translate notes.', 'error'); return; } const lang = document.getElementById('translate-lang').value; const btn = document.getElementById('translate-btn'); const originalText = btn.innerHTML; btn.innerHTML = '⏳ Translating...'; btn.disabled = true; notesOutput.innerHTML = `No quiz available.
'; return; } window.checkQuizAnswer = function(qIndex, optIndex, correctIndex, el) { const parent = el.closest('.action-card'); const buttons = parent.querySelectorAll('button'); // Disable all buttons in this question buttons.forEach(btn => btn.disabled = true); // Highlight correct answer buttons[correctIndex].style.backgroundColor = 'rgba(16, 185, 129, 0.2)'; // Green tint buttons[correctIndex].style.borderColor = '#10b981'; buttons[correctIndex].style.color = '#10b981'; buttons[correctIndex].innerHTML += ' ✅'; // Highlight selected if incorrect if (optIndex !== correctIndex) { el.style.backgroundColor = 'rgba(239, 68, 68, 0.2)'; // Red tint el.style.borderColor = '#ef4444'; el.style.color = '#ef4444'; el.innerHTML += ' ❌'; } }; container.innerHTML = quiz.map((q, qIndex) => `No topics extracted yet. Process a video to see structured topic summaries.
'; return; } actionsOutput.innerHTML = topics.map(item => `No Q&A generated.
'; return; } qaOutput.innerHTML = qaPairs.map(item => `No transcript available.
'; return; } transcriptOutput.innerHTML = segments.map(seg => `