// ==================== API CONFIGURATION ====================
const API_URL = '';
const TOKEN_STORAGE_KEY = 'video_transcriptor_api_token';
// Token management
let apiToken = localStorage.getItem(TOKEN_STORAGE_KEY) || '';
// Helper: Get API headers with token
function getHeaders(additionalHeaders = {}) {
const headers = { ...additionalHeaders };
if (apiToken) {
headers['X-API-Key'] = apiToken;
}
return headers;
}
// Helper: Authenticated fetch
async function apiFetch(url, options = {}) {
const defaultHeaders = getHeaders(options.headers || {});
const response = await fetch(url, {
...options,
headers: defaultHeaders,
});
// Check for auth errors
if (response.status === 401 || response.status === 403) {
showResult('api-error', false, `
⚠️ Authentication Error
${response.status === 401 ? 'API token required' : 'Invalid API token'}
Please configure your API token in the configuration panel above.
`);
throw new Error('Authentication failed');
}
return response;
}
// Helper: Format YouTube enhanced errors
function formatYouTubeError(errorData) {
// Check if it's an enhanced YouTube bot detection error
if (errorData.error === 'YouTube Bot Detection' || errorData.solution) {
return `
🚫 ${errorData.error || 'YouTube Error'}
${errorData.message}
${errorData.reason ? `
Technical Details
${errorData.reason}
` : ''}
💡 Solution
${errorData.solution.quick}
${errorData.solution.steps.map(step => `- ${step}
`).join('')}
${errorData.solution.alternative ? `
Alternative: ${errorData.solution.alternative}
` : ''}
${errorData.solution.documentation ? `
📚 ${errorData.solution.documentation}
` : ''}
${errorData.currentConfig ? `
Current Configuration
- Cookies File: ${errorData.currentConfig.cookiesFile}
- Browser Extraction: ${errorData.currentConfig.cookiesBrowser}
- Status: ${errorData.currentConfig.status}
` : ''}
`;
}
// Generic YouTube error
if (errorData.error && errorData.error.includes('YouTube')) {
return `
❌ ${errorData.error}
${errorData.message}
${errorData.solution ? `${errorData.solution}
` : ''}
`;
}
// Default error
return `Error
${errorData.message || errorData.error || 'Unknown error'}
`;
}
// API Configuration Panel
const configPanel = document.getElementById('api-config-panel');
const configHeader = document.querySelector('.config-header');
const configContent = document.getElementById('config-content');
const toggleConfigBtn = document.getElementById('toggle-config');
const apiTokenInput = document.getElementById('api-token');
const toggleVisibilityBtn = document.getElementById('toggle-token-visibility');
const apiConfigForm = document.getElementById('api-config-form');
const clearTokenBtn = document.getElementById('clear-token');
const configStatus = document.getElementById('config-status');
// Load token on page load
if (apiToken) {
apiTokenInput.value = apiToken;
updateConnectionStatus(true);
}
// Toggle configuration panel
function toggleConfigPanel() {
const isExpanded = configContent.style.display !== 'none';
configContent.style.display = isExpanded ? 'none' : 'block';
configHeader.classList.toggle('expanded', !isExpanded);
}
configHeader.addEventListener('click', (e) => {
if (e.target.closest('.btn-toggle') || e.target === configHeader) {
toggleConfigPanel();
}
});
// Toggle token visibility
toggleVisibilityBtn.addEventListener('click', () => {
const isPassword = apiTokenInput.type === 'password';
apiTokenInput.type = isPassword ? 'text' : 'password';
const eyeIcon = document.getElementById('eye-icon');
if (isPassword) {
eyeIcon.innerHTML = `
`;
} else {
eyeIcon.innerHTML = `
`;
}
});
// Update connection status UI
function updateConnectionStatus(connected, message = '') {
const indicator = configStatus.querySelector('.status-indicator');
const text = configStatus.querySelector('.status-text');
if (connected) {
indicator.className = 'status-indicator status-connected';
text.textContent = message || 'Connected ✓';
} else {
indicator.className = 'status-indicator status-disconnected';
text.textContent = message || 'Not configured';
}
}
// Test API connection
async function testApiConnection(token) {
try {
const tempToken = apiToken;
apiToken = token; // Temporarily set token for test
const response = await apiFetch(`${API_URL}/health`);
const data = await response.json();
if (data.status === 'ok') {
apiToken = tempToken; // Restore original
return { success: true, message: 'Connected ✓' };
} else {
apiToken = tempToken;
return { success: false, message: 'API error' };
}
} catch (error) {
return { success: false, message: error.message };
}
}
// Save and test token
apiConfigForm.addEventListener('submit', async (e) => {
e.preventDefault();
const token = apiTokenInput.value.trim();
if (!token) {
updateConnectionStatus(false, 'Please enter a token');
return;
}
const submitBtn = apiConfigForm.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = 'Testing...';
submitBtn.disabled = true;
try {
const result = await testApiConnection(token);
if (result.success) {
// Save token
localStorage.setItem(TOKEN_STORAGE_KEY, token);
apiToken = token;
updateConnectionStatus(true, result.message);
// Show success message
showNotification('✓ API token saved successfully!', 'success');
// Collapse panel after 2 seconds
setTimeout(() => {
toggleConfigPanel();
}, 2000);
} else {
updateConnectionStatus(false, 'Connection failed');
showNotification('✗ Failed to connect to API. Check your token.', 'error');
}
} catch (error) {
updateConnectionStatus(false, 'Connection error');
showNotification('✗ Error testing connection: ' + error.message, 'error');
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
});
// Clear token
clearTokenBtn.addEventListener('click', () => {
localStorage.removeItem(TOKEN_STORAGE_KEY);
apiToken = '';
apiTokenInput.value = '';
updateConnectionStatus(false, 'Token cleared');
showNotification('Token removed', 'info');
});
// Helper: Show notification toast
function showNotification(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `notification notification-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
background: ${type === 'success' ? 'rgba(16, 185, 129, 0.9)' : type === 'error' ? 'rgba(239, 68, 68, 0.9)' : 'rgba(59, 130, 246, 0.9)'};
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
animation: slideIn 0.3s ease;
font-weight: 500;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Add animations to document
if (!document.getElementById('notification-styles')) {
const style = document.createElement('style');
style.id = 'notification-styles';
style.textContent = `
@keyframes slideIn {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
}
// ==================== TAB SWITCHING ====================
// 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: ${data.title}`;
}
}
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: ${data.title} (${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, `
Download Complete!
${data.successCount}/${data.totalVideos} videos downloaded
${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
${data.videos.map(v => `
-
${v.success ? '✓' : '✗'}
${v.title}
${v.success && v.fileUrl ? `Download` : ''}
${v.error ? `(${v.error})` : ''}
`).join('')}
`);
setLoading(button, false);
});
eventSource.addEventListener('error', (e) => {
let errorHtml = 'Error
Download failed
';
try {
const errorData = JSON.parse(e.data);
errorHtml = formatYouTubeError(errorData);
} catch {
// Keep default error
}
eventSource.close();
progressContainer.style.display = 'none';
showResult('download-result', false, errorHtml);
setLoading(button, false);
});
eventSource.onerror = () => {
eventSource.close();
progressContainer.style.display = 'none';
showResult('download-result', false, `Error
Connection lost
`);
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) => `
${file.name}
${formatSize(file.size)}
`).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, `
Conversion Complete!
${data.successCount}/${data.totalFiles} files converted to MP3
${data.results.map(r => r.success ? `
` : `
${r.fileName}
${r.error}
`).join('')}
`);
// Clear selected files after successful conversion
convertSelectedFiles = [];
updateConvertFilesList();
} catch (error) {
showResult('convert-result', false, `Error
${error.message}
`);
} 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) => `
${file.name}
${formatSize(file.size)}
`).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, `
Transcription Complete!
${data.successCount}/${data.totalFiles} files transcribed
${data.results.map(r => `
-
${r.success ? '✓' : '✗'}
${r.fileName}
${r.success && r.transcriptionUrl ? `View` : ''}
${r.error ? `(${r.error})` : ''}
`).join('')}
${data.results[0]?.text ? `
Preview (first file):
${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}
` : ''}
`);
selectedFiles = [];
updateFilesList();
} catch (error) {
transcribeProgress.style.display = 'none';
showResult('transcribe-result', false, `Error
${error.message}
`);
} 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}: ${data.title}`;
}
if (data.totalVideos > 1) {
processProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
}
});
eventSource.addEventListener('video-complete', (e) => {
const data = JSON.parse(e.data);
processProgressCurrent.innerHTML = `Downloaded: ${data.title}`;
});
eventSource.addEventListener('transcribe-complete', (e) => {
const data = JSON.parse(e.data);
processProgressCurrent.innerHTML = `Transcribed: ${data.title} (${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, `
Processing Complete!
${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
Downloaded: ${data.downloadedCount}/${data.totalVideos}
Transcribed: ${data.transcribedCount}/${data.totalVideos}
${data.results.map(r => `
-
${r.transcriptionSuccess ? '✓' : '✗'}
${r.title}
${r.audioUrl ? `MP3` : ''}
${r.transcriptionUrl ? `TXT` : ''}
${r.error ? `(${r.error})` : ''}
`).join('')}
${data.results[0]?.text ? `
Preview (first file):
${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}
` : ''}
`);
setLoading(button, false);
});
eventSource.addEventListener('error', (e) => {
let errorHtml = 'Error
Processing failed
';
try {
const errorData = JSON.parse(e.data);
errorHtml = formatYouTubeError(errorData);
} catch {
// Keep default error
}
eventSource.close();
processProgress.style.display = 'none';
showResult('process-result', false, errorHtml);
setLoading(button, false);
});
eventSource.onerror = () => {
eventSource.close();
processProgress.style.display = 'none';
showResult('process-result', false, `Error
Connection lost
`);
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, 'Error
Please enter text to translate
');
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, `
Translation Complete!
From: ${data.sourceLanguage} To: ${data.targetLanguage}
${data.translatedText}
`);
} catch (error) {
showResult('translate-text-result', false, `Error
${error.message}
`);
} 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) => `
${file.name}
${formatSize(file.size)}
`).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, `
Translation Complete!
${data.successCount}/${data.totalFiles} files translated
${data.results.map(r => `
-
${r.success ? '✓' : '✗'}
${r.fileName || r.originalPath}
${r.success && r.translationUrl ? `View` : ''}
${r.error ? `(${r.error})` : ''}
`).join('')}
${data.results[0]?.translatedText ? `
Preview (first file):
${data.results[0].translatedText.substring(0, 1000)}${data.results[0].translatedText.length > 1000 ? '...' : ''}
` : ''}
`);
translateSelectedFiles = [];
updateTranslateFilesList();
} catch (error) {
showResult('translate-file-result', false, `Error
${error.message}
`);
} 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, 'Error
Please enter text to summarize
');
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, `
Summary Complete!
Model: ${data.model} | Style: ${data.style} | Chunks: ${data.chunks}
${data.summary}
`);
} catch (error) {
showResult('summarize-text-result', false, `Error
${error.message}
`);
} 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) => `
${file.name}
${formatSize(file.size)}
`).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, `
Summarization Complete!
${data.successCount}/${data.totalFiles} files summarized
${data.results.map(r => `
-
${r.success ? '✓' : '✗'}
${r.fileName || r.filePath}
${r.success && r.summaryUrl ? `View` : ''}
${r.error ? `(${r.error})` : ''}
`).join('')}
${data.results[0]?.summary ? `
Preview (first file):
${data.results[0].summary.substring(0, 1000)}${data.results[0].summary.length > 1000 ? '...' : ''}
` : ''}
`);
summarizeSelectedFiles = [];
updateSummarizeFilesList();
} catch (error) {
showResult('summarize-file-result', false, `Error
${error.message}
`);
} 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}: ${data.title}`;
}
if (data.totalVideos > 1) {
linkProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
}
});
eventSource.addEventListener('video-complete', (e) => {
const data = JSON.parse(e.data);
linkProgressCurrent.innerHTML = `Downloaded: ${data.title}`;
});
eventSource.addEventListener('transcribe-complete', (e) => {
const data = JSON.parse(e.data);
linkProgressCurrent.innerHTML = `Transcribed: ${data.title}`;
});
eventSource.addEventListener('summarize-complete', (e) => {
const data = JSON.parse(e.data);
linkProgressCurrent.innerHTML = `Summarized: ${data.title}`;
});
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, `
Pipeline Complete!
${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
Downloaded: ${data.downloadedCount}/${data.totalVideos} | Transcribed: ${data.transcribedCount} | Summarized: ${data.summarizedCount}
${data.results.map(r => `
-
${r.success ? '✓' : '✗'}
${r.title}
${r.summaryUrl ? `Summary` : ''}
${r.transcriptionUrl ? `Transcript` : ''}
${r.error ? `(${r.error})` : ''}
`).join('')}
${data.results[0]?.summary ? `
Summary Preview:
${data.results[0].summary.substring(0, 2000)}${data.results[0].summary.length > 2000 ? '...' : ''}
` : ''}
`);
setLoading(button, false);
});
eventSource.addEventListener('error', (e) => {
let errorHtml = 'Error
Processing failed
';
try {
const errorData = JSON.parse(e.data);
errorHtml = formatYouTubeError(errorData);
} catch {
// Keep default error
}
eventSource.close();
linkProgress.style.display = 'none';
showResult('summarize-link-result', false, errorHtml);
setLoading(button, false);
});
eventSource.onerror = () => {
eventSource.close();
linkProgress.style.display = 'none';
showResult('summarize-link-result', false, `Error
Connection lost
`);
setLoading(button, false);
};
});
}