Docfile commited on
Commit
0a6012f
·
verified ·
1 Parent(s): e6d4827

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +188 -126
templates/index.html CHANGED
@@ -6,8 +6,6 @@
6
  <title>Synthétiseur AI Pro</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
11
  <style>
12
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
13
  body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; }
@@ -93,7 +91,7 @@
93
  </div>
94
 
95
  <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">
96
- <span id="btnText">Générer la synthèse</span>
97
  <div id="btnSpinner" class="hidden animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent ml-3"></div>
98
  </button>
99
  </form>
@@ -112,12 +110,11 @@
112
  <span id="copyBtnText">Copier</span>
113
  </button>
114
  <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">
115
- <span id="downloadBtnSpinner" class="hidden animate-spin rounded-full h-4 w-4 border-2 border-blue-800 border-t-transparent"></span>
116
- <svg id="downloadBtnIcon" 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>
117
- <span id="downloadBtnText">Télécharger PDF</span>
118
  </button>
119
  </div>
120
- <div id="summaryContentWrapper" class="bg-slate-50 p-4 rounded-lg">
121
  <div id="summaryContent" class="prose prose-slate max-w-none"></div>
122
  </div>
123
  </div>
@@ -134,15 +131,15 @@
134
  <div class="lg:col-span-2">
135
  <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;">
136
  <div class="flex justify-between items-center mb-4">
137
- <h2 class="text-xl font-bold text-gray-800">Historique</h2>
138
- <button id="clearHistory" class="text-red-600 hover:text-red-800 text-sm font-medium transition-colors">
139
  Tout effacer
140
  </button>
141
  </div>
142
- <div id="historyList" class="space-y-2 max-h-[600px] overflow-y-auto pr-2">
143
  <div id="historyEmptyState" class="text-center py-16">
144
  <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>
145
- <p class="mt-4 text-slate-500 font-medium">Vos résumés apparaîtront ici.</p>
146
  </div>
147
  </div>
148
  </div>
@@ -151,8 +148,6 @@
151
  </div>
152
 
153
  <script>
154
- const { jsPDF } = window.jspdf;
155
-
156
  // DOM Elements
157
  const uploadForm = document.getElementById('uploadForm');
158
  const fileInput = document.getElementById('fileInput');
@@ -169,41 +164,52 @@
169
  const resultCard = document.getElementById('resultCard');
170
  const resultFilename = document.getElementById('resultFilename');
171
  const summaryContent = document.getElementById('summaryContent');
172
- const summaryContentWrapper = document.getElementById('summaryContentWrapper');
173
  const errorCard = document.getElementById('errorCard');
174
  const errorMessage = document.getElementById('errorMessage');
175
  const copyBtn = document.getElementById('copyBtn');
176
  const copyBtnText = document.getElementById('copyBtnText');
177
  const downloadBtn = document.getElementById('downloadBtn');
178
- const downloadBtnText = document.getElementById('downloadBtnText');
179
- const downloadBtnIcon = document.getElementById('downloadBtnIcon');
180
- const downloadBtnSpinner = document.getElementById('downloadBtnSpinner');
181
  const historyList = document.getElementById('historyList');
182
  const historyEmptyState = document.getElementById('historyEmptyState');
183
- const clearHistoryBtn = document.getElementById('clearHistory');
184
 
185
- let currentHistoryId = null;
 
186
 
187
  // --- UI Helper Functions ---
188
  function showLoading(isLoading) {
189
  submitBtn.disabled = isLoading;
190
- btnText.textContent = isLoading ? 'Génération en cours...' : 'Générer la synthèse';
191
  btnSpinner.classList.toggle('hidden', !isLoading);
192
  }
193
 
