- Add API token authentication middleware (X-API-Key header) - Add CORS configuration with ALLOWED_ORIGINS - Add security HTTP headers (X-Frame-Options, CSP, etc.) - Add web interface for API token configuration with localStorage - Add toggle visibility for token input - Add connection status indicator - Add auto-save token functionality - Update API documentation with authentication examples - Add deployment guides (OVH specific and general) - Add local testing guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1347 lines
50 KiB
JavaScript
1347 lines
50 KiB
JavaScript
// ==================== 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, `
|
||
<h3>⚠️ Authentication Error</h3>
|
||
<p>${response.status === 401 ? 'API token required' : 'Invalid API token'}</p>
|
||
<p>Please configure your API token in the configuration panel above.</p>
|
||
`);
|
||
throw new Error('Authentication failed');
|
||
}
|
||
|
||
return response;
|
||
}
|
||
|
||
// 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 = `
|
||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
|
||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||
`;
|
||
} else {
|
||
eyeIcon.innerHTML = `
|
||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||
<circle cx="12" cy="12" r="3"></circle>
|
||
`;
|
||
}
|
||
});
|
||
|
||
// 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 = '<span>Testing...</span>';
|
||
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: <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);
|
||
};
|
||
});
|
||
}
|