ia-resume / templates /index.html
Docfile's picture
Update templates/index.html
295c8a0 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synthétiseur AI Pro</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; }
.fade-in-up { animation: fadeInUp 0.6s ease-out forwards; opacity: 0; }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.segmented-control input:checked + label { background-color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); color: #1e40af; }
.drag-over { border-color: #3b82f6; background-color: #eff6ff; }
.prose h3 { font-weight: 600; }
.prose ul { list-style-type: disc; padding-left: 1.5rem; }
.prose strong { color: #1e3a8a; }
#historyList::-webkit-scrollbar { width: 6px; }
#historyList::-webkit-scrollbar-track { background: #e0e7ff; }
#historyList::-webkit-scrollbar-thumb { background: #a5b4fc; border-radius: 3px; }
#historyList::-webkit-scrollbar-thumb:hover { background: #818cf8; }
.divider { display: flex; align-items: center; text-align: center; margin: 1.25rem 0; color: #94a3b8; }
.divider::before, .divider::after { content: ''; flex: 1; border-bottom: 1px solid #e2e8f0; }
.divider:not(:empty)::before { margin-right: .75em; }
.divider:not(:empty)::after { margin-left: .75em; }
</style>
</head>
<body class="min-h-screen text-slate-800">
<div class="container mx-auto px-4 py-8 md:py-12 max-w-7xl">
<!-- Header -->
<header class="text-center mb-10 md:mb-12 fade-in-up">
<h1 class="text-4xl sm:text-5xl md:text-6xl font-extrabold text-slate-900 mb-3">Synthétiseur AI Pro</h1>
<p class="text-base sm:text-lg text-slate-600 max-w-2xl mx-auto">Transformez vos documents et vidéos en résumés clairs et concis.</p>
</header>
<!-- Main Content -->
<div class="grid lg:grid-cols-5 gap-8">
<!-- Colonne Principale (Upload & Résultat) -->
<div class="lg:col-span-3 space-y-8">
<!-- Upload Card -->
<div class="bg-white/80 backdrop-blur-sm border border-slate-200 rounded-2xl shadow-lg p-6 md:p-8 fade-in-up" style="animation-delay: 100ms;">
<form id="uploadForm" class="space-y-6">
<div>
<h2 class="text-xl md:text-2xl font-bold text-slate-800 mb-1">1. Choisissez votre source</h2>
<p class="text-slate-500 mb-6">Importez un fichier ou collez un lien YouTube.</p>
<label for="fileInput" id="dropzone" class="group block border-2 border-dashed border-slate-300 rounded-xl p-8 text-center transition-all duration-300 hover:border-blue-500 hover:bg-blue-50 cursor-pointer">
<input type="file" id="fileInput" name="file" class="hidden" accept=".pdf,.mp3,.mp4,.wav,.txt,.doc,.docx">
<div class="flex flex-col items-center justify-center space-y-4 text-slate-500">
<svg class="w-12 h-12" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l-3 3m3-3l3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>
<p class="font-semibold text-slate-700">Glissez-déposez ou <span class="text-blue-600">parcourez</span></p>
<p class="text-xs sm:text-sm">PDF, MP3, MP4, WAV, TXT, DOCX (Max 50MB)</p>
</div>
</label>
<div class="divider">OU</div>
<div>
<label for="youtubeUrlInput" class="block text-sm font-medium text-slate-700 mb-2">Pour une vidéo YouTube</label>
<input type="url" id="youtubeUrlInput" name="youtube_url" class="block w-full px-4 py-2.5 text-slate-800 bg-slate-50 border border-slate-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 transition-colors" placeholder="https://www.youtube.com/watch?v=...">
</div>
</div>
<div id="fileInfo" class="hidden items-center justify-between bg-slate-100 rounded-lg p-3">
<div class="flex items-center gap-3 min-w-0">
<div class="flex-shrink-0" id="fileIconContainer"></div>
<div class="text-sm min-w-0">
<p id="fileNameText" class="font-medium text-slate-800 truncate"></p>
<p id="fileSizeText" class="text-slate-500"></p>
</div>
</div>
<button type="button" id="removeFileBtn" class="p-1 rounded-full text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors flex-shrink-0 ml-2">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div>
<h2 class="text-xl md:text-2xl font-bold text-slate-800 mb-1 mt-2">2. Choisissez le niveau de détail</h2>
<p class="text-slate-500 mb-6">Du plus concis au plus exhaustif.</p>
<div class="segmented-control grid grid-cols-3 gap-2 bg-slate-100 p-1.5 rounded-xl">
<input type="radio" name="summary_type" id="type_court" value="court" class="hidden peer">
<label for="type_court" class="block text-center font-semibold py-2.5 px-2 rounded-lg cursor-pointer transition-all text-slate-600 hover:bg-slate-200 peer-checked:text-blue-800">Court</label>
<input type="radio" name="summary_type" id="type_moyen" value="moyen" class="hidden peer" checked>
<label for="type_moyen" class="block text-center font-semibold py-2.5 px-2 rounded-lg cursor-pointer transition-all text-slate-600 hover:bg-slate-200 peer-checked:text-blue-800">Équilibré</label>
<input type="radio" name="summary_type" id="type_detaille" value="detaille" class="hidden peer">
<label for="type_detaille" class="block text-center font-semibold py-2.5 px-2 rounded-lg cursor-pointer transition-all text-slate-600 hover:bg-slate-200 peer-checked:text-blue-800">Détaillé</label>
</div>
</div>
<button type="submit" id="submitBtn" class="flex items-center justify-center w-full bg-blue-700 text-white font-bold py-3 px-6 rounded-xl hover:bg-blue-800 transition-all shadow-lg hover:shadow-blue-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none">
<span id="btnText">Lancer la synthèse</span>
<div id="btnSpinner" class="hidden animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent ml-3"></div>
</button>
</form>
</div>
<!-- Result Card -->
<div id="resultCard" class="hidden bg-white/80 backdrop-blur-sm border border-slate-200 rounded-2xl shadow-lg">
<div class="p-6 border-b border-slate-200">
<h2 class="text-xl md:text-2xl font-bold text-slate-800 mb-1">Synthèse terminée ✨</h2>
<p class="text-slate-500">Voici le résumé de <span id="resultFilename" class="font-semibold text-slate-600"></span>.</p>
</div>
<div class="p-6">
<div class="flex flex-col sm:flex-row items-center justify-end gap-2 mb-4">
<button id="copyBtn" class="w-full sm:w-auto flex items-center justify-center gap-2 bg-slate-100 text-slate-700 px-3 py-1.5 rounded-lg hover:bg-slate-200 transition-colors text-sm font-medium">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375V9.375a2.25 2.25 0 00-2.25-2.25H1.5A2.25 2.25 0 00-.75 9.375v10.125c0 1.243 1.007 2.25 2.25 2.25H18a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H15.75" /></svg>
<span id="copyBtnText">Copier</span>
</button>
<button id="downloadBtn" class="w-full sm:w-auto flex items-center justify-center gap-2 bg-blue-100 text-blue-800 px-3 py-1.5 rounded-lg hover:bg-blue-200 transition-colors text-sm font-medium">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>
<span>Télécharger PDF</span>
</button>
</div>
<div class="bg-slate-50 p-4 rounded-lg">
<div id="summaryContent" class="prose prose-slate max-w-none"></div>
</div>
</div>
</div>
<!-- Error Card -->
<div id="errorCard" class="hidden bg-red-50 border-l-4 border-red-500 p-6 rounded-lg">
<h3 class="text-xl font-bold text-red-800 mb-2">❌ Oups, une erreur est survenue</h3>
<p id="errorMessage" class="text-red-700"></p>
</div>
</div>
<!-- Colonne de l'Historique -->
<div class="lg:col-span-2">
<div class="bg-white/80 backdrop-blur-sm border border-slate-200 rounded-2xl shadow-lg p-6 sticky top-8 fade-in-up" style="animation-delay: 200ms;">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800">Mon Historique</h2>
<button id="clearHistoryBtn" class="text-sm font-medium text-red-600 hover:text-red-800 transition-colors disabled:text-red-300 disabled:cursor-not-allowed">
Tout effacer
</button>
</div>
<div id="historyList" class="space-y-3 max-h-[600px] overflow-y-auto pr-2">
<div id="historyEmptyState" class="text-center py-16">
<svg class="w-16 h-16 mx-auto text-slate-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0A2.25 2.25 0 013.75 7.5h16.5a2.25 2.25 0 012.25 2.25m-18.75 0h18.75c.621 0 1.125.504 1.125 1.125v.158c0 .53-.223.998-.586 1.328l-5.454 4.849a3.75 3.75 0 01-5.304 0L4.336 12.36a1.125 1.125 0 01-.586-1.328v-.158c0-.621.504-1.125 1.125-1.125z" /></svg>
<p class="mt-4 text-slate-500 font-medium">Vos synthèses apparaîtront ici.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// DOM Elements
const uploadForm = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput');
const youtubeUrlInput = document.getElementById('youtubeUrlInput');
const dropzone = document.getElementById('dropzone');
const fileInfo = document.getElementById('fileInfo');
const fileIconContainer = document.getElementById('fileIconContainer');
const fileNameText = document.getElementById('fileNameText');
const fileSizeText = document.getElementById('fileSizeText');
const removeFileBtn = document.getElementById('removeFileBtn');
const submitBtn = document.getElementById('submitBtn');
const btnText = document.getElementById('btnText');
const btnSpinner = document.getElementById('btnSpinner');
const resultCard = document.getElementById('resultCard');
const resultFilename = document.getElementById('resultFilename');
const summaryContent = document.getElementById('summaryContent');
const errorCard = document.getElementById('errorCard');
const errorMessage = document.getElementById('errorMessage');
const copyBtn = document.getElementById('copyBtn');
const copyBtnText = document.getElementById('copyBtnText');
const downloadBtn = document.getElementById('downloadBtn');
const historyList = document.getElementById('historyList');
const historyEmptyState = document.getElementById('historyEmptyState');
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
let currentTaskId = null;
const taskPollers = {};
const LOCAL_STORAGE_KEY = 'aiSummarizerTasks';
// --- Fonctions utilitaires pour le LocalStorage ---
/**
* Récupère les tâches depuis le localStorage.
* @returns {Array} La liste des tâches.
*/
function getLocalTasks() {
const tasksJSON = localStorage.getItem(LOCAL_STORAGE_KEY);
try {
return tasksJSON ? JSON.parse(tasksJSON) : [];
} catch (e) {
console.error("Erreur de parsing des tâches locales:", e);
return [];
}
}
/**
* Sauvegarde la liste des tâches dans le localStorage.
* @param {Array} tasks La liste des tâches à sauvegarder.
*/
function saveLocalTasks(tasks) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(tasks));
}
// --- Fonctions UI ---
function showLoading(isLoading) {
submitBtn.disabled = isLoading;
btnText.textContent = isLoading ? 'Lancement...' : 'Lancer la synthèse';
btnSpinner.classList.toggle('hidden', !isLoading);
}
function displayTaskResult(task) {
if (task.status === 'completed' && task.summary) {
summaryContent.innerHTML = marked.parse(task.summary);
resultFilename.textContent = task.filename;
currentTaskId = task.task_id;
errorCard.classList.add('hidden');
resultCard.classList.remove('hidden');
resultCard.classList.add('fade-in-up');
if (resultCard.getBoundingClientRect().top < 0 || resultCard.getBoundingClientRect().bottom > window.innerHeight) {
window.scrollTo({ top: resultCard.offsetTop - 80, behavior: 'smooth' });
}
} else if (task.status === 'failed' && task.error) {
displayError(task.error);
resultCard.classList.add('hidden');
} else {
resultCard.classList.add('hidden');
errorCard.classList.add('hidden');
}
}
function displayError(message) {
errorMessage.textContent = message;
errorCard.classList.remove('hidden');
errorCard.classList.add('fade-in-up');
}
function getFileIcon(filename, sizeClass = 'w-8 h-8') {
if (filename.toLowerCase().includes('youtube')) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="${sizeClass} text-red-600"><path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"></path></svg>`;
}
const extension = filename.split('.').pop().toLowerCase();
let iconSvg = '';
switch (extension) {
case 'pdf': iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="${sizeClass} text-red-500"><path d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0016.5 9h-1.875a.375.375 0 01-.375-.375V6.75A3.75 3.75 0 009 3H5.625zM12.75 3v3.75a.375.375 0 00.375.375h3.75a3.75 3.75 0 00-4.125-4.125z" /><path d="M10.125 13.125a.375.375 0 00-1.125 0v3.375c0 .207.168.375.375.375h.375a.375.375 0 00.375-.375V13.125zM12.375 12.75a.375.375 0 00-.375.375v3.375c0 .207.168.375.375.375h.375a.375.375 0 00.375-.375V13.125a.375.375 0 00-.375-.375h-.375zM15 12.75a.375.375 0 00-.375.375v3.375c0 .207.168.375.375.375H15a.375.375 0 00.375-.375V13.125a.375.375 0 00-.375-.375h-.375z" /></svg>`; break;
case 'mp3': case 'wav': iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="${sizeClass} text-orange-500"><path fill-rule="evenodd" d="M19.952 1.651a.75.75 0 01.298.6V11.25a.75.75 0 01-1.5 0V3.362l-10.5 4.667v10.138A2.25 2.25 0 116 19.5a2.25 2.25 0 01-2.22-2.018l.002-11.833a.75.75 0 01.993-.74l12 5.333a.75.75 0 01.22.031zM8.25 17.625a.75.75 0 100 1.5.75.75 0 000-1.5z" clip-rule="evenodd" /></svg>`; break;
case 'mp4': iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="${sizeClass} text-indigo-500"><path d="M4.5 4.5a3 3 0 00-3 3v9a3 3 0 003 3h15a3 3 0 003-3v-9a3 3 0 00-3-3h-15zm11.25 9.75a.75.75 0 01-1.5 0v-4.5a.75.75 0 011.5 0v4.5z" /><path d="M8.25 12a.75.75 0 01.75-.75h3a.75.75 0 010 1.5h-3a.75.75 0 01-.75-.75z" /></svg>`; break;
default: iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="${sizeClass} text-slate-500"><path fill-rule="evenodd" d="M5.625 1.5H9a2.25 2.25 0 012.25 2.25v1.875a.75.75 0 01-1.5 0V3.75h-1.5a.75.75 0 010-1.5h1.5a.75.75 0 00-.75-.75H5.625a.75.75 0 00-.75.75v17.25c0 .414.336.75.75.75h12.75a.75.75 0 00.75-.75V16.5a.75.75 0 011.5 0v2.625a2.25 2.25 0 01-2.25 2.25H5.625a2.25 2.25 0 01-2.25-2.25V3.75a2.25 2.25 0 012.25-2.25zm6.875 7.5a.75.75 0 01.75-.75h3.75a.75.75 0 010 1.5h-3.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" /></svg>`;
}
return iconSvg;
}
function resetFileSelection() {
fileInput.value = '';
fileInfo.classList.add('hidden');
fileInfo.classList.remove('flex');
dropzone.classList.remove('hidden');
}
function handleFileSelect(file) {
if (!file) return;
youtubeUrlInput.value = '';
fileNameText.textContent = file.name;
fileSizeText.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`;
fileIconContainer.innerHTML = getFileIcon(file.name, 'w-6 h-6');
dropzone.classList.add('hidden');
fileInfo.classList.remove('hidden');
fileInfo.classList.add('flex');
}
// --- Task & History Management ---
function getStatusBadge(status) {
switch (status) {
case 'completed': return `<span class="status-badge bg-green-100 text-green-800">Terminé</span>`;
case 'processing': return `<span class="status-badge bg-blue-100 text-blue-800">En cours</span>`;
case 'pending': return `<span class="status-badge bg-yellow-100 text-yellow-800">En attente</span>`;
case 'failed': return `<span class="status-badge bg-red-100 text-red-800">Échoué</span>`;
default: return `<span class="status-badge bg-slate-100 text-slate-800">${status}</span>`;
}
}
function createHistoryItemHTML(task) {
const date = new Date(task.created_at).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'});
return `
<div class="task-item bg-white p-3 rounded-lg border border-slate-200" data-task-id="${task.task_id}">
<div class="flex items-center gap-4">
<div class="flex-shrink-0">${getFileIcon(task.filename)}</div>
<div class="flex-1 overflow-hidden">
<div class="flex justify-between items-start">
<p class="font-semibold text-sm text-slate-800 truncate pr-2 cursor-pointer hover:text-blue-600" onclick="viewTask('${task.task_id}')">${task.filename}</p>
<button onclick="deleteTask('${task.task_id}')" class="delete-btn flex-shrink-0 text-slate-400 hover:text-red-600 transition-colors p-1 -mt-1 -mr-1">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
</div>
<div class="flex justify-between items-center mt-2 text-xs text-slate-500">
<span>${date}</span>
${getStatusBadge(task.status)}
</div>
</div>
</div>
<div class="progress-container bg-slate-200 rounded-full h-1.5 w-full mt-3 ${task.status === 'completed' || task.status === 'failed' ? 'hidden' : ''}">
<div class="progress-bar bg-blue-600 h-1.5 rounded-full" style="width: ${task.progress || 0}%"></div>
</div>
</div>`;
}
function renderHistory(tasks) {
historyEmptyState.classList.toggle('hidden', tasks.length > 0);
clearHistoryBtn.disabled = tasks.length === 0;
historyList.innerHTML = tasks.map(createHistoryItemHTML).join('');
}
function loadTasks() {
const tasks = getLocalTasks();
renderHistory(tasks);
tasks.forEach(task => {
if (task.status === 'pending' || task.status === 'processing') {
pollTaskStatus(task.task_id);
}
});
}
function updateTaskInHistoryUI(task) {
const taskElement = document.querySelector(`.task-item[data-task-id="${task.task_id}"]`);
if (!taskElement) return;
taskElement.querySelector('.status-badge').outerHTML = getStatusBadge(task.status);
const progressBar = taskElement.querySelector('.progress-bar');
const progressContainer = taskElement.querySelector('.progress-container');
if (progressBar) progressBar.style.width = `${task.progress || 0}%`;
if (task.status === 'completed' || task.status === 'failed') {
progressContainer?.classList.add('hidden');
} else {
progressContainer?.classList.remove('hidden');
}
}
function pollTaskStatus(taskId) {
if (taskPollers[taskId]) return;
taskPollers[taskId] = setInterval(async () => {
try {
const response = await fetch(`/task/${taskId}`);
if (!response.ok) {
if (response.status === 404) clearInterval(taskPollers[taskId]);
return;
}
const updatedTask = await response.json();
let tasks = getLocalTasks();
const taskIndex = tasks.findIndex(t => t.task_id === taskId);
if (taskIndex !== -1) {
tasks[taskIndex] = updatedTask;
saveLocalTasks(tasks);
}
updateTaskInHistoryUI(updatedTask);
if (updatedTask.status === 'completed' || updatedTask.status === 'failed') {
clearInterval(taskPollers[taskId]);
delete taskPollers[taskId];
if (taskIndex === 0) {
displayTaskResult(updatedTask);
}
}
} catch (err) {
console.error(`Error polling task ${taskId}:`, err);
clearInterval(taskPollers[taskId]);
delete taskPollers[taskId];
}
}, 3000);
}
window.viewTask = function(taskId) {
const tasks = getLocalTasks();
const task = tasks.find(t => t.task_id === taskId);
if (task) {
displayTaskResult(task);
}
}
window.deleteTask = function(taskId) {
if (confirm('Voulez-vous vraiment supprimer cette tâche ?')) {
let tasks = getLocalTasks();
tasks = tasks.filter(t => t.task_id !== taskId);
saveLocalTasks(tasks);
loadTasks();
}
}
clearHistoryBtn.addEventListener('click', async () => {
if (confirm('Êtes-vous sûr de vouloir effacer tout l\'historique ? Cette action est irréversible.')) {
saveLocalTasks([]);
loadTasks();
resultCard.classList.add('hidden');
errorCard.classList.add('hidden');
}
});
// --- Event Listeners ---
fileInput.addEventListener('change', (e) => handleFileSelect(e.target.files[0]));
dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); });
dropzone.addEventListener('dragleave', (e) => { e.preventDefault(); dropzone.classList.remove('drag-over'); });
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
fileInput.files = e.dataTransfer.files;
handleFileSelect(file);
});
removeFileBtn.addEventListener('click', resetFileSelection);
youtubeUrlInput.addEventListener('input', () => {
if (youtubeUrlInput.value.trim() !== '') resetFileSelection();
});
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!fileInput.files[0] && !youtubeUrlInput.value.trim()) {
displayError('Veuillez sélectionner un fichier ou entrer une URL YouTube.');
return;
}
showLoading(true);
resultCard.classList.add('hidden');
errorCard.classList.add('hidden');
const formData = new FormData(uploadForm);
try {
const response = await fetch('/upload', { method: 'POST', body: formData });
const data = await response.json();
if (!response.ok || data.error) throw new Error(data.error || `Erreur serveur: ${response.status}`);
const taskResponse = await fetch(`/task/${data.task_id}`);
const newTask = await taskResponse.json();
let tasks = getLocalTasks();
tasks.unshift(newTask);
saveLocalTasks(tasks);
renderHistory(tasks);
pollTaskStatus(newTask.task_id);
} catch (err) {
displayError(err.message);
} finally {
showLoading(false);
resetFileSelection();
youtubeUrlInput.value = '';
}
});
downloadBtn.addEventListener('click', () => {
if (currentTaskId) {
window.location.href = `/download/${currentTaskId}`;
}
});
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(summaryContent.innerText).then(() => {
copyBtnText.textContent = 'Copié !';
setTimeout(() => { copyBtnText.textContent = 'Copier'; }, 2000);
});
});
// Initial Load
loadTasks();
</script>
</body>
</html>