194
- function displaySummary(item) {
195
- summaryContent.innerHTML = marked.parse(item.summary);
196
- resultFilename.textContent = item.filename;
197
- currentHistoryId = item.id;
198
-
199
- errorCard.classList.add('hidden');
200
- resultCard.classList.remove('hidden');
201
- resultCard.classList.add('fade-in-up');
202
- window.scrollTo({ top: resultCard.offsetTop - 80, behavior: 'smooth' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
 
205
- function getFileIcon(filename, sizeClass = 'w-6 h-6') {
206
- if (filename === 'Vidéo YouTube') {
207
  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>`;
208
  }
209
  const extension = filename.split('.').pop().toLowerCase();
@@ -219,21 +225,155 @@
219
 
220
  function resetFileSelection() {
221
  fileInput.value = '';
222
- fileInfo.classList.add('hidden'); fileInfo.classList.remove('flex');
 
223
  dropzone.classList.remove('hidden');
224
  }
225
 
226
- // --- Event Listeners & Core Logic ---
227
  function handleFileSelect(file) {
228
  if (!file) return;
229
- youtubeUrlInput.value = ''; // Clear URL input if a file is selected
230
  fileNameText.textContent = file.name;
231
  fileSizeText.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`;
232
- fileIconContainer.innerHTML = getFileIcon(file.name);
233
  dropzone.classList.add('hidden');
234
  fileInfo.classList.remove('hidden');
235
  fileInfo.classList.add('flex');
236
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
  fileInput.addEventListener('change', (e) => handleFileSelect(e.target.files[0]));
239
  dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); });
@@ -249,14 +389,14 @@
249
 
250
  youtubeUrlInput.addEventListener('input', () => {
251
  if (youtubeUrlInput.value.trim() !== '') {
252
- resetFileSelection(); // Clear file selection if URL is being typed
253
  }
254
  });
255
 
256
  uploadForm.addEventListener('submit', async (e) => {
257
  e.preventDefault();
258
  if (!fileInput.files[0] && !youtubeUrlInput.value.trim()) {
259
- alert('Veuillez sélectionner un fichier ou entrer une URL YouTube.');
260
  return;
261
  }
262
  showLoading(true);
@@ -270,62 +410,21 @@
270
  const data = await response.json();
271
  if (!response.ok || data.error) throw new Error(data.error || `Erreur serveur: ${response.status}`);
272
 
273
- let sourceName = fileInput.files[0] ? fileInput.files[0].name : 'Vidéo YouTube';
274
- const newItem = { id: data.history_id, summary: data.summary, filename: sourceName };
275
- displaySummary(newItem);
276
- loadHistory();
277
-
278
  } catch (err) {
279
- errorMessage.textContent = err.message;
280
- errorCard.classList.remove('hidden'); errorCard.classList.add('fade-in-up');
281
  } finally {
282
  showLoading(false);
 
 
283
  }
284
  });
285
 
286
- downloadBtn.addEventListener('click', async () => {
287
- downloadBtn.disabled = true;
288
- downloadBtnText.textContent = 'Génération...';
289
- downloadBtnIcon.classList.add('hidden');
290
- downloadBtnSpinner.classList.remove('hidden');
291
-
292
- try {
293
- const canvas = await html2canvas(summaryContentWrapper, { scale: 2, useCORS: true });
294
- const imgData = canvas.toDataURL('image/jpeg', 0.95);
295
- const pdf = new jsPDF({ orientation: 'portrait', unit: 'px', format: 'a4' });
296
- const pdfWidth = pdf.internal.pageSize.getWidth();
297
- const pdfHeight = pdf.internal.pageSize.getHeight();
298
- const ratio = canvas.width / pdfWidth;
299
- const imgHeight = canvas.height / ratio;
300
-
301
- pdf.setFontSize(16);
302
- pdf.setTextColor(40, 40, 40);
303
- pdf.text(`Résumé de : ${resultFilename.textContent}`, 20, 30);
304
-
305
- let heightLeft = imgHeight;
306
- let position = 40;
307
-
308
- pdf.addImage(imgData, 'JPEG', 0, position, pdfWidth, imgHeight);
309
- heightLeft -= (pdfHeight - position);
310
-
311
- while (heightLeft > 0) {
312
- position -= pdfHeight;
313
- pdf.addPage();
314
- pdf.addImage(imgData, 'JPEG', 0, position, pdfWidth, imgHeight);
315
- heightLeft -= pdfHeight;
316
- }
317
-
318
- const safeFilename = resultFilename.textContent.replace(/[^a-z0-9]/gi, '_').toLowerCase() + "_resume.pdf";
319
- pdf.save(safeFilename);
320
-
321
- } catch (err) {
322
- console.error("Erreur lors de la génération du PDF:", err);
323
- alert("Impossible de générer le PDF.");
324
- } finally {
325
- downloadBtn.disabled = false;
326
- downloadBtnText.textContent = 'Télécharger PDF';
327
- downloadBtnIcon.classList.remove('hidden');
328
- downloadBtnSpinner.add('hidden');
329
  }
330
  });
331
 
@@ -336,46 +435,9 @@
336
  });
337
  });
338
 
