Implement drag-and-drop interface for converting video and audio files to MP3 format using FFmpeg. Users can now upload files (MP4, M4A, AVI, MKV, MOV, WAV, FLAC, OGG) and convert them with customizable bitrate and quality settings. - Add conversion service with FFmpeg integration - Add /convert-to-mp3 and /supported-formats API endpoints - Add new "Video to MP3" tab with drag-and-drop UI - Support multiple file uploads with batch conversion - Add bitrate (128k-320k) and VBR quality options 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1130 lines
41 KiB
JavaScript
1130 lines
41 KiB
JavaScript
// API Base URL
|
||
const API_URL = '';
|
||
|
||
// Tab switching
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(tab.dataset.tab).classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Helper: Show result
|
||
function showResult(elementId, success, content) {
|
||
const el = document.getElementById(elementId);
|
||
el.className = `result show ${success ? 'success' : 'error'}`;
|
||
el.innerHTML = content;
|
||
}
|
||
|
||
// Helper: Set loading state
|
||
function setLoading(button, loading) {
|
||
button.disabled = loading;
|
||
button.classList.toggle('loading', loading);
|
||
}
|
||
|
||
// Format seconds to MM:SS or HH:MM:SS
|
||
function formatTime(seconds) {
|
||
if (!seconds || seconds < 0) return '--:--';
|
||
const hrs = Math.floor(seconds / 3600);
|
||
const mins = Math.floor((seconds % 3600) / 60);
|
||
const secs = Math.floor(seconds % 60);
|
||
if (hrs > 0) {
|
||
return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||
}
|
||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||
}
|
||
|
||
// Format file size
|
||
function formatSize(bytes) {
|
||
if (!bytes) return '';
|
||
const units = ['B', 'KB', 'MB', 'GB'];
|
||
let i = 0;
|
||
while (bytes >= 1024 && i < units.length - 1) {
|
||
bytes /= 1024;
|
||
i++;
|
||
}
|
||
return `${bytes.toFixed(1)} ${units[i]}`;
|
||
}
|
||
|
||
// ==================== DOWNLOAD TAB ====================
|
||
|
||
const progressContainer = document.getElementById('download-progress');
|
||
const progressFill = document.getElementById('progress-fill');
|
||
const progressPercent = document.getElementById('progress-percent');
|
||
const progressTitle = document.getElementById('progress-title');
|
||
const progressEta = document.getElementById('progress-eta');
|
||
const progressInfo = document.getElementById('progress-info');
|
||
const progressSpeed = document.getElementById('progress-speed');
|
||
const progressCurrent = document.getElementById('progress-current');
|
||
|
||
function updateDownloadProgress(data) {
|
||
progressFill.style.width = `${data.percent}%`;
|
||
progressPercent.textContent = `${data.percent}%`;
|
||
|
||
if (data.totalVideos > 1) {
|
||
progressInfo.textContent = `Video ${data.currentVideo}/${data.totalVideos}`;
|
||
} else {
|
||
progressInfo.textContent = '';
|
||
}
|
||
|
||
if (data.speed) progressSpeed.textContent = data.speed;
|
||
if (data.estimatedRemaining) {
|
||
progressEta.textContent = `ETA: ${formatTime(data.estimatedRemaining)}`;
|
||
} else if (data.eta) {
|
||
progressEta.textContent = `ETA: ${data.eta}`;
|
||
}
|
||
if (data.title) {
|
||
progressCurrent.innerHTML = `Downloading: <span class="video-title">${data.title}</span>`;
|
||
}
|
||
}
|
||
|
||
function resetDownloadProgress() {
|
||
progressFill.style.width = '0%';
|
||
progressPercent.textContent = '0%';
|
||
progressTitle.textContent = 'Downloading...';
|
||
progressEta.textContent = '';
|
||
progressInfo.textContent = '';
|
||
progressSpeed.textContent = '';
|
||
progressCurrent.textContent = '';
|
||
}
|
||
|
||
document.getElementById('download-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = e.target.querySelector('button[type="submit"]');
|
||
const url = document.getElementById('download-url').value;
|
||
const resultDiv = document.getElementById('download-result');
|
||
|
||
setLoading(button, true);
|
||
resetDownloadProgress();
|
||
progressContainer.style.display = 'block';
|
||
resultDiv.classList.remove('show');
|
||
|
||
const eventSource = new EventSource(`${API_URL}/download-stream?url=${encodeURIComponent(url)}`);
|
||
|
||
eventSource.addEventListener('status', (e) => {
|
||
progressTitle.textContent = JSON.parse(e.data).message;
|
||
});
|
||
|
||
eventSource.addEventListener('info', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
progressTitle.textContent = data.totalVideos > 1
|
||
? `Downloading playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
|
||
: `Downloading: ${data.title}`;
|
||
});
|
||
|
||
eventSource.addEventListener('progress', (e) => updateDownloadProgress(JSON.parse(e.data)));
|
||
|
||
eventSource.addEventListener('video-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
progressCurrent.innerHTML = `Completed: <span class="video-title">${data.title}</span> (${data.videosCompleted}/${data.totalVideos})`;
|
||
});
|
||
|
||
eventSource.addEventListener('complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
eventSource.close();
|
||
progressFill.style.width = '100%';
|
||
progressPercent.textContent = '100%';
|
||
progressTitle.textContent = 'Download Complete!';
|
||
progressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
|
||
|
||
showResult('download-result', true, `
|
||
<h3>Download Complete!</h3>
|
||
<p>${data.successCount}/${data.totalVideos} videos downloaded</p>
|
||
${data.playlistTitle ? `<p>Playlist: ${data.playlistTitle}</p>` : ''}
|
||
<ul>${data.videos.map(v => `
|
||
<li>
|
||
<span class="${v.success ? 'icon-success' : 'icon-error'}">${v.success ? '✓' : '✗'}</span>
|
||
${v.title}
|
||
${v.success && v.fileUrl ? `<a href="${v.fileUrl}" target="_blank">Download</a>` : ''}
|
||
${v.error ? `<small>(${v.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.addEventListener('error', (e) => {
|
||
let errorMsg = 'Download failed';
|
||
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
|
||
eventSource.close();
|
||
progressContainer.style.display = 'none';
|
||
showResult('download-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.onerror = () => {
|
||
eventSource.close();
|
||
progressContainer.style.display = 'none';
|
||
showResult('download-result', false, `<h3>Error</h3><p>Connection lost</p>`);
|
||
setLoading(button, false);
|
||
};
|
||
});
|
||
|
||
// ==================== CONVERT TAB (Video to MP3) ====================
|
||
|
||
let convertSelectedFiles = [];
|
||
const convertDropZone = document.getElementById('convert-drop-zone');
|
||
const convertFileInput = document.getElementById('convert-file-input');
|
||
const convertSelectedFilesDiv = document.getElementById('convert-selected-files');
|
||
const convertFilesList = document.getElementById('convert-files-list');
|
||
const convertBtn = document.getElementById('convert-btn');
|
||
const convertClearFilesBtn = document.getElementById('convert-clear-files');
|
||
|
||
function updateConvertFilesList() {
|
||
if (convertSelectedFiles.length === 0) {
|
||
convertSelectedFilesDiv.style.display = 'none';
|
||
convertBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
convertSelectedFilesDiv.style.display = 'block';
|
||
convertBtn.disabled = false;
|
||
|
||
convertFilesList.innerHTML = convertSelectedFiles.map((file, index) => `
|
||
<li>
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatSize(file.size)}</span>
|
||
<button type="button" class="remove-file" data-index="${index}">×</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
// Add remove handlers
|
||
convertFilesList.querySelectorAll('.remove-file').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
convertSelectedFiles.splice(parseInt(btn.dataset.index), 1);
|
||
updateConvertFilesList();
|
||
});
|
||
});
|
||
}
|
||
|
||
function addConvertFiles(files) {
|
||
const videoAudioFiles = Array.from(files).filter(f =>
|
||
f.type.startsWith('video/') || f.type.startsWith('audio/') ||
|
||
f.name.match(/\.(mp4|avi|mkv|mov|m4a|wav|flac|ogg|webm|wmv|flv)$/i)
|
||
);
|
||
convertSelectedFiles = [...convertSelectedFiles, ...videoAudioFiles];
|
||
updateConvertFilesList();
|
||
}
|
||
|
||
// Drag & Drop events for convert
|
||
convertDropZone.addEventListener('click', () => convertFileInput.click());
|
||
|
||
convertDropZone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
convertDropZone.classList.add('drag-over');
|
||
});
|
||
|
||
convertDropZone.addEventListener('dragleave', () => {
|
||
convertDropZone.classList.remove('drag-over');
|
||
});
|
||
|
||
convertDropZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
convertDropZone.classList.remove('drag-over');
|
||
addConvertFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
convertFileInput.addEventListener('change', () => {
|
||
addConvertFiles(convertFileInput.files);
|
||
convertFileInput.value = '';
|
||
});
|
||
|
||
convertClearFilesBtn.addEventListener('click', () => {
|
||
convertSelectedFiles = [];
|
||
updateConvertFilesList();
|
||
});
|
||
|
||
// Convert form submit
|
||
document.getElementById('convert-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (convertSelectedFiles.length === 0) return;
|
||
|
||
const button = convertBtn;
|
||
const bitrate = document.getElementById('convert-bitrate').value;
|
||
const quality = document.getElementById('convert-quality').value;
|
||
|
||
const convertProgress = document.getElementById('convert-progress');
|
||
const convertProgressFill = document.getElementById('convert-progress-fill');
|
||
const convertProgressTitle = document.getElementById('convert-progress-title');
|
||
const convertProgressPercent = document.getElementById('convert-progress-percent');
|
||
const convertProgressInfo = document.getElementById('convert-progress-info');
|
||
const convertProgressCurrent = document.getElementById('convert-progress-current');
|
||
|
||
setLoading(button, true);
|
||
convertProgress.style.display = 'block';
|
||
convertProgressFill.style.width = '0%';
|
||
convertProgressTitle.textContent = 'Converting to MP3...';
|
||
convertProgressPercent.textContent = '0%';
|
||
convertProgressInfo.textContent = `0/${convertSelectedFiles.length} files`;
|
||
document.getElementById('convert-result').classList.remove('show');
|
||
|
||
const formData = new FormData();
|
||
convertSelectedFiles.forEach(file => formData.append('files', file));
|
||
formData.append('bitrate', bitrate);
|
||
formData.append('quality', quality);
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/convert-to-mp3`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) throw new Error(data.error || 'Conversion failed');
|
||
|
||
convertProgressFill.style.width = '100%';
|
||
convertProgressPercent.textContent = '100%';
|
||
convertProgressTitle.textContent = 'Conversion Complete!';
|
||
convertProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
|
||
|
||
showResult('convert-result', true, `
|
||
<h3>Conversion Complete!</h3>
|
||
<p>${data.successCount}/${data.totalFiles} files converted to MP3</p>
|
||
<div class="file-results">
|
||
${data.results.map(r => r.success ? `
|
||
<div class="file-result success">
|
||
<div class="file-info">
|
||
<strong>${r.fileName}</strong>
|
||
<span class="file-size">${r.size}</span>
|
||
</div>
|
||
<a href="${r.outputUrl}" download class="btn btn-small btn-primary">Download MP3</a>
|
||
</div>
|
||
` : `
|
||
<div class="file-result error">
|
||
<strong>${r.fileName}</strong>
|
||
<span class="error-msg">${r.error}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`);
|
||
|
||
// Clear selected files after successful conversion
|
||
convertSelectedFiles = [];
|
||
updateConvertFilesList();
|
||
|
||
} catch (error) {
|
||
showResult('convert-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
setTimeout(() => {
|
||
convertProgress.style.display = 'none';
|
||
}, 1000);
|
||
}
|
||
});
|
||
|
||
// ==================== TRANSCRIBE TAB (Drag & Drop) ====================
|
||
|
||
let selectedFiles = [];
|
||
const dropZone = document.getElementById('drop-zone');
|
||
const fileInput = document.getElementById('file-input');
|
||
const selectedFilesDiv = document.getElementById('selected-files');
|
||
const filesList = document.getElementById('files-list');
|
||
const transcribeBtn = document.getElementById('transcribe-btn');
|
||
const clearFilesBtn = document.getElementById('clear-files');
|
||
|
||
function updateFilesList() {
|
||
if (selectedFiles.length === 0) {
|
||
selectedFilesDiv.style.display = 'none';
|
||
transcribeBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
selectedFilesDiv.style.display = 'block';
|
||
transcribeBtn.disabled = false;
|
||
|
||
filesList.innerHTML = selectedFiles.map((file, index) => `
|
||
<li>
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatSize(file.size)}</span>
|
||
<button type="button" class="remove-file" data-index="${index}">×</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
// Add remove handlers
|
||
filesList.querySelectorAll('.remove-file').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
selectedFiles.splice(parseInt(btn.dataset.index), 1);
|
||
updateFilesList();
|
||
});
|
||
});
|
||
}
|
||
|
||
function addFiles(files) {
|
||
const audioFiles = Array.from(files).filter(f =>
|
||
f.type.startsWith('audio/') || f.name.match(/\.(mp3|wav|m4a|ogg|flac)$/i)
|
||
);
|
||
selectedFiles = [...selectedFiles, ...audioFiles];
|
||
updateFilesList();
|
||
}
|
||
|
||
// Drag & Drop events
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
|
||
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');
|
||
addFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
fileInput.addEventListener('change', () => {
|
||
addFiles(fileInput.files);
|
||
fileInput.value = '';
|
||
});
|
||
|
||
clearFilesBtn.addEventListener('click', () => {
|
||
selectedFiles = [];
|
||
updateFilesList();
|
||
});
|
||
|
||
// Transcribe form submit
|
||
document.getElementById('transcribe-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (selectedFiles.length === 0) return;
|
||
|
||
const button = transcribeBtn;
|
||
const language = document.getElementById('transcribe-lang').value;
|
||
const model = document.getElementById('transcribe-model').value;
|
||
|
||
const transcribeProgress = document.getElementById('transcribe-progress');
|
||
const transcribeProgressFill = document.getElementById('transcribe-progress-fill');
|
||
const transcribeProgressTitle = document.getElementById('transcribe-progress-title');
|
||
const transcribeProgressPercent = document.getElementById('transcribe-progress-percent');
|
||
const transcribeProgressInfo = document.getElementById('transcribe-progress-info');
|
||
const transcribeProgressCurrent = document.getElementById('transcribe-progress-current');
|
||
|
||
setLoading(button, true);
|
||
transcribeProgress.style.display = 'block';
|
||
transcribeProgressFill.style.width = '0%';
|
||
transcribeProgressTitle.textContent = 'Uploading and transcribing...';
|
||
transcribeProgressPercent.textContent = '0%';
|
||
transcribeProgressInfo.textContent = `0/${selectedFiles.length} files`;
|
||
document.getElementById('transcribe-result').classList.remove('show');
|
||
|
||
const formData = new FormData();
|
||
selectedFiles.forEach(file => formData.append('files', file));
|
||
if (language) formData.append('language', language);
|
||
formData.append('model', model);
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/upload-transcribe`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) throw new Error(data.error || 'Transcription failed');
|
||
|
||
transcribeProgressFill.style.width = '100%';
|
||
transcribeProgressPercent.textContent = '100%';
|
||
transcribeProgressTitle.textContent = 'Transcription Complete!';
|
||
transcribeProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
|
||
|
||
showResult('transcribe-result', true, `
|
||
<h3>Transcription Complete!</h3>
|
||
<p>${data.successCount}/${data.totalFiles} files transcribed</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.success ? 'icon-success' : 'icon-error'}">${r.success ? '✓' : '✗'}</span>
|
||
${r.fileName}
|
||
${r.success && r.transcriptionUrl ? `<a href="${r.transcriptionUrl}" target="_blank">View</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.text ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="preview">${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
|
||
selectedFiles = [];
|
||
updateFilesList();
|
||
|
||
} catch (error) {
|
||
transcribeProgress.style.display = 'none';
|
||
showResult('transcribe-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// ==================== PROCESS TAB (Download + Transcribe) ====================
|
||
|
||
const processProgress = document.getElementById('process-progress');
|
||
const processProgressFill = document.getElementById('process-progress-fill');
|
||
const processProgressTitle = document.getElementById('process-progress-title');
|
||
const processProgressPercent = document.getElementById('process-progress-percent');
|
||
const processProgressPhase = document.getElementById('process-progress-phase');
|
||
const processProgressSpeed = document.getElementById('process-progress-speed');
|
||
const processProgressCurrent = document.getElementById('process-progress-current');
|
||
const processProgressEta = document.getElementById('process-progress-eta');
|
||
|
||
function resetProcessProgress() {
|
||
processProgressFill.style.width = '0%';
|
||
processProgressPercent.textContent = '0%';
|
||
processProgressTitle.textContent = 'Processing...';
|
||
processProgressPhase.textContent = '';
|
||
processProgressSpeed.textContent = '';
|
||
processProgressCurrent.textContent = '';
|
||
processProgressEta.textContent = '';
|
||
}
|
||
|
||
document.getElementById('process-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = e.target.querySelector('button[type="submit"]');
|
||
const url = document.getElementById('process-url').value;
|
||
const language = document.getElementById('process-lang').value;
|
||
const model = document.getElementById('process-model').value;
|
||
const resultDiv = document.getElementById('process-result');
|
||
|
||
setLoading(button, true);
|
||
resetProcessProgress();
|
||
processProgress.style.display = 'block';
|
||
resultDiv.classList.remove('show');
|
||
|
||
const params = new URLSearchParams({ url });
|
||
if (language) params.append('language', language);
|
||
params.append('model', model);
|
||
|
||
const eventSource = new EventSource(`${API_URL}/process-stream?${params}`);
|
||
|
||
eventSource.addEventListener('status', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressTitle.textContent = data.message;
|
||
if (data.phase === 'transcribing') {
|
||
processProgressPhase.textContent = 'Transcribing';
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('info', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressTitle.textContent = data.totalVideos > 1
|
||
? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
|
||
: `Processing: ${data.title}`;
|
||
});
|
||
|
||
eventSource.addEventListener('progress', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressFill.style.width = `${data.percent}%`;
|
||
processProgressPercent.textContent = `${Math.round(data.percent)}%`;
|
||
processProgressPhase.textContent = data.phaseLabel || '';
|
||
if (data.speed) processProgressSpeed.textContent = data.speed;
|
||
if (data.title) {
|
||
processProgressCurrent.innerHTML = `${data.phaseLabel}: <span class="video-title">${data.title}</span>`;
|
||
}
|
||
if (data.totalVideos > 1) {
|
||
processProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('video-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressCurrent.innerHTML = `Downloaded: <span class="video-title">${data.title}</span>`;
|
||
});
|
||
|
||
eventSource.addEventListener('transcribe-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
processProgressCurrent.innerHTML = `Transcribed: <span class="video-title">${data.title}</span> (${data.videosCompleted}/${data.totalFiles})`;
|
||
});
|
||
|
||
eventSource.addEventListener('complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
eventSource.close();
|
||
processProgressFill.style.width = '100%';
|
||
processProgressPercent.textContent = '100%';
|
||
processProgressTitle.textContent = 'Processing Complete!';
|
||
processProgressPhase.textContent = '';
|
||
processProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
|
||
|
||
showResult('process-result', true, `
|
||
<h3>Processing Complete!</h3>
|
||
${data.playlistTitle ? `<p>Playlist: ${data.playlistTitle}</p>` : ''}
|
||
<p>Downloaded: ${data.downloadedCount}/${data.totalVideos}</p>
|
||
<p>Transcribed: ${data.transcribedCount}/${data.totalVideos}</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.transcriptionSuccess ? 'icon-success' : 'icon-error'}">${r.transcriptionSuccess ? '✓' : '✗'}</span>
|
||
${r.title}
|
||
${r.audioUrl ? `<a href="${r.audioUrl}" target="_blank">MP3</a>` : ''}
|
||
${r.transcriptionUrl ? `<a href="${r.transcriptionUrl}" target="_blank">TXT</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.text ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="preview">${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.addEventListener('error', (e) => {
|
||
let errorMsg = 'Processing failed';
|
||
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
|
||
eventSource.close();
|
||
processProgress.style.display = 'none';
|
||
showResult('process-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.onerror = () => {
|
||
eventSource.close();
|
||
processProgress.style.display = 'none';
|
||
showResult('process-result', false, `<h3>Error</h3><p>Connection lost</p>`);
|
||
setLoading(button, false);
|
||
};
|
||
});
|
||
|
||
// ==================== TRANSLATE CHECKBOXES (Transcribe & Process tabs) ====================
|
||
|
||
// Transcribe tab checkbox
|
||
const transcribeTranslateCheckbox = document.getElementById('transcribe-translate');
|
||
const transcribeTranslateLang = document.getElementById('transcribe-translate-lang');
|
||
|
||
transcribeTranslateCheckbox.addEventListener('change', () => {
|
||
transcribeTranslateLang.disabled = !transcribeTranslateCheckbox.checked;
|
||
});
|
||
|
||
// Process tab checkbox
|
||
const processTranslateCheckbox = document.getElementById('process-translate');
|
||
const processTranslateLang = document.getElementById('process-translate-lang');
|
||
|
||
processTranslateCheckbox.addEventListener('change', () => {
|
||
processTranslateLang.disabled = !processTranslateCheckbox.checked;
|
||
});
|
||
|
||
// ==================== TRANSLATE TAB ====================
|
||
|
||
// Mode switching
|
||
const translateModeBtns = document.querySelectorAll('.mode-btn');
|
||
const translateTextMode = document.getElementById('translate-text-mode');
|
||
const translateFileMode = document.getElementById('translate-file-mode');
|
||
|
||
translateModeBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
translateModeBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
if (btn.dataset.mode === 'text') {
|
||
translateTextMode.style.display = 'block';
|
||
translateFileMode.style.display = 'none';
|
||
} else {
|
||
translateTextMode.style.display = 'none';
|
||
translateFileMode.style.display = 'block';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Text translation form
|
||
document.getElementById('translate-text-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = document.getElementById('translate-text-btn');
|
||
const text = document.getElementById('translate-input').value;
|
||
const sourceLang = document.getElementById('translate-source').value;
|
||
const targetLang = document.getElementById('translate-target').value;
|
||
|
||
if (!text.trim()) {
|
||
showResult('translate-text-result', false, '<h3>Error</h3><p>Please enter text to translate</p>');
|
||
return;
|
||
}
|
||
|
||
setLoading(button, true);
|
||
document.getElementById('translate-text-result').classList.remove('show');
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/translate`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text, targetLang, sourceLang: sourceLang || null })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) throw new Error(data.error || 'Translation failed');
|
||
|
||
showResult('translate-text-result', true, `
|
||
<h3>Translation Complete!</h3>
|
||
<p><strong>From:</strong> ${data.sourceLanguage} <strong>To:</strong> ${data.targetLanguage}</p>
|
||
<div class="translation-output">${data.translatedText}</div>
|
||
`);
|
||
} catch (error) {
|
||
showResult('translate-text-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// File translation - Drag & Drop
|
||
let translateSelectedFiles = [];
|
||
const translateDropZone = document.getElementById('translate-drop-zone');
|
||
const translateFileInput = document.getElementById('translate-file-input');
|
||
const translateSelectedFilesDiv = document.getElementById('translate-selected-files');
|
||
const translateFilesList = document.getElementById('translate-files-list');
|
||
const translateFileBtn = document.getElementById('translate-file-btn');
|
||
const translateClearFilesBtn = document.getElementById('translate-clear-files');
|
||
|
||
function updateTranslateFilesList() {
|
||
if (translateSelectedFiles.length === 0) {
|
||
translateSelectedFilesDiv.style.display = 'none';
|
||
translateFileBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
translateSelectedFilesDiv.style.display = 'block';
|
||
translateFileBtn.disabled = false;
|
||
|
||
translateFilesList.innerHTML = translateSelectedFiles.map((file, index) => `
|
||
<li>
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatSize(file.size)}</span>
|
||
<button type="button" class="remove-file" data-index="${index}">x</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
translateFilesList.querySelectorAll('.remove-file').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
translateSelectedFiles.splice(parseInt(btn.dataset.index), 1);
|
||
updateTranslateFilesList();
|
||
});
|
||
});
|
||
}
|
||
|
||
function addTranslateFiles(files) {
|
||
const textFiles = Array.from(files).filter(f =>
|
||
f.type === 'text/plain' || f.name.endsWith('.txt')
|
||
);
|
||
translateSelectedFiles = [...translateSelectedFiles, ...textFiles];
|
||
updateTranslateFilesList();
|
||
}
|
||
|
||
translateDropZone.addEventListener('click', () => translateFileInput.click());
|
||
|
||
translateDropZone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
translateDropZone.classList.add('drag-over');
|
||
});
|
||
|
||
translateDropZone.addEventListener('dragleave', () => {
|
||
translateDropZone.classList.remove('drag-over');
|
||
});
|
||
|
||
translateDropZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
translateDropZone.classList.remove('drag-over');
|
||
addTranslateFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
translateFileInput.addEventListener('change', () => {
|
||
addTranslateFiles(translateFileInput.files);
|
||
translateFileInput.value = '';
|
||
});
|
||
|
||
translateClearFilesBtn.addEventListener('click', () => {
|
||
translateSelectedFiles = [];
|
||
updateTranslateFilesList();
|
||
});
|
||
|
||
// File translation form submit
|
||
document.getElementById('translate-file-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (translateSelectedFiles.length === 0) return;
|
||
|
||
const button = translateFileBtn;
|
||
const sourceLang = document.getElementById('translate-file-source').value;
|
||
const targetLang = document.getElementById('translate-file-target').value;
|
||
|
||
setLoading(button, true);
|
||
document.getElementById('translate-file-result').classList.remove('show');
|
||
|
||
const formData = new FormData();
|
||
translateSelectedFiles.forEach(file => formData.append('files', file));
|
||
formData.append('targetLang', targetLang);
|
||
if (sourceLang) formData.append('sourceLang', sourceLang);
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/translate-file`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) throw new Error(data.error || 'Translation failed');
|
||
|
||
showResult('translate-file-result', true, `
|
||
<h3>Translation Complete!</h3>
|
||
<p>${data.successCount}/${data.totalFiles} files translated</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.success ? 'icon-success' : 'icon-error'}">${r.success ? '✓' : '✗'}</span>
|
||
${r.fileName || r.originalPath}
|
||
${r.success && r.translationUrl ? `<a href="${r.translationUrl}" target="_blank">View</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.translatedText ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="translation-output">${data.results[0].translatedText.substring(0, 1000)}${data.results[0].translatedText.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
|
||
translateSelectedFiles = [];
|
||
updateTranslateFilesList();
|
||
|
||
} catch (error) {
|
||
showResult('translate-file-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// ==================== SUMMARIZE TAB ====================
|
||
|
||
// Mode switching
|
||
const summarizeModeBtns = document.querySelectorAll('.summarize-mode-selector .mode-btn');
|
||
const summarizeTextMode = document.getElementById('summarize-text-mode');
|
||
const summarizeFileMode = document.getElementById('summarize-file-mode');
|
||
|
||
const summarizeLinkMode = document.getElementById('summarize-link-mode');
|
||
|
||
summarizeModeBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
summarizeModeBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
summarizeTextMode.style.display = 'none';
|
||
summarizeFileMode.style.display = 'none';
|
||
if (summarizeLinkMode) summarizeLinkMode.style.display = 'none';
|
||
|
||
if (btn.dataset.mode === 'text') {
|
||
summarizeTextMode.style.display = 'block';
|
||
} else if (btn.dataset.mode === 'file') {
|
||
summarizeFileMode.style.display = 'block';
|
||
} else if (btn.dataset.mode === 'link' && summarizeLinkMode) {
|
||
summarizeLinkMode.style.display = 'block';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Text summarization form
|
||
document.getElementById('summarize-text-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = document.getElementById('summarize-text-btn');
|
||
const text = document.getElementById('summarize-input').value;
|
||
const style = document.getElementById('summarize-style').value;
|
||
const language = document.getElementById('summarize-language').value;
|
||
|
||
if (!text.trim()) {
|
||
showResult('summarize-text-result', false, '<h3>Error</h3><p>Please enter text to summarize</p>');
|
||
return;
|
||
}
|
||
|
||
setLoading(button, true);
|
||
document.getElementById('summarize-text-result').classList.remove('show');
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/summarize`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text, style, language })
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) throw new Error(data.error || 'Summarization failed');
|
||
|
||
showResult('summarize-text-result', true, `
|
||
<h3>Summary Complete!</h3>
|
||
<p><strong>Model:</strong> ${data.model} | <strong>Style:</strong> ${data.style} | <strong>Chunks:</strong> ${data.chunks}</p>
|
||
<div class="summary-output">${data.summary}</div>
|
||
`);
|
||
} catch (error) {
|
||
showResult('summarize-text-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// File summarization - Drag & Drop
|
||
let summarizeSelectedFiles = [];
|
||
const summarizeDropZone = document.getElementById('summarize-drop-zone');
|
||
const summarizeFileInput = document.getElementById('summarize-file-input');
|
||
const summarizeSelectedFilesDiv = document.getElementById('summarize-selected-files');
|
||
const summarizeFilesList = document.getElementById('summarize-files-list');
|
||
const summarizeFileBtn = document.getElementById('summarize-file-btn');
|
||
const summarizeClearFilesBtn = document.getElementById('summarize-clear-files');
|
||
|
||
function updateSummarizeFilesList() {
|
||
if (!summarizeSelectedFilesDiv || !summarizeFileBtn || !summarizeFilesList) return;
|
||
|
||
if (summarizeSelectedFiles.length === 0) {
|
||
summarizeSelectedFilesDiv.style.display = 'none';
|
||
summarizeFileBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
summarizeSelectedFilesDiv.style.display = 'block';
|
||
summarizeFileBtn.disabled = false;
|
||
|
||
summarizeFilesList.innerHTML = summarizeSelectedFiles.map((file, index) => `
|
||
<li>
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatSize(file.size)}</span>
|
||
<button type="button" class="remove-file" data-index="${index}">x</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
summarizeFilesList.querySelectorAll('.remove-file').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
summarizeSelectedFiles.splice(parseInt(btn.dataset.index), 1);
|
||
updateSummarizeFilesList();
|
||
});
|
||
});
|
||
}
|
||
|
||
function addSummarizeFiles(files) {
|
||
console.log('Adding files:', files);
|
||
const textFiles = Array.from(files).filter(f =>
|
||
f.type === 'text/plain' || f.name.endsWith('.txt')
|
||
);
|
||
console.log('Filtered text files:', textFiles);
|
||
summarizeSelectedFiles = [...summarizeSelectedFiles, ...textFiles];
|
||
updateSummarizeFilesList();
|
||
}
|
||
|
||
if (summarizeDropZone) {
|
||
summarizeDropZone.addEventListener('click', () => {
|
||
console.log('Drop zone clicked');
|
||
summarizeFileInput.click();
|
||
});
|
||
|
||
summarizeDropZone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
summarizeDropZone.classList.add('drag-over');
|
||
});
|
||
|
||
summarizeDropZone.addEventListener('dragleave', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
summarizeDropZone.classList.remove('drag-over');
|
||
});
|
||
|
||
summarizeDropZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
console.log('Files dropped:', e.dataTransfer.files);
|
||
summarizeDropZone.classList.remove('drag-over');
|
||
addSummarizeFiles(e.dataTransfer.files);
|
||
});
|
||
}
|
||
|
||
if (summarizeFileInput) {
|
||
summarizeFileInput.addEventListener('change', () => {
|
||
console.log('File input changed:', summarizeFileInput.files);
|
||
addSummarizeFiles(summarizeFileInput.files);
|
||
summarizeFileInput.value = '';
|
||
});
|
||
}
|
||
|
||
if (summarizeClearFilesBtn) {
|
||
summarizeClearFilesBtn.addEventListener('click', () => {
|
||
summarizeSelectedFiles = [];
|
||
updateSummarizeFilesList();
|
||
});
|
||
}
|
||
|
||
// File summarization form submit
|
||
document.getElementById('summarize-file-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
if (summarizeSelectedFiles.length === 0) return;
|
||
|
||
const button = summarizeFileBtn;
|
||
const style = document.getElementById('summarize-file-style').value;
|
||
const language = document.getElementById('summarize-file-language').value;
|
||
|
||
setLoading(button, true);
|
||
document.getElementById('summarize-file-result').classList.remove('show');
|
||
|
||
const formData = new FormData();
|
||
summarizeSelectedFiles.forEach(file => formData.append('files', file));
|
||
formData.append('style', style);
|
||
formData.append('language', language);
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/summarize-file`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) throw new Error(data.error || 'Summarization failed');
|
||
|
||
showResult('summarize-file-result', true, `
|
||
<h3>Summarization Complete!</h3>
|
||
<p>${data.successCount}/${data.totalFiles} files summarized</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.success ? 'icon-success' : 'icon-error'}">${r.success ? '✓' : '✗'}</span>
|
||
${r.fileName || r.filePath}
|
||
${r.success && r.summaryUrl ? `<a href="${r.summaryUrl}" target="_blank">View</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.summary ? `
|
||
<h4>Preview (first file):</h4>
|
||
<div class="summary-output">${data.results[0].summary.substring(0, 1000)}${data.results[0].summary.length > 1000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
|
||
summarizeSelectedFiles = [];
|
||
updateSummarizeFilesList();
|
||
|
||
} catch (error) {
|
||
showResult('summarize-file-result', false, `<h3>Error</h3><p>${error.message}</p>`);
|
||
} finally {
|
||
setLoading(button, false);
|
||
}
|
||
});
|
||
|
||
// ==================== SUMMARIZE LINK MODE (Full Pipeline) ====================
|
||
|
||
const summarizeLinkForm = document.getElementById('summarize-link-form');
|
||
if (summarizeLinkForm) {
|
||
const linkProgress = document.getElementById('summarize-link-progress');
|
||
const linkProgressFill = document.getElementById('summarize-link-progress-fill');
|
||
const linkProgressTitle = document.getElementById('summarize-link-progress-title');
|
||
const linkProgressPercent = document.getElementById('summarize-link-progress-percent');
|
||
const linkProgressPhase = document.getElementById('summarize-link-progress-phase');
|
||
const linkProgressSpeed = document.getElementById('summarize-link-progress-speed');
|
||
const linkProgressCurrent = document.getElementById('summarize-link-progress-current');
|
||
const linkProgressEta = document.getElementById('summarize-link-progress-eta');
|
||
|
||
function resetLinkProgress() {
|
||
if (linkProgressFill) linkProgressFill.style.width = '0%';
|
||
if (linkProgressPercent) linkProgressPercent.textContent = '0%';
|
||
if (linkProgressTitle) linkProgressTitle.textContent = 'Processing...';
|
||
if (linkProgressPhase) linkProgressPhase.textContent = '';
|
||
if (linkProgressSpeed) linkProgressSpeed.textContent = '';
|
||
if (linkProgressCurrent) linkProgressCurrent.textContent = '';
|
||
if (linkProgressEta) linkProgressEta.textContent = '';
|
||
}
|
||
|
||
summarizeLinkForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const button = document.getElementById('summarize-link-btn');
|
||
const url = document.getElementById('summarize-url').value;
|
||
const style = document.getElementById('summarize-link-style').value;
|
||
const language = document.getElementById('summarize-link-language').value;
|
||
const resultDiv = document.getElementById('summarize-link-result');
|
||
|
||
setLoading(button, true);
|
||
resetLinkProgress();
|
||
linkProgress.style.display = 'block';
|
||
resultDiv.classList.remove('show');
|
||
|
||
const params = new URLSearchParams({ url, style, language });
|
||
const eventSource = new EventSource(`${API_URL}/summarize-stream?${params}`);
|
||
|
||
eventSource.addEventListener('status', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
linkProgressTitle.textContent = data.message;
|
||
if (data.percent !== undefined) {
|
||
linkProgressFill.style.width = `${data.percent}%`;
|
||
linkProgressPercent.textContent = `${Math.round(data.percent)}%`;
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('info', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
linkProgressTitle.textContent = data.totalVideos > 1
|
||
? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
|
||
: `Processing: ${data.title}`;
|
||
});
|
||
|
||
eventSource.addEventListener('progress', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
linkProgressFill.style.width = `${data.percent}%`;
|
||
linkProgressPercent.textContent = `${Math.round(data.percent)}%`;
|
||
linkProgressPhase.textContent = data.phaseLabel || '';
|
||
if (data.speed) linkProgressSpeed.textContent = data.speed;
|
||
if (data.title) {
|
||
linkProgressCurrent.innerHTML = `${data.phaseLabel}: <span class="video-title">${data.title}</span>`;
|
||
}
|
||
if (data.totalVideos > 1) {
|
||
linkProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
|
||
}
|
||
});
|
||
|
||
eventSource.addEventListener('video-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
linkProgressCurrent.innerHTML = `Downloaded: <span class="video-title">${data.title}</span>`;
|
||
});
|
||
|
||
eventSource.addEventListener('transcribe-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
linkProgressCurrent.innerHTML = `Transcribed: <span class="video-title">${data.title}</span>`;
|
||
});
|
||
|
||
eventSource.addEventListener('summarize-complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
linkProgressCurrent.innerHTML = `Summarized: <span class="video-title">${data.title}</span>`;
|
||
});
|
||
|
||
eventSource.addEventListener('complete', (e) => {
|
||
const data = JSON.parse(e.data);
|
||
eventSource.close();
|
||
linkProgressFill.style.width = '100%';
|
||
linkProgressPercent.textContent = '100%';
|
||
linkProgressTitle.textContent = 'Complete!';
|
||
linkProgressPhase.textContent = '';
|
||
linkProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
|
||
|
||
showResult('summarize-link-result', true, `
|
||
<h3>Pipeline Complete!</h3>
|
||
${data.playlistTitle ? `<p>Playlist: ${data.playlistTitle}</p>` : ''}
|
||
<p>Downloaded: ${data.downloadedCount}/${data.totalVideos} | Transcribed: ${data.transcribedCount} | Summarized: ${data.summarizedCount}</p>
|
||
<ul>${data.results.map(r => `
|
||
<li>
|
||
<span class="${r.success ? 'icon-success' : 'icon-error'}">${r.success ? '✓' : '✗'}</span>
|
||
<strong>${r.title}</strong>
|
||
${r.summaryUrl ? `<a href="${r.summaryUrl}" target="_blank">Summary</a>` : ''}
|
||
${r.transcriptionUrl ? `<a href="${r.transcriptionUrl}" target="_blank">Transcript</a>` : ''}
|
||
${r.error ? `<small>(${r.error})</small>` : ''}
|
||
</li>
|
||
`).join('')}</ul>
|
||
${data.results[0]?.summary ? `
|
||
<h4>Summary Preview:</h4>
|
||
<div class="summary-output">${data.results[0].summary.substring(0, 2000)}${data.results[0].summary.length > 2000 ? '...' : ''}</div>
|
||
` : ''}
|
||
`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.addEventListener('error', (e) => {
|
||
let errorMsg = 'Processing failed';
|
||
try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
|
||
eventSource.close();
|
||
linkProgress.style.display = 'none';
|
||
showResult('summarize-link-result', false, `<h3>Error</h3><p>${errorMsg}</p>`);
|
||
setLoading(button, false);
|
||
});
|
||
|
||
eventSource.onerror = () => {
|
||
eventSource.close();
|
||
linkProgress.style.display = 'none';
|
||
showResult('summarize-link-result', false, `<h3>Error</h3><p>Connection lost</p>`);
|
||
setLoading(button, false);
|
||
};
|
||
});
|
||
}
|