|
|
<!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 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> |
|
|
|
|
|
|
|
|
<div class="grid lg:grid-cols-5 gap-8"> |
|
|
|
|
|
<div class="lg:col-span-3 space-y-8"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function saveLocalTasks(tasks) { |
|
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(tasks)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
loadTasks(); |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
</html> |