339
- async function loadHistory() {
340
- try {
341
- const response = await fetch('/history');
342
- const history = await response.json();
343
- historyEmptyState.classList.toggle('hidden', history.length > 0);
344
- historyList.innerHTML = history.length > 0 ? history.map(item => `
345
- <div class="flex items-center gap-4 p-3 rounded-lg hover:bg-slate-100 transition-colors cursor-pointer" onclick="viewSummary(${item.id})">
346
- <div class="flex-shrink-0">${getFileIcon(item.filename)}</div>
347
- <div class="flex-1 overflow-hidden">
348
- <div class="font-semibold text-sm text-slate-800 truncate">${item.filename}</div>
349
- <div class="flex justify-between items-center mt-1">
350
- <span class="text-xs text-slate-500">${item.date}</span>
351
- <span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full font-medium capitalize">${item.summary_type === 'detaille' ? 'détaillé' : item.summary_type}</span>
352
- </div>
353
- </div>
354
- </div>
355
- `).reverse().join('') : '';
356
- } catch (err) {
357
- console.error('Error loading history:', err);
358
- historyList.innerHTML = '<p class="text-red-500 text-center py-8">Impossible de charger l\'historique.</p>';
359
- }
360
- }
361
-
362
- window.viewSummary = function(id) {
363
- fetch('/history').then(res => res.json()).then(history => {
364
- const item = history.find(item => item.id === id);
365
- if (item) displaySummary(item);
366
- });
367
- }
368
-
369
- clearHistoryBtn.addEventListener('click', async () => {
370
- if (confirm('Êtes-vous sûr de vouloir effacer tout l\'historique ? Cette action est irréversible.')) {
371
- await fetch('/clear-history', { method: 'POST' });
372
- loadHistory();
373
- resultCard.classList.add('hidden');
374
- }
375
- });
376
-
377
  // Initial Load
378
- loadHistory();
 
379
  </script>
380
  </body>
381
  </html>
 
6
  <title>Synthétiseur AI Pro</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
 
 
9
  <style>
10
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
11
  body { font-family: 'Inter', sans-serif; background-color: #f0f4f8; }
 
91
  </div>
92
 
93
  <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">
94
+ <span id="btnText">Lancer la synthèse</span>
95
  <div id="btnSpinner" class="hidden animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent ml-3"></div>
96
  </button>
97
  </form>
 
110
  <span id="copyBtnText">Copier</span>
111
  </button>
112
  <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">
113
+ <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>
114
+ <span>Télécharger PDF</span>
 
115
  </button>
116
  </div>
117
+ <div class="bg-slate-50 p-4 rounded-lg">
118
  <div id="summaryContent" class="prose prose-slate max-w-none"></div>
119
  </div>
120
  </div>
 
131
  <div class="lg:col-span-2">
132
  <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;">
133
  <div class="flex justify-between items-center mb-4">
134
+ <h2 class="text-xl font-bold text-gray-800">Historique des Tâches</h2>
135
+ <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">
136
  Tout effacer
137
  </button>
138
  </div>
139
+ <div id="historyList" class="space-y-3 max-h-[600px] overflow-y-auto pr-2">
140
  <div id="historyEmptyState" class="text-center py-16">
141
  <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>
142
+ <p class="mt-4 text-slate-500 font-medium">Vos tâches apparaîtront ici.</p>
143
  </div>
144
  </div>
145
  </div>
 
148
  </div>
149
 
150
  <script>
 
 
151
  // DOM Elements
152
  const uploadForm = document.getElementById('uploadForm');
153
  const fileInput = document.getElementById('fileInput');
 
164
  const resultCard = document.getElementById('resultCard');
165
  const resultFilename = document.getElementById('resultFilename');
166
  const summaryContent = document.getElementById('summaryContent');
 
167
  const errorCard = document.getElementById('errorCard');
168
  const errorMessage = document.getElementById('errorMessage');
169
  const copyBtn = document.getElementById('copyBtn');
170
  const copyBtnText = document.getElementById('copyBtnText');
171
  const downloadBtn = document.getElementById('downloadBtn');
 
 
 
172
  const historyList = document.getElementById('historyList');
173
  const historyEmptyState = document.getElementById('historyEmptyState');
174
+ const clearHistoryBtn = document.getElementById('clearHistoryBtn');
175
 
176
+ let currentTaskId = null;
177
+ const taskPollers = {};
178
 
179
  // --- UI Helper Functions ---
180
  function showLoading(isLoading) {
181
  submitBtn.disabled = isLoading;
182
+ btnText.textContent = isLoading ? 'Lancement...' : 'Lancer la synthèse';
183
  btnSpinner.classList.toggle('hidden', !isLoading);
184
  }
185
 
186
+ function displayTaskResult(task) {
187
+ if (task.status === 'completed' && task.summary) {
188
+ summaryContent.innerHTML = marked.parse(task.summary);
189
+ resultFilename.textContent = task.filename;
190
+ currentTaskId = task.task_id;
191
+
192
+ errorCard.classList.add('hidden');
193
+ resultCard.classList.remove('hidden');
194
+ resultCard.classList.add('fade-in-up');
195
+ window.scrollTo({ top: resultCard.offsetTop - 80, behavior: 'smooth' });
196
+ } else if (task.status === 'failed' && task.error) {
197
+ displayError(task.error);
198
+ resultCard.classList.add('hidden');
199
+ } else {
200
+ resultCard.classList.add('hidden');
201
+ errorCard.classList.add('hidden');
202
+ }
203
+ }
204
+
205
+ function displayError(message) {
206
+ errorMessage.textContent = message;
207
+ errorCard.classList.remove('hidden');
208
+ errorCard.classList.add('fade-in-up');
209
  }
210
 
211
+ function getFileIcon(filename, sizeClass = 'w-8 h-8') {
212
+ if (filename.toLowerCase().includes('youtube')) {
213
  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>`;
214
  }
215
  const extension = filename.split('.').pop().toLowerCase();
 
225
 
226
  function resetFileSelection() {
227
  fileInput.value = '';
228
+ fileInfo.classList.add('hidden');
229
+ fileInfo.classList.remove('flex');
230
  dropzone.classList.remove('hidden');
231
  }
232
 
 
233
  function handleFileSelect(file) {
234
  if (!file) return;
235
+ youtubeUrlInput.value = '';
236
  fileNameText.textContent = file.name;
237
  fileSizeText.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`;
238
+ fileIconContainer.innerHTML = getFileIcon(file.name, 'w-6 h-6');
239
  dropzone.classList.add('hidden');
240
  fileInfo.classList.remove('hidden');
241
  fileInfo.classList.add('flex');
242
  }
243
+
244
+ // --- Task & History Management ---
245
+
246
+ function getStatusBadge(status) {
247
+ switch (status) {
248
+ case 'completed': return `<span class="status-badge bg-green-100 text-green-800">Terminé</span>`;
249
+ case 'processing': return `<span class="status-badge bg-blue-100 text-blue-800">En cours</span>`;
250
+ case 'pending': return `<span class="status-badge bg-yellow-100 text-yellow-800">En attente</span>`;
251
+ case 'failed': return `<span class="status-badge bg-red-100 text-red-800">Échoué</span>`;
252
+ default: return `<span class="status-badge bg-slate-100 text-slate-800">${status}</span>`;
253
+ }
254
+ }
255
+
256
+ function createHistoryItemHTML(task) {
257
+ const date = new Date(task.created_at).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'});
258
+ return `
259
+ <div class="task-item bg-white p-3 rounded-lg border border-slate-200" data-task-id="${task.task_id}">
260
+ <div class="flex items-center gap-4">
261
+ <div class="flex-shrink-0">${getFileIcon(task.filename)}</div>
262
+ <div class="flex-1 overflow-hidden">
263
+ <div class="flex justify-between items-start">
264
+ <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>
265
+ <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">
266
+ <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>
267
+ </button>
268
+ </div>
269
+ <div class="flex justify-between items-center mt-2 text-xs text-slate-500">
270
+ <span>${date}</span>
271
+ ${getStatusBadge(task.status)}
272
+ </div>
273
+ </div>
274
+ </div>
275
+ <div class="progress-container bg-slate-200 rounded-full h-1.5 w-full mt-3 ${task.status === 'completed' || task.status === 'failed' ? 'hidden' : ''}">
276
+ <div class="progress-bar bg-blue-600 h-1.5 rounded-full" style="width: ${task.progress || 0}%"></div>
277
+ </div>
278
+ </div>`;
279
+ }
280
+
281
+ async function loadTasks() {
282
+ try {
283
+ const response = await fetch('/tasks');
284
+ const tasks = await response.json();
285
+
286
+ historyEmptyState.classList.toggle('hidden', tasks.length > 0);
287
+ clearHistoryBtn.disabled = tasks.length === 0;
288
+
289
+ historyList.innerHTML = '';
290
+ for(const task of tasks) {
291
+ historyList.insertAdjacentHTML('beforeend', createHistoryItemHTML(task));
292
+ if (task.status === 'pending' || task.status === 'processing') {
293
+ pollTaskStatus(task.task_id);
294
+ }
295
+ }
296
+ } catch (err) {
297
+ console.error('Error loading history:', err);
298
+ historyList.innerHTML = '<p class="text-red-500 text-center py-8">Impossible de charger l\'historique.</p>';
299
+ }
300
+ }
301
+
302
+ function updateTaskInHistoryUI(task) {
303
+ const taskElement = document.querySelector(`.task-item[data-task-id="${task.task_id}"]`);
304
+ if (!taskElement) return;
305
+
306
+ taskElement.querySelector('.status-badge').outerHTML = getStatusBadge(task.status);
307
+ const progressBar = taskElement.querySelector('.progress-bar');
308
+ const progressContainer = taskElement.querySelector('.progress-container');
309
+
310
+ if (progressBar) progressBar.style.width = `${task.progress || 0}%`;
311
+
312
+ if (task.status === 'completed' || task.status === 'failed') {
313
+ progressContainer?.classList.add('hidden');
314
+ } else {
315
+ progressContainer?.classList.remove('hidden');
316
+ }
317
+ }
318
+
319
+ function pollTaskStatus(taskId) {
320
+ if (taskPollers[taskId]) return;
321
+
322
+ taskPollers[taskId] = setInterval(async () => {
323
+ try {
324
+ const response = await fetch(`/task/${taskId}`);
325
+ if (!response.ok) {
326
+ // Stop polling if task not found (e.g., deleted)
327
+ if (response.status === 404) clearInterval(taskPollers[taskId]);
328
+ return;
329
+ }
330
+ const task = await response.json();
331
+ updateTaskInHistoryUI(task);
332
+
333
+ if (task.status === 'completed' || task.status === 'failed') {
334
+ clearInterval(taskPollers[taskId]);
335
+ delete taskPollers[taskId];
336
+ // If it's the most recent task, display its final state
337
+ const firstTaskInList = historyList.querySelector('.task-item');
338
+ if (firstTaskInList && firstTaskInList.dataset.taskId === taskId) {
339
+ displayTaskResult(task);
340
+ }
341
+ }
342
+ } catch (err) {
343
+ console.error(`Error polling task ${taskId}:`, err);
344
+ clearInterval(taskPollers[taskId]); // Stop on network error
345
+ delete taskPollers[taskId];
346
+ }
347
+ }, 2000); // Poll every 2 seconds
348
+ }
349
+
350
+ window.viewTask = async function(taskId) {
351
+ const response = await fetch(`/task/${taskId}`);
352
+ const task = await response.json();
353
+ displayTaskResult(task);
354
+ }
355
+
356
+ window.deleteTask = async function(taskId) {
357
+ if (confirm('Voulez-vous vraiment supprimer cette tâche ?')) {
358
+ await fetch(`/delete-task/${taskId}`, { method: 'DELETE' });
359
+ document.querySelector(`.task-item[data-task-id="${taskId}"]`)?.remove();
360
+ if (historyList.children.length === 1) { // 1 is the empty state
361
+ historyEmptyState.classList.remove('hidden');
362
+ clearHistoryBtn.disabled = true;
363
+ }
364
+ }
365
+ }
366
+
367
+ clearHistoryBtn.addEventListener('click', async () => {
368
+ if (confirm('Êtes-vous sûr de vouloir effacer tout l\'historique ? Cette action est irréversible.')) {
369
+ await fetch('/clear-tasks', { method: 'POST' });
370
+ loadTasks(); // Reload to show empty state
371
+ resultCard.classList.add('hidden');
372
+ }
373
+ });
374
+
375
+
376
+ // --- Event Listeners ---
377
 
378
  fileInput.addEventListener('change', (e) => handleFileSelect(e.target.files[0]));
379
  dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); });
 
389
 
390
  youtubeUrlInput.addEventListener('input', () => {
391
  if (youtubeUrlInput.value.trim() !== '') {
392
+ resetFileSelection();
393
  }
394
  });
395
 
396
  uploadForm.addEventListener('submit', async (e) => {
397
  e.preventDefault();
398
  if (!fileInput.files[0] && !youtubeUrlInput.value.trim()) {
399
+ displayError('Veuillez sélectionner un fichier ou entrer une URL YouTube.');
400
  return;
401
  }
402
  showLoading(true);
 
410
  const data = await response.json();
411
  if (!response.ok || data.error) throw new Error(data.error || `Erreur serveur: ${response.status}`);
412
 
413
+ // Add new task to top of history and start polling
414
+ await loadTasks(); // Reload entire list to get the new task on top
415
+
 
 
416
  } catch (err) {
417
+ displayError(err.message);
 
418
  } finally {
419
  showLoading(false);
420
+ resetFileSelection();
421
+ youtubeUrlInput.value = '';
422
  }
423
  });
424
 
425
+ downloadBtn.addEventListener('click', () => {
426
+ if (currentTaskId) {
427
+ window.location.href = `/download/${currentTaskId}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  }
429
  });
430
 
 
435
  });
436
  });
437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  // Initial Load
439
+ loadTasks();
440
+
441
  </script>
442
  </body>
443
  </html>