Docfile commited on
Commit
837bc52
·
verified ·
1 Parent(s): 02836aa

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +1104 -333
templates/index.html CHANGED
@@ -6,314 +6,841 @@
6
  <title>Résolveur d'Images & PDF - Mariam</title>
7
  <style>
8
  :root {
9
- --primary-color: #3498db; --primary-hover: #2980b9; --secondary-color: #2ecc71;
10
- --secondary-hover: #27ae60; --danger-color: #e74c3c; --danger-hover: #c0392b;
11
- --background-color: #f4f7f6; --text-color: #333; --border-color: #e0e0e0;
12
- --shadow: 0 4px 15px rgba(0,0,0,0.1); --spacing-unit: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
- * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
15
  body {
16
- font-family: 'Segoe UI', system-ui, sans-serif; max-width: 800px; margin: 0 auto;
17
- padding: calc(var(--spacing-unit) * 2); line-height: 1.6;
18
- background-color: var(--background-color); color: var(--text-color);
19
- }
20
- .header { text-align: center; margin-bottom: calc(var(--spacing-unit) * 2); }
21
- .header h1 { font-size: 2.5rem; color: #2c3e50; margin-bottom: calc(var(--spacing-unit) * 0.5); }
22
- .header .subtitle { font-size: 1.1rem; color: #555; }
23
- .telegram-join-button-container { text-align: center; margin-bottom: calc(var(--spacing-unit) * 2); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  .telegram-button {
25
- display: inline-block; background-color: #0088cc; color: white;
26
- padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); border-radius: 0.5rem;
27
- text-decoration: none; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
28
- }
29
- .telegram-button:hover { transform: translateY(-2px); background-color: #006699; }
30
- .container { background-color: white; padding: calc(var(--spacing-unit) * 2); border-radius: 1rem; box-shadow: var(--shadow); margin-bottom: 2rem; }
31
- .style-selection { background-color: #f9f9f9; padding: calc(var(--spacing-unit) * 1.5); border-radius: 0.75rem; border: 1px solid var(--border-color); margin-bottom: calc(var(--spacing-unit) * 1.5); }
32
- .style-selection h3 { margin-bottom: var(--spacing-unit); color: #2c3e50; font-size: 1.2rem; }
33
- .radio-group { display: flex; flex-direction: column; gap: var(--spacing-unit); }
34
- .radio-option { display: flex; align-items: flex-start; padding: calc(var(--spacing-unit) * 0.75); border-radius: 0.5rem; transition: background-color 0.2s; cursor: pointer; border: 1px solid transparent; }
35
- .radio-option:hover { background-color: #f0f4f8; border-color: var(--primary-color); }
36
- .radio-option input[type="radio"] { margin-top: 0.25rem; margin-right: calc(var(--spacing-unit) * 0.75); width: 1.25rem; height: 1.25rem; accent-color: var(--primary-color); }
37
- .radio-content { flex: 1; }
38
- .radio-label { font-weight: 500; margin-bottom: calc(var(--spacing-unit) * 0.25); display: block; }
39
- .radio-description { font-size: 0.9rem; color: #666; }
40
- .upload-section { border: 3px dashed var(--border-color); padding: calc(var(--spacing-unit) * 2); text-align: center; border-radius: 0.75rem; cursor: pointer; transition: all 0.3s ease; background-color: #f8f9fa; margin: calc(var(--spacing-unit) * 1.5) 0; }
41
- .upload-section:hover { border-color: var(--primary-color); background-color: #e8f4fb; }
42
- .upload-icon { font-size: 2.5rem; margin-bottom: var(--spacing-unit); color: var(--primary-color); }
43
- #file-input { display: none; }
44
- #file-preview-area { margin-top: var(--spacing-unit); display: flex; flex-wrap: wrap; gap: var(--spacing-unit); justify-content: center; }
45
- .preview-item { display: flex; flex-direction: column; align-items: center; gap: calc(var(--spacing-unit) * 0.5); padding: calc(var(--spacing-unit) * 0.5); border: 1px solid var(--border-color); border-radius: 0.5rem; background-color: #fdfdfd; }
46
- .preview-item img { max-width: 100px; max-height: 100px; border-radius: 0.25rem; object-fit: cover; }
47
- .preview-item .pdf-icon { font-size: 3rem; color: var(--danger-color); }
48
- .preview-item span { font-size: 0.8rem; color: #555; word-break: break-all; max-width: 100px; text-align: center; }
49
- .button { width: 100%; padding: var(--spacing-unit); border: none; border-radius: 0.5rem; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; margin: var(--spacing-unit) 0; background-color: var(--primary-color); color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-decoration: none; display: inline-block; text-align:center; }
50
- .button:hover:not(:disabled) { transform: translateY(-2px); background-color: var(--primary-hover); }
51
- .button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
52
- .clear-button { background-color: var(--danger-color); margin-top: 0; }
53
- .clear-button:hover:not(:disabled) { background-color: var(--danger-hover); }
54
- .download-button { background-color: var(--secondary-color); }
55
- .download-button:hover:not(:disabled) { background-color: var(--secondary-hover); }
56
- .cooldown-notice { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 0.5rem; padding: var(--spacing-unit); margin: var(--spacing-unit) 0; text-align: center; color: #856404; font-weight: 500; }
57
- .cooldown-timer { font-size: 1.2rem; color: #d63031; font-weight: bold; }
58
- #solving-container { display: none; background-color: #f9f9f9; padding: calc(var(--spacing-unit) * 1.5); border-radius: 0.75rem; border: 1px solid var(--border-color); margin-top: calc(var(--spacing-unit) * 1.5); }
59
- .status { text-align: center; margin-bottom: var(--spacing-unit); font-weight: bold; color: #2c3e50; }
60
- .status.error { color: #e74c3c; } .status.completed { color: #2ecc71; }
61
- .telegram-notice { background-color: #eaf5ff; border-left: 5px solid var(--primary-color); padding: var(--spacing-unit); margin: var(--spacing-unit) 0; border-radius: 0 0.5rem 0.5rem 0; }
62
- .response-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); }
63
- #response { background-color: #fdfdfd; padding: var(--spacing-unit); border-radius: 0.5rem; border: 1px solid #eee; min-height: 50px; white-space: pre-wrap; word-wrap: break-word; }
64
- #history-container { margin-top: 2rem; }
65
- #history-container h2 { text-align:center; margin-bottom: 1rem; color: #2c3e50;}
66
- #history-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem;}
67
- .history-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: #fff; border: 1px solid var(--border-color); border-radius: 0.5rem; transition: box-shadow 0.2s; }
68
- .history-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
69
- .history-info { display: flex; flex-direction: column; }
70
- .history-filename { font-weight: 500; }
71
- .history-status { font-size: 0.85rem; }
72
- .history-status-pending { color: #f39c12; } .history-status-completed { color: var(--secondary-color); } .history-status-error { color: var(--danger-color); }
73
- .history-actions .button { width: auto; padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0; }
74
- #clear-history-button { background-color: var(--danger-color); margin-top: 1rem; }
75
- @media (max-width: 768px) {
76
- body { padding: var(--spacing-unit); }
77
- .header h1 { font-size: 1.75rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
  </style>
80
  </head>
81
  <body>
82
- <div class="header">
83
- <h1>🖼️ Science (Math, Physique, Chimie) 🧠</h1>
84
- <p class="subtitle">Avec Mariam, votre assistante IA</p>
85
- </div>
 
 
 
86
 
87
- <div class="telegram-join-button-container">
88
- <a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" class="telegram-button">
89
- 🚀 Rejoindre le Groupe Telegram
90
- </a>
91
- </div>
92
 
93
- <div class="container">
94
- <div class="style-selection">
95
- <h3>🎨 Choisissez le style de résolution</h3>
96
- <div class="radio-group">
97
- <div class="radio-option" onclick="selectStyle('light')">
98
- <input type="radio" id="style-light" name="resolution-style" value="light">
99
- <div class="radio-content">
100
- <label class="radio-label" for="style-light">📝 Résolution Light</label>
101
- <div class="radio-description">Format simple et épuré, idéal pour une lecture rapide</div>
102
- </div>
103
  </div>
104
- <div class="radio-option" onclick="selectStyle('colorful')">
105
- <input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked>
106
- <div class="radio-content">
107
- <label class="radio-label" for="style-colorful">🌈 Résolution Colorée</label>
108
- <div class="radio-description">Format richement formaté avec couleurs et mise en page élégante</div>
 
 
 
 
 
 
 
 
 
 
 
109
  </div>
110
  </div>
111
  </div>
112
- </div>
113
-
114
- <div id="cooldown-notice" class="cooldown-notice" style="display: none;">
115
- ⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer">2:00</span> avant de pouvoir soumettre à nouveau.
116
- </div>
117
-
118
- <div id="upload-section" class="upload-section">
119
- <div class="upload-icon">📤</div>
120
- <p>Cliquez ou glissez-déposez vos images et/ou 1 fichier PDF ici</p>
121
- <input type="file" id="file-input" accept="image/*,application/pdf" multiple>
122
- <div id="file-preview-area"></div>
123
- </div>
124
-
125
- <button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer les fichiers</button>
126
- <button id="solve-button" class="button" disabled>🔍 Résoudre</button>
127
-
128
- <div id="solving-container">
129
- <div class="status" id="status">En attente de résolution...</div>
130
- <div class="telegram-notice">
131
- Votre PDF sera disponible au téléchargement ici-même et dans votre historique une fois le traitement terminé.
132
  </div>
133
- <div class="response-container" id="response-container">
134
- <div id="response"></div>
135
- <a id="download-button" class="button download-button" style="display: none;">📥 Télécharger le PDF</a>
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
- </div>
138
- </div>
139
 
140
- <div id="history-container" class="container">
141
- <h2>Historique des Tâches</h2>
142
- <ul id="history-list"></ul>
143
- <button id="clear-history-button" class="button">🗑️ Vider l'historique</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  </div>
145
 
146
  <script>
147
- document.addEventListener('DOMContentLoaded', function() {
148
- const uploadSection = document.getElementById('upload-section');
149
- const fileInput = document.getElementById('file-input');
150
- const filePreviewArea = document.getElementById('file-preview-area');
151
- const solveButton = document.getElementById('solve-button');
152
- const clearFilesButton = document.getElementById('clear-files-button');
153
- const solvingContainer = document.getElementById('solving-container');
154
- const responseContainer = document.getElementById('response-container');
155
- const responseDiv = document.getElementById('response');
156
- const statusElement = document.getElementById('status');
157
- const downloadButton = document.getElementById('download-button');
158
- const cooldownNotice = document.getElementById('cooldown-notice');
159
- const cooldownTimer = document.getElementById('cooldown-timer');
160
- const historyList = document.getElementById('history-list');
161
- const clearHistoryButton = document.getElementById('clear-history-button');
162
-
163
- let selectedFiles = [];
164
- let cooldownEndTime = 0;
165
- let cooldownInterval = null;
166
- const eventSources = {};
167
-
168
- const getHistory = () => JSON.parse(localStorage.getItem('mariamTaskHistory')) || [];
169
- const saveHistory = (history) => localStorage.setItem('mariamTaskHistory', JSON.stringify(history));
170
-
171
- function renderHistory() {
172
- historyList.innerHTML = '';
173
- const history = getHistory();
174
- if (history.length === 0) {
175
- historyList.innerHTML = '<p style="text-align:center; color:#777;">Aucune tâche dans votre historique.</p>';
176
- clearHistoryButton.style.display = 'none';
177
- return;
178
- }
179
- clearHistoryButton.style.display = 'block';
180
 
181
- history.sort((a, b) => b.timestamp - a.timestamp).forEach(task => {
182
- const li = document.createElement('li');
183
- li.classList.add('history-item');
184
- li.dataset.taskId = task.id;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- let statusText = 'En attente...';
187
- let statusClass = 'history-status-pending';
188
- if (task.status === 'completed') {
189
- statusText = 'Terminé';
190
- statusClass = 'history-status-completed';
191
- } else if (task.status === 'error') {
192
- statusText = 'Erreur';
193
- statusClass = 'history-status-error';
194
- } else if (task.status && task.status.startsWith('generating')) {
195
- statusText = 'Génération en cours...';
196
- } else if (task.status) {
197
- statusText = task.status.charAt(0).toUpperCase() + task.status.slice(1);
198
- }
199
 
200
- li.innerHTML = `
201
- <div class="history-info">
202
- <span class="history-filename">${task.filename}</span>
203
- <small class="history-status ${statusClass}">${statusText} - ${new Date(task.timestamp).toLocaleString('fr-FR')}</small>
204
- </div>
205
- <div class="history-actions" id="actions-${task.id}"></div>
206
- `;
207
- historyList.appendChild(li);
208
- updateHistoryItemActions(task);
 
 
 
 
209
  });
210
  }
211
 
212
- function updateHistoryItemActions(task) {
213
- const container = document.getElementById(`actions-${task.id}`);
214
- if (!container) return;
215
-
216
- if (task.status === 'completed' && task.download_url) {
217
- container.innerHTML = `<a href="${task.download_url}" class="button download-button">📥 Télécharger</a>`;
218
- } else if (task.status === 'error') {
219
- container.innerHTML = `<span style="color:var(--danger-color); font-weight:bold;">Échec</span>`;
220
- } else {
221
- container.innerHTML = `<span style="color:var(--primary-color); font-style:italic;">En cours...</span>`;
222
- }
223
  }
224
 
225
- function updateTaskInHistory(taskId, updates) {
226
- let history = getHistory();
227
- const taskIndex = history.findIndex(t => t.id === taskId);
228
- if (taskIndex > -1) {
229
- history[taskIndex] = { ...history[taskIndex], ...updates };
230
- saveHistory(history);
231
- renderHistory();
232
- }
233
  }
234
 
235
- function checkHistoryStatus() {
236
- getHistory().forEach(task => {
237
- if (task.status && !['completed', 'error'].includes(task.status)) {
238
- fetch(`/task/${task.id}`)
239
- .then(response => response.json())
240
- .then(data => {
241
- if (data.status && data.status !== task.status) {
242
- updateTaskInHistory(task.id, { status: data.status, download_url: data.download_url, error: data.error });
243
- }
244
- }).catch(err => console.error(`Could not check status for ${task.id}:`, err));
245
- }
246
- });
247
  }
248
 
249
- window.selectStyle = (style) => document.getElementById(`style-${style}`).checked = true;
250
-
251
- function checkCooldownOnLoad() {
252
- const savedCooldown = localStorage.getItem('mariamCooldownEndTime');
253
- if (savedCooldown && parseInt(savedCooldown) > Date.now()) {
254
- cooldownEndTime = parseInt(savedCooldown);
255
- startCooldownTimer();
256
  }
257
  }
258
-
259
- function startCooldown() {
260
- cooldownEndTime = Date.now() + 2 * 60 * 1000;
261
- localStorage.setItem('mariamCooldownEndTime', cooldownEndTime.toString());
262
- startCooldownTimer();
263
- }
264
-
265
- function startCooldownTimer() {
266
- cooldownNotice.style.display = 'block';
267
- solveButton.disabled = true;
268
- if (cooldownInterval) clearInterval(cooldownInterval);
269
- cooldownInterval = setInterval(() => {
270
- const remaining = Math.max(0, cooldownEndTime - Date.now());
271
- if (remaining <= 0) {
272
- clearInterval(cooldownInterval);
273
- cooldownNotice.style.display = 'none';
274
- updateButtonsState();
275
- return;
276
- }
277
- const minutes = Math.floor(remaining / 60000);
278
- const seconds = Math.floor((remaining % 60000) / 1000);
279
- cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
280
- }, 1000);
281
- }
282
-
283
- const isCooldownActive = () => Date.now() < cooldownEndTime;
284
 
285
- const handleFileSelection = (files) => {
286
  const newFiles = Array.from(files);
287
- let pdfSelected = selectedFiles.some(f => f.type === 'application/pdf');
 
288
  newFiles.forEach(file => {
289
  if (file.type.startsWith('image/')) {
290
- if (!selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) selectedFiles.push(file);
 
 
291
  } else if (file.type === 'application/pdf') {
292
  if (!pdfSelected) {
293
- selectedFiles = selectedFiles.filter(f => f.type !== 'application/pdf');
294
- selectedFiles.push(file);
295
  pdfSelected = true;
296
  }
297
  }
298
  });
299
- updateFilePreviews();
300
- updateButtonsState();
301
- };
302
-
303
- uploadSection.addEventListener('click', () => fileInput.click());
304
- uploadSection.addEventListener('dragover', (e) => { e.preventDefault(); uploadSection.classList.add('hover'); });
305
- uploadSection.addEventListener('dragleave', (e) => uploadSection.classList.remove('hover'));
306
- uploadSection.addEventListener('drop', (e) => { e.preventDefault(); uploadSection.classList.remove('hover'); if (e.dataTransfer.files.length) handleFileSelection(e.dataTransfer.files); });
307
- fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFileSelection(e.target.files); });
308
-
309
- function updateFilePreviews() {
310
- filePreviewArea.innerHTML = '';
311
- if (selectedFiles.length === 0) return;
312
- selectedFiles.forEach(file => {
313
  const item = document.createElement('div');
314
  item.className = 'preview-item';
315
- const name = document.createElement('span');
316
- name.textContent = file.name.length > 15 ? file.name.substring(0, 12) + "..." : file.name;
 
 
 
317
  if (file.type.startsWith('image/')) {
318
  const img = document.createElement('img');
319
  img.src = URL.createObjectURL(file);
@@ -321,42 +848,45 @@
321
  } else {
322
  item.innerHTML = '<div class="pdf-icon">📄</div>';
323
  }
324
- item.appendChild(name);
325
- filePreviewArea.appendChild(item);
 
 
 
 
 
326
  });
327
  }
328
-
329
- function updateButtonsState() {
330
- const hasFiles = selectedFiles.length > 0;
331
- solveButton.disabled = !hasFiles || isCooldownActive();
332
- solveButton.textContent = hasFiles ? `🔍 Résoudre (${selectedFiles.length} fichier(s))` : '🔍 Résoudre';
333
- clearFilesButton.style.display = hasFiles ? 'block' : 'none';
334
- }
335
-
336
- clearFilesButton.addEventListener('click', () => {
337
- selectedFiles = [];
338
- fileInput.value = '';
339
- updateFilePreviews();
340
- updateButtonsState();
341
- solvingContainer.style.display = 'none';
342
- });
343
-
344
- solveButton.addEventListener('click', () => {
345
- if (selectedFiles.length === 0 || isCooldownActive()) return;
346
 
347
- startCooldown();
348
- solveButton.disabled = true;
349
- solveButton.textContent = '⏳ Traitement...';
 
350
 
351
- solvingContainer.style.display = 'block';
352
- responseContainer.style.display = 'none';
353
- downloadButton.style.display = 'none';
354
- statusElement.className = 'status';
355
- statusElement.textContent = 'Préparation...';
356
- responseDiv.innerHTML = '';
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  const formData = new FormData();
359
- selectedFiles.forEach(file => formData.append('user_files', file));
360
  formData.append('style', document.querySelector('input[name="resolution-style"]:checked').value);
361
 
362
  fetch('/solve', { method: 'POST', body: formData })
@@ -366,70 +896,311 @@
366
  })
367
  .then(data => {
368
  const { task_id, first_filename } = data;
369
-
370
- let history = getHistory();
371
- history.push({ id: task_id, filename: first_filename, status: 'pending', timestamp: Date.now() });
372
- saveHistory(history);
373
- renderHistory();
374
 
375
- statusElement.textContent = 'Traitement en arrière-plan (ID: ' + task_id.substring(0, 8) + '...)';
376
- listenToTask(task_id);
 
 
 
 
 
 
 
 
 
 
377
  })
378
- .catch(error => handleError(error.message));
379
- });
380
 
381
- function listenToTask(taskId) {
382
- if (eventSources[taskId]) eventSources[taskId].close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
 
384
  const eventSource = new EventSource('/stream/' + taskId);
385
- eventSources[taskId] = eventSource;
386
 
387
- eventSource.onmessage = function(event) {
388
  const data = JSON.parse(event.data);
389
 
390
- updateTaskInHistory(taskId, { status: data.status, download_url: data.download_url, error: data.error });
391
-
392
- statusElement.textContent = `Statut: ${data.status}`;
 
 
393
 
394
  if (data.status === 'completed') {
395
- statusElement.className = 'status completed';
396
- statusElement.textContent = 'Traitement terminé avec succès ! 🎉';
397
- responseDiv.innerHTML = `<p style="color: #2ecc71; text-align: center;">Votre PDF est prêt.</p>`;
398
- downloadButton.href = data.download_url;
399
- downloadButton.style.display = 'block';
400
- responseContainer.style.display = 'block';
401
  eventSource.close();
 
 
 
 
 
402
  } else if (data.status === 'error') {
403
- handleError(data.error || 'Une erreur inattendue est survenue.', taskId);
404
  eventSource.close();
 
 
405
  }
406
  };
407
 
408
- eventSource.onerror = function() {
409
  eventSource.close();
410
- checkHistoryStatus();
411
  };
412
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
- function handleError(errorMessage, taskId = null) {
415
- statusElement.className = 'status error';
416
- statusElement.textContent = 'Erreur:';
417
- responseDiv.innerHTML = `<p style="color:red;">${errorMessage}</p>`;
418
- responseContainer.style.display = 'block';
419
- downloadButton.style.display = 'none';
420
- if (taskId) updateTaskInHistory(taskId, { status: 'error', error: errorMessage });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
 
423
- clearHistoryButton.addEventListener('click', () => {
424
- if(confirm("Êtes-vous sûr de vouloir vider tout l'historique ? Cette action est irréversible.")) {
425
  localStorage.removeItem('mariamTaskHistory');
426
- renderHistory();
 
427
  }
428
- });
429
-
430
- checkCooldownOnLoad();
431
- renderHistory();
432
- checkHistoryStatus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  });
434
  </script>
435
  </body>
 
6
  <title>Résolveur d'Images & PDF - Mariam</title>
7
  <style>
8
  :root {
9
+ --primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ --primary-solid: #667eea;
11
+ --primary-dark: #5a6fd8;
12
+ --secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
13
+ --secondary-solid: #f093fb;
14
+ --success: #10dc60;
15
+ --success-bg: rgba(16, 220, 96, 0.1);
16
+ --danger: #f04141;
17
+ --danger-bg: rgba(240, 65, 65, 0.1);
18
+ --warning: #ffce00;
19
+ --warning-bg: rgba(255, 206, 0, 0.1);
20
+ --background: #f8fafc;
21
+ --surface: #ffffff;
22
+ --surface-elevated: #ffffff;
23
+ --text-primary: #1a202c;
24
+ --text-secondary: #718096;
25
+ --text-muted: #a0aec0;
26
+ --border: #e2e8f0;
27
+ --border-light: #f1f5f9;
28
+ --shadow-sm: 0 2px 4px rgba(0,0,0,0.05);
29
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.1);
30
+ --shadow-lg: 0 8px 25px rgba(0,0,0,0.15);
31
+ --radius: 16px;
32
+ --radius-sm: 12px;
33
+ --radius-lg: 24px;
34
+ --spacing: 1rem;
35
  }
36
+
37
+ * {
38
+ box-sizing: border-box;
39
+ margin: 0;
40
+ padding: 0;
41
+ }
42
+
43
  body {
44
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ background: var(--background);
46
+ color: var(--text-primary);
47
+ line-height: 1.6;
48
+ -webkit-font-smoothing: antialiased;
49
+ -moz-osx-font-smoothing: grayscale;
50
+ padding: 0;
51
+ margin: 0;
52
+ min-height: 100vh;
53
+ }
54
+
55
+ .app-container {
56
+ max-width: 100%;
57
+ min-height: 100vh;
58
+ display: flex;
59
+ flex-direction: column;
60
+ }
61
+
62
+ /* Header */
63
+ .header {
64
+ background: var(--primary);
65
+ color: white;
66
+ text-align: center;
67
+ padding: 2rem 1rem 1.5rem;
68
+ position: relative;
69
+ overflow: hidden;
70
+ }
71
+
72
+ .header::before {
73
+ content: '';
74
+ position: absolute;
75
+ top: 0;
76
+ left: 0;
77
+ right: 0;
78
+ bottom: 0;
79
+ background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E") repeat;
80
+ opacity: 0.3;
81
+ }
82
+
83
+ .header-content {
84
+ position: relative;
85
+ z-index: 2;
86
+ }
87
+
88
+ .header h1 {
89
+ font-size: 1.75rem;
90
+ font-weight: 800;
91
+ margin-bottom: 0.5rem;
92
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
93
+ }
94
+
95
+ .header .subtitle {
96
+ font-size: 1rem;
97
+ opacity: 0.9;
98
+ font-weight: 500;
99
+ }
100
+
101
+ /* Main Content */
102
+ .main-content {
103
+ flex: 1;
104
+ padding: 1rem;
105
+ padding-bottom: 2rem;
106
+ }
107
+
108
+ .card {
109
+ background: var(--surface);
110
+ border-radius: var(--radius);
111
+ box-shadow: var(--shadow-md);
112
+ margin-bottom: 1rem;
113
+ overflow: hidden;
114
+ border: 1px solid var(--border-light);
115
+ }
116
+
117
+ .card-header {
118
+ padding: 1.5rem 1.5rem 1rem;
119
+ border-bottom: 1px solid var(--border-light);
120
+ }
121
+
122
+ .card-body {
123
+ padding: 1.5rem;
124
+ }
125
+
126
+ .card-title {
127
+ font-size: 1.1rem;
128
+ font-weight: 700;
129
+ color: var(--text-primary);
130
+ margin-bottom: 1rem;
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 0.5rem;
134
+ }
135
+
136
+ /* Telegram Button */
137
+ .telegram-section {
138
+ margin-bottom: 1rem;
139
+ }
140
+
141
  .telegram-button {
142
+ display: block;
143
+ background: linear-gradient(135deg, #0088cc 0%, #005599 100%);
144
+ color: white;
145
+ padding: 1rem 1.5rem;
146
+ border-radius: var(--radius);
147
+ text-decoration: none;
148
+ font-weight: 600;
149
+ text-align: center;
150
+ box-shadow: var(--shadow-md);
151
+ transition: all 0.3s ease;
152
+ position: relative;
153
+ overflow: hidden;
154
+ }
155
+
156
+ .telegram-button::before {
157
+ content: '';
158
+ position: absolute;
159
+ top: 0;
160
+ left: -100%;
161
+ width: 100%;
162
+ height: 100%;
163
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
164
+ transition: left 0.5s;
165
+ }
166
+
167
+ .telegram-button:hover::before {
168
+ left: 100%;
169
+ }
170
+
171
+ .telegram-button:active {
172
+ transform: scale(0.98);
173
+ }
174
+
175
+ /* Style Selection */
176
+ .style-options {
177
+ display: grid;
178
+ gap: 0.75rem;
179
+ }
180
+
181
+ .style-option {
182
+ position: relative;
183
+ cursor: pointer;
184
+ border-radius: var(--radius-sm);
185
+ overflow: hidden;
186
+ transition: all 0.3s ease;
187
+ }
188
+
189
+ .style-option input[type="radio"] {
190
+ position: absolute;
191
+ opacity: 0;
192
+ width: 100%;
193
+ height: 100%;
194
+ cursor: pointer;
195
+ z-index: 2;
196
+ }
197
+
198
+ .style-option-content {
199
+ padding: 1rem;
200
+ border: 2px solid var(--border);
201
+ border-radius: var(--radius-sm);
202
+ background: var(--surface);
203
+ transition: all 0.3s ease;
204
+ position: relative;
205
+ }
206
+
207
+ .style-option input[type="radio"]:checked + .style-option-content {
208
+ border-color: var(--primary-solid);
209
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
210
+ transform: translateY(-2px);
211
+ box-shadow: var(--shadow-lg);
212
+ }
213
+
214
+ .style-option input[type="radio"]:checked + .style-option-content::after {
215
+ content: '✓';
216
+ position: absolute;
217
+ top: 0.75rem;
218
+ right: 0.75rem;
219
+ width: 24px;
220
+ height: 24px;
221
+ background: var(--primary-solid);
222
+ color: white;
223
+ border-radius: 50%;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ font-size: 0.8rem;
228
+ font-weight: bold;
229
+ }
230
+
231
+ .style-label {
232
+ font-weight: 600;
233
+ color: var(--text-primary);
234
+ margin-bottom: 0.25rem;
235
+ display: block;
236
+ }
237
+
238
+ .style-description {
239
+ font-size: 0.9rem;
240
+ color: var(--text-secondary);
241
+ }
242
+
243
+ /* Upload Section */
244
+ .upload-area {
245
+ border: 2px dashed var(--border);
246
+ border-radius: var(--radius);
247
+ padding: 2rem 1rem;
248
+ text-align: center;
249
+ background: linear-gradient(135deg, var(--surface) 0%, #f8fafc 100%);
250
+ transition: all 0.3s ease;
251
+ cursor: pointer;
252
+ position: relative;
253
+ overflow: hidden;
254
+ }
255
+
256
+ .upload-area.dragover {
257
+ border-color: var(--primary-solid);
258
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
259
+ transform: scale(1.02);
260
+ }
261
+
262
+ .upload-icon {
263
+ font-size: 3rem;
264
+ margin-bottom: 1rem;
265
+ background: var(--primary);
266
+ -webkit-background-clip: text;
267
+ -webkit-text-fill-color: transparent;
268
+ background-clip: text;
269
+ }
270
+
271
+ .upload-text {
272
+ font-weight: 600;
273
+ color: var(--text-primary);
274
+ margin-bottom: 0.5rem;
275
+ }
276
+
277
+ .upload-hint {
278
+ font-size: 0.9rem;
279
+ color: var(--text-secondary);
280
+ }
281
+
282
+ #file-input {
283
+ display: none;
284
+ }
285
+
286
+ /* File Previews */
287
+ .file-previews {
288
+ margin-top: 1rem;
289
+ display: grid;
290
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
291
+ gap: 0.75rem;
292
+ max-height: 200px;
293
+ overflow-y: auto;
294
+ }
295
+
296
+ .preview-item {
297
+ display: flex;
298
+ flex-direction: column;
299
+ align-items: center;
300
+ gap: 0.5rem;
301
+ padding: 0.75rem;
302
+ background: var(--surface-elevated);
303
+ border-radius: var(--radius-sm);
304
+ border: 1px solid var(--border);
305
+ box-shadow: var(--shadow-sm);
306
+ position: relative;
307
+ animation: fadeInUp 0.3s ease;
308
+ }
309
+
310
+ @keyframes fadeInUp {
311
+ from {
312
+ opacity: 0;
313
+ transform: translateY(20px);
314
+ }
315
+ to {
316
+ opacity: 1;
317
+ transform: translateY(0);
318
+ }
319
+ }
320
+
321
+ .preview-item img {
322
+ width: 60px;
323
+ height: 60px;
324
+ object-fit: cover;
325
+ border-radius: var(--radius-sm);
326
+ }
327
+
328
+ .preview-item .pdf-icon {
329
+ font-size: 2.5rem;
330
+ color: var(--danger);
331
+ }
332
+
333
+ .preview-filename {
334
+ font-size: 0.75rem;
335
+ color: var(--text-secondary);
336
+ text-align: center;
337
+ word-break: break-all;
338
+ line-height: 1.2;
339
+ }
340
+
341
+ /* Buttons */
342
+ .btn {
343
+ padding: 1rem 1.5rem;
344
+ border: none;
345
+ border-radius: var(--radius);
346
+ font-size: 1rem;
347
+ font-weight: 600;
348
+ cursor: pointer;
349
+ transition: all 0.3s ease;
350
+ text-decoration: none;
351
+ display: inline-flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ gap: 0.5rem;
355
+ min-height: 56px;
356
+ position: relative;
357
+ overflow: hidden;
358
+ }
359
+
360
+ .btn:disabled {
361
+ opacity: 0.6;
362
+ cursor: not-allowed;
363
+ transform: none !important;
364
+ }
365
+
366
+ .btn-primary {
367
+ background: var(--primary);
368
+ color: white;
369
+ box-shadow: var(--shadow-md);
370
+ }
371
+
372
+ .btn-primary:not(:disabled):active {
373
+ transform: translateY(1px);
374
+ }
375
+
376
+ .btn-danger {
377
+ background: var(--danger);
378
+ color: white;
379
+ }
380
+
381
+ .btn-success {
382
+ background: var(--success);
383
+ color: white;
384
+ }
385
+
386
+ .btn-block {
387
+ width: 100%;
388
+ margin-bottom: 0.75rem;
389
+ }
390
+
391
+ .btn::before {
392
+ content: '';
393
+ position: absolute;
394
+ top: 0;
395
+ left: -100%;
396
+ width: 100%;
397
+ height: 100%;
398
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
399
+ transition: left 0.5s;
400
+ }
401
+
402
+ .btn:hover::before {
403
+ left: 100%;
404
+ }
405
+
406
+ /* Cooldown Notice */
407
+ .cooldown-notice {
408
+ background: var(--warning-bg);
409
+ border: 1px solid var(--warning);
410
+ border-radius: var(--radius);
411
+ padding: 1rem;
412
+ margin-bottom: 1rem;
413
+ text-align: center;
414
+ display: none;
415
+ }
416
+
417
+ .cooldown-timer {
418
+ font-size: 1.2rem;
419
+ font-weight: bold;
420
+ color: var(--warning);
421
+ }
422
+
423
+ /* Status Container */
424
+ .status-container {
425
+ display: none;
426
+ animation: slideDown 0.3s ease;
427
+ }
428
+
429
+ @keyframes slideDown {
430
+ from {
431
+ opacity: 0;
432
+ transform: translateY(-20px);
433
+ }
434
+ to {
435
+ opacity: 1;
436
+ transform: translateY(0);
437
+ }
438
+ }
439
+
440
+ .status-content {
441
+ text-align: center;
442
+ padding: 1.5rem;
443
+ }
444
+
445
+ .status-icon {
446
+ font-size: 3rem;
447
+ margin-bottom: 1rem;
448
+ }
449
+
450
+ .status-text {
451
+ font-size: 1.1rem;
452
+ font-weight: 600;
453
+ margin-bottom: 1rem;
454
+ }
455
+
456
+ .status-description {
457
+ color: var(--text-secondary);
458
+ font-size: 0.9rem;
459
+ margin-bottom: 1rem;
460
+ }
461
+
462
+ .status.success .status-icon { color: var(--success); }
463
+ .status.error .status-icon { color: var(--danger); }
464
+ .status.processing .status-icon { color: var(--primary-solid); }
465
+
466
+ .status.success .status-text { color: var(--success); }
467
+ .status.error .status-text { color: var(--danger); }
468
+ .status.processing .status-text { color: var(--primary-solid); }
469
+
470
+ /* Loading Animation */
471
+ .loading-spinner {
472
+ display: inline-block;
473
+ width: 20px;
474
+ height: 20px;
475
+ border: 2px solid rgba(255,255,255,0.3);
476
+ border-radius: 50%;
477
+ border-top-color: white;
478
+ animation: spin 1s ease-in-out infinite;
479
+ }
480
+
481
+ @keyframes spin {
482
+ to { transform: rotate(360deg); }
483
+ }
484
+
485
+ /* History */
486
+ .history-item {
487
+ display: flex;
488
+ align-items: center;
489
+ padding: 1rem;
490
+ background: var(--surface);
491
+ border-radius: var(--radius-sm);
492
+ border: 1px solid var(--border-light);
493
+ margin-bottom: 0.75rem;
494
+ transition: all 0.3s ease;
495
+ }
496
+
497
+ .history-item:active {
498
+ transform: scale(0.98);
499
+ box-shadow: var(--shadow-lg);
500
+ }
501
+
502
+ .history-icon {
503
+ width: 40px;
504
+ height: 40px;
505
+ border-radius: 50%;
506
+ display: flex;
507
+ align-items: center;
508
+ justify-content: center;
509
+ margin-right: 1rem;
510
+ font-size: 1.2rem;
511
+ }
512
+
513
+ .history-icon.success {
514
+ background: var(--success-bg);
515
+ color: var(--success);
516
+ }
517
+
518
+ .history-icon.error {
519
+ background: var(--danger-bg);
520
+ color: var(--danger);
521
+ }
522
+
523
+ .history-icon.pending {
524
+ background: var(--warning-bg);
525
+ color: var(--warning);
526
+ }
527
+
528
+ .history-info {
529
+ flex: 1;
530
+ }
531
+
532
+ .history-filename {
533
+ font-weight: 600;
534
+ color: var(--text-primary);
535
+ margin-bottom: 0.25rem;
536
+ }
537
+
538
+ .history-meta {
539
+ font-size: 0.8rem;
540
+ color: var(--text-muted);
541
+ }
542
+
543
+ .history-actions {
544
+ margin-left: 0.5rem;
545
+ }
546
+
547
+ .btn-sm {
548
+ padding: 0.5rem 1rem;
549
+ min-height: 36px;
550
+ font-size: 0.9rem;
551
+ }
552
+
553
+ /* Responsive Improvements */
554
+ @media (max-width: 480px) {
555
+ .main-content {
556
+ padding: 0.75rem;
557
+ }
558
+
559
+ .card-body,
560
+ .card-header {
561
+ padding: 1rem;
562
+ }
563
+
564
+ .header {
565
+ padding: 1.5rem 1rem 1rem;
566
+ }
567
+
568
+ .header h1 {
569
+ font-size: 1.5rem;
570
+ }
571
+
572
+ .file-previews {
573
+ grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
574
+ }
575
+ }
576
+
577
+ /* Toast Notifications */
578
+ .toast {
579
+ position: fixed;
580
+ top: 20px;
581
+ left: 50%;
582
+ transform: translateX(-50%);
583
+ background: var(--surface-elevated);
584
+ color: var(--text-primary);
585
+ padding: 1rem 1.5rem;
586
+ border-radius: var(--radius);
587
+ box-shadow: var(--shadow-lg);
588
+ border: 1px solid var(--border);
589
+ z-index: 1000;
590
+ opacity: 0;
591
+ transition: all 0.3s ease;
592
+ }
593
+
594
+ .toast.show {
595
+ opacity: 1;
596
+ transform: translateX(-50%) translateY(0);
597
+ }
598
+
599
+ .toast.success {
600
+ background: var(--success-bg);
601
+ border-color: var(--success);
602
+ color: var(--success);
603
+ }
604
+
605
+ .toast.error {
606
+ background: var(--danger-bg);
607
+ border-color: var(--danger);
608
+ color: var(--danger);
609
+ }
610
+
611
+ /* Smooth Scrolling */
612
+ html {
613
+ scroll-behavior: smooth;
614
+ }
615
+
616
+ /* Focus States */
617
+ .btn:focus,
618
+ .style-option input:focus + .style-option-content,
619
+ .upload-area:focus {
620
+ outline: 2px solid var(--primary-solid);
621
+ outline-offset: 2px;
622
  }
623
  </style>
624
  </head>
625
  <body>
626
+ <div class="app-container">
627
+ <header class="header">
628
+ <div class="header-content">
629
+ <h1>🧠 Science avec Mariam</h1>
630
+ <p class="subtitle">Votre assistante IA pour Math, Physique & Chimie</p>
631
+ </div>
632
+ </header>
633
 
634
+ <main class="main-content">
635
+ <!-- Telegram Section -->
636
+
 
 
637
 
638
+ <!-- Style Selection -->
639
+ <div class="card">
640
+ <div class="card-header">
641
+ <h3 class="card-title">🎨 Style de résolution</h3>
 
 
 
 
 
 
642
  </div>
643
+ <div class="card-body">
644
+ <div class="style-options">
645
+ <div class="style-option">
646
+ <input type="radio" id="style-light" name="resolution-style" value="light">
647
+ <div class="style-option-content">
648
+ <div class="style-label">📝 Résolution Light</div>
649
+ <div class="style-description">Format simple et épuré pour une lecture rapide</div>
650
+ </div>
651
+ </div>
652
+ <div class="style-option">
653
+ <input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked>
654
+ <div class="style-option-content">
655
+ <div class="style-label">🌈 Résolution Colorée</div>
656
+ <div class="style-description">Format richement formaté avec couleurs et mise en page élégante</div>
657
+ </div>
658
+ </div>
659
  </div>
660
  </div>
661
  </div>
662
+
663
+ <!-- Cooldown Notice -->
664
+ <div id="cooldown-notice" class="cooldown-notice">
665
+ ⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer">2:00</span> avant de pouvoir soumettre à nouveau.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  </div>
667
+
668
+ <!-- Upload Section -->
669
+ <div class="card">
670
+ <div class="card-header">
671
+ <h3 class="card-title">📤 Vos fichiers</h3>
672
+ </div>
673
+ <div class="card-body">
674
+ <div id="upload-area" class="upload-area" tabindex="0">
675
+ <div class="upload-icon">📱</div>
676
+ <div class="upload-text">Toucher pour sélectionner</div>
677
+ <div class="upload-hint">Images et PDF acceptés</div>
678
+ <input type="file" id="file-input" accept="image/*,application/pdf" multiple>
679
+ </div>
680
+ <div id="file-previews" class="file-previews"></div>
681
+ </div>
682
  </div>
 
 
683
 
684
+ <!-- Action Buttons -->
685
+ <button id="clear-files-btn" class="btn btn-danger btn-block" style="display: none;">
686
+ 🗑️ Effacer les fichiers
687
+ </button>
688
+ <button id="solve-btn" class="btn btn-primary btn-block" disabled>
689
+ 🔍 Résoudre
690
+ </button>
691
+
692
+ <!-- Status Container -->
693
+ <div id="status-container" class="card status-container">
694
+ <div class="card-body">
695
+ <div class="status-content">
696
+ <div id="status-icon" class="status-icon">⏳</div>
697
+ <div id="status-text" class="status-text">Traitement en cours...</div>
698
+ <div id="status-description" class="status-description">
699
+ Votre PDF sera disponible ici une fois le traitement terminé.
700
+ </div>
701
+ <div id="response-container" style="display: none;">
702
+ <div id="response"></div>
703
+ <a id="download-btn" class="btn btn-success btn-block" style="display: none;">
704
+ 📥 Télécharger le PDF
705
+ </a>
706
+ </div>
707
+ </div>
708
+ </div>
709
+ </div>
710
+
711
+ <!-- History -->
712
+ <div class="card">
713
+ <div class="card-header">
714
+ <h3 class="card-title">📋 Historique</h3>
715
+ </div>
716
+ <div class="card-body">
717
+ <div id="history-list"></div>
718
+ <button id="clear-history-btn" class="btn btn-danger btn-block">
719
+ 🗑️ Vider l'historique
720
+ </button>
721
+ </div>
722
+ </div>
723
+ </main>
724
  </div>
725
 
726
  <script>
727
+ class MariamApp {
728
+ constructor() {
729
+ this.selectedFiles = [];
730
+ this.cooldownEndTime = 0;
731
+ this.cooldownInterval = null;
732
+ this.eventSources = {};
733
+
734
+ this.initElements();
735
+ this.attachEventListeners();
736
+ this.checkCooldownOnLoad();
737
+ this.renderHistory();
738
+ this.checkHistoryStatus();
739
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
+ initElements() {
742
+ this.uploadArea = document.getElementById('upload-area');
743
+ this.fileInput = document.getElementById('file-input');
744
+ this.filePreviews = document.getElementById('file-previews');
745
+ this.solveBtn = document.getElementById('solve-btn');
746
+ this.clearFilesBtn = document.getElementById('clear-files-btn');
747
+ this.statusContainer = document.getElementById('status-container');
748
+ this.statusIcon = document.getElementById('status-icon');
749
+ this.statusText = document.getElementById('status-text');
750
+ this.statusDescription = document.getElementById('status-description');
751
+ this.responseContainer = document.getElementById('response-container');
752
+ this.responseDiv = document.getElementById('response');
753
+ this.downloadBtn = document.getElementById('download-btn');
754
+ this.cooldownNotice = document.getElementById('cooldown-notice');
755
+ this.cooldownTimer = document.getElementById('cooldown-timer');
756
+ this.historyList = document.getElementById('history-list');
757
+ this.clearHistoryBtn = document.getElementById('clear-history-btn');
758
+ }
759
 
760
+ attachEventListeners() {
761
+ // File upload
762
+ this.uploadArea.addEventListener('click', () => this.fileInput.click());
763
+ this.uploadArea.addEventListener('dragover', this.handleDragOver.bind(this));
764
+ this.uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this));
765
+ this.uploadArea.addEventListener('drop', this.handleDrop.bind(this));
766
+ this.fileInput.addEventListener('change', this.handleFileSelect.bind(this));
 
 
 
 
 
 
767
 
768
+ // Buttons
769
+ this.clearFilesBtn.addEventListener('click', this.clearFiles.bind(this));
770
+ this.solveBtn.addEventListener('click', this.solve.bind(this));
771
+ this.clearHistoryBtn.addEventListener('click', this.clearHistory.bind(this));
772
+
773
+ // Style selection
774
+ document.querySelectorAll('input[name="resolution-style"]').forEach(input => {
775
+ input.addEventListener('change', () => {
776
+ // Add haptic feedback on mobile
777
+ if (navigator.vibrate) {
778
+ navigator.vibrate(50);
779
+ }
780
+ });
781
  });
782
  }
783
 
784
+ handleDragOver(e) {
785
+ e.preventDefault();
786
+ this.uploadArea.classList.add('dragover');
 
 
 
 
 
 
 
 
787
  }
788
 
789
+ handleDragLeave(e) {
790
+ e.preventDefault();
791
+ this.uploadArea.classList.remove('dragover');
 
 
 
 
 
792
  }
793
 
794
+ handleDrop(e) {
795
+ e.preventDefault();
796
+ this.uploadArea.classList.remove('dragover');
797
+ if (e.dataTransfer.files.length) {
798
+ this.handleFileSelection(e.dataTransfer.files);
799
+ }
 
 
 
 
 
 
800
  }
801
 
802
+ handleFileSelect(e) {
803
+ if (e.target.files.length) {
804
+ this.handleFileSelection(e.target.files);
 
 
 
 
805
  }
806
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
 
808
+ handleFileSelection(files) {
809
  const newFiles = Array.from(files);
810
+ let pdfSelected = this.selectedFiles.some(f => f.type === 'application/pdf');
811
+
812
  newFiles.forEach(file => {
813
  if (file.type.startsWith('image/')) {
814
+ if (!this.selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) {
815
+ this.selectedFiles.push(file);
816
+ }
817
  } else if (file.type === 'application/pdf') {
818
  if (!pdfSelected) {
819
+ this.selectedFiles = this.selectedFiles.filter(f => f.type !== 'application/pdf');
820
+ this.selectedFiles.push(file);
821
  pdfSelected = true;
822
  }
823
  }
824
  });
825
+
826
+ this.updateFilePreviews();
827
+ this.updateButtonsState();
828
+ this.showToast('Fichiers ajoutés avec succès', 'success');
829
+ }
830
+
831
+ updateFilePreviews() {
832
+ this.filePreviews.innerHTML = '';
833
+
834
+ if (this.selectedFiles.length === 0) return;
835
+
836
+ this.selectedFiles.forEach(file => {
 
 
837
  const item = document.createElement('div');
838
  item.className = 'preview-item';
839
+
840
+ const filename = file.name.length > 10 ?
841
+ file.name.substring(0, 8) + "..." :
842
+ file.name;
843
+
844
  if (file.type.startsWith('image/')) {
845
  const img = document.createElement('img');
846
  img.src = URL.createObjectURL(file);
 
848
  } else {
849
  item.innerHTML = '<div class="pdf-icon">📄</div>';
850
  }
851
+
852
+ const filenameEl = document.createElement('div');
853
+ filenameEl.className = 'preview-filename';
854
+ filenameEl.textContent = filename;
855
+ item.appendChild(filenameEl);
856
+
857
+ this.filePreviews.appendChild(item);
858
  });
859
  }
860
+
861
+ updateButtonsState() {
862
+ const hasFiles = this.selectedFiles.length > 0;
863
+ const isCooldown = this.isCooldownActive();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
 
865
+ this.solveBtn.disabled = !hasFiles || isCooldown;
866
+ this.solveBtn.innerHTML = hasFiles ?
867
+ `🔍 Résoudre (${this.selectedFiles.length})` :
868
+ '🔍 Résoudre';
869
 
870
+ this.clearFilesBtn.style.display = hasFiles ? 'block' : 'none';
871
+ }
 
 
 
 
872
 
873
+ clearFiles() {
874
+ this.selectedFiles = [];
875
+ this.fileInput.value = '';
876
+ this.updateFilePreviews();
877
+ this.updateButtonsState();
878
+ this.statusContainer.style.display = 'none';
879
+ this.showToast('Fichiers effacés', 'success');
880
+ }
881
+
882
+ solve() {
883
+ if (this.selectedFiles.length === 0 || this.isCooldownActive()) return;
884
+
885
+ this.startCooldown();
886
+ this.showSolvingStatus();
887
+
888
  const formData = new FormData();
889
+ this.selectedFiles.forEach(file => formData.append('user_files', file));
890
  formData.append('style', document.querySelector('input[name="resolution-style"]:checked').value);
891
 
892
  fetch('/solve', { method: 'POST', body: formData })
 
896
  })
897
  .then(data => {
898
  const { task_id, first_filename } = data;
 
 
 
 
 
899
 
900
+ let history = this.getHistory();
901
+ history.push({
902
+ id: task_id,
903
+ filename: first_filename,
904
+ status: 'pending',
905
+ timestamp: Date.now()
906
+ });
907
+ this.saveHistory(history);
908
+ this.renderHistory();
909
+
910
+ this.updateSolvingStatus('Traitement en arrière-plan...', 'processing');
911
+ this.listenToTask(task_id);
912
  })
913
+ .catch(error => this.handleError(error.message));
914
+ }
915
 
916
+ showSolvingStatus() {
917
+ this.statusContainer.style.display = 'block';
918
+ this.statusContainer.className = 'card status-container';
919
+ this.statusIcon.textContent = '⏳';
920
+ this.statusText.textContent = 'Préparation...';
921
+ this.statusDescription.textContent = 'Envoi de vos fichiers en cours...';
922
+ this.responseContainer.style.display = 'none';
923
+ this.downloadBtn.style.display = 'none';
924
+
925
+ this.solveBtn.disabled = true;
926
+ this.solveBtn.innerHTML = '<span class="loading-spinner"></span> Traitement...';
927
+ }
928
+
929
+ updateSolvingStatus(text, type = 'processing') {
930
+ this.statusContainer.className = `card status-container status ${type}`;
931
+ this.statusText.textContent = text;
932
+
933
+ switch(type) {
934
+ case 'success':
935
+ this.statusIcon.textContent = '✅';
936
+ this.statusDescription.textContent = 'Votre PDF est prêt au téléchargement !';
937
+ break;
938
+ case 'error':
939
+ this.statusIcon.textContent = '❌';
940
+ this.statusDescription.textContent = 'Une erreur est survenue lors du traitement.';
941
+ break;
942
+ case 'processing':
943
+ this.statusIcon.textContent = '⏳';
944
+ this.statusDescription.textContent = 'Traitement en cours, veuillez patienter...';
945
+ break;
946
+ }
947
+ }
948
+
949
+ listenToTask(taskId) {
950
+ if (this.eventSources[taskId]) this.eventSources[taskId].close();
951
 
952
  const eventSource = new EventSource('/stream/' + taskId);
953
+ this.eventSources[taskId] = eventSource;
954
 
955
+ eventSource.onmessage = (event) => {
956
  const data = JSON.parse(event.data);
957
 
958
+ this.updateTaskInHistory(taskId, {
959
+ status: data.status,
960
+ download_url: data.download_url,
961
+ error: data.error
962
+ });
963
 
964
  if (data.status === 'completed') {
965
+ this.updateSolvingStatus('Traitement terminé ! 🎉', 'success');
966
+ this.responseDiv.innerHTML = '<p style="color: #10dc60; text-align: center; font-weight: 600;">Votre PDF est prêt.</p>';
967
+ this.downloadBtn.href = data.download_url;
968
+ this.downloadBtn.style.display = 'block';
969
+ this.responseContainer.style.display = 'block';
970
+ this.showToast('PDF généré avec succès !', 'success');
971
  eventSource.close();
972
+
973
+ // Haptic feedback on mobile
974
+ if (navigator.vibrate) {
975
+ navigator.vibrate([100, 30, 100, 30, 100]);
976
+ }
977
  } else if (data.status === 'error') {
978
+ this.handleError(data.error || 'Une erreur inattendue est survenue.', taskId);
979
  eventSource.close();
980
+ } else {
981
+ this.updateSolvingStatus(`Statut: ${data.status}`, 'processing');
982
  }
983
  };
984
 
985
+ eventSource.onerror = () => {
986
  eventSource.close();
987
+ this.checkHistoryStatus();
988
  };
989
  }
990
+
991
+ handleError(errorMessage, taskId = null) {
992
+ this.updateSolvingStatus('Erreur de traitement', 'error');
993
+ this.responseDiv.innerHTML = `<p style="color: #f04141; text-align: center; font-weight: 600;">${errorMessage}</p>`;
994
+ this.responseContainer.style.display = 'block';
995
+ this.downloadBtn.style.display = 'none';
996
+ this.showToast('Erreur: ' + errorMessage, 'error');
997
+
998
+ if (taskId) {
999
+ this.updateTaskInHistory(taskId, { status: 'error', error: errorMessage });
1000
+ }
1001
+ }
1002
+
1003
+ // Cooldown Management
1004
+ checkCooldownOnLoad() {
1005
+ const savedCooldown = localStorage.getItem('mariamCooldownEndTime');
1006
+ if (savedCooldown && parseInt(savedCooldown) > Date.now()) {
1007
+ this.cooldownEndTime = parseInt(savedCooldown);
1008
+ this.startCooldownTimer();
1009
+ }
1010
+ }
1011
 
1012
+ startCooldown() {
1013
+ this.cooldownEndTime = Date.now() + 2 * 60 * 1000;
1014
+ localStorage.setItem('mariamCooldownEndTime', this.cooldownEndTime.toString());
1015
+ this.startCooldownTimer();
1016
+ }
1017
+
1018
+ startCooldownTimer() {
1019
+ this.cooldownNotice.style.display = 'block';
1020
+ this.solveBtn.disabled = true;
1021
+
1022
+ if (this.cooldownInterval) clearInterval(this.cooldownInterval);
1023
+
1024
+ this.cooldownInterval = setInterval(() => {
1025
+ const remaining = Math.max(0, this.cooldownEndTime - Date.now());
1026
+ if (remaining <= 0) {
1027
+ clearInterval(this.cooldownInterval);
1028
+ this.cooldownNotice.style.display = 'none';
1029
+ this.updateButtonsState();
1030
+ return;
1031
+ }
1032
+ const minutes = Math.floor(remaining / 60000);
1033
+ const seconds = Math.floor((remaining % 60000) / 1000);
1034
+ this.cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
1035
+ }, 1000);
1036
+ }
1037
+
1038
+ isCooldownActive() {
1039
+ return Date.now() < this.cooldownEndTime;
1040
+ }
1041
+
1042
+ // History Management
1043
+ getHistory() {
1044
+ return JSON.parse(localStorage.getItem('mariamTaskHistory')) || [];
1045
+ }
1046
+
1047
+ saveHistory(history) {
1048
+ localStorage.setItem('mariamTaskHistory', JSON.stringify(history));
1049
+ }
1050
+
1051
+ renderHistory() {
1052
+ this.historyList.innerHTML = '';
1053
+ const history = this.getHistory();
1054
+
1055
+ if (history.length === 0) {
1056
+ this.historyList.innerHTML = '<p style="text-align:center; color: var(--text-muted); padding: 2rem;">Aucune tâche dans votre historique.</p>';
1057
+ this.clearHistoryBtn.style.display = 'none';
1058
+ return;
1059
+ }
1060
+
1061
+ this.clearHistoryBtn.style.display = 'block';
1062
+
1063
+ history.sort((a, b) => b.timestamp - a.timestamp).forEach(task => {
1064
+ const item = document.createElement('div');
1065
+ item.className = 'history-item';
1066
+ item.dataset.taskId = task.id;
1067
+
1068
+ let statusText = 'En attente...';
1069
+ let iconClass = 'pending';
1070
+ let icon = '⏳';
1071
+
1072
+ if (task.status === 'completed') {
1073
+ statusText = 'Terminé';
1074
+ iconClass = 'success';
1075
+ icon = '✅';
1076
+ } else if (task.status === 'error') {
1077
+ statusText = 'Erreur';
1078
+ iconClass = 'error';
1079
+ icon = '❌';
1080
+ } else if (task.status && task.status.startsWith('generating')) {
1081
+ statusText = 'Génération...';
1082
+ icon = '⚙️';
1083
+ }
1084
+
1085
+ item.innerHTML = `
1086
+ <div class="history-icon ${iconClass}">${icon}</div>
1087
+ <div class="history-info">
1088
+ <div class="history-filename">${task.filename}</div>
1089
+ <div class="history-meta">${statusText} • ${new Date(task.timestamp).toLocaleDateString('fr-FR')}</div>
1090
+ </div>
1091
+ <div class="history-actions" id="actions-${task.id}"></div>
1092
+ `;
1093
+
1094
+ this.historyList.appendChild(item);
1095
+ this.updateHistoryItemActions(task);
1096
+ });
1097
+ }
1098
+
1099
+ updateHistoryItemActions(task) {
1100
+ const container = document.getElementById(`actions-${task.id}`);
1101
+ if (!container) return;
1102
+
1103
+ if (task.status === 'completed' && task.download_url) {
1104
+ container.innerHTML = `<a href="${task.download_url}" class="btn btn-success btn-sm">📥</a>`;
1105
+ } else if (task.status === 'error') {
1106
+ container.innerHTML = `<span style="color: var(--danger); font-size: 0.8rem;">Échec</span>`;
1107
+ } else {
1108
+ container.innerHTML = `<span style="color: var(--primary-solid); font-size: 0.8rem;">En cours...</span>`;
1109
+ }
1110
+ }
1111
+
1112
+ updateTaskInHistory(taskId, updates) {
1113
+ let history = this.getHistory();
1114
+ const taskIndex = history.findIndex(t => t.id === taskId);
1115
+ if (taskIndex > -1) {
1116
+ history[taskIndex] = { ...history[taskIndex], ...updates };
1117
+ this.saveHistory(history);
1118
+ this.renderHistory();
1119
+ }
1120
+ }
1121
+
1122
+ checkHistoryStatus() {
1123
+ this.getHistory().forEach(task => {
1124
+ if (task.status && !['completed', 'error'].includes(task.status)) {
1125
+ fetch(`/task/${task.id}`)
1126
+ .then(response => response.json())
1127
+ .then(data => {
1128
+ if (data.status && data.status !== task.status) {
1129
+ this.updateTaskInHistory(task.id, {
1130
+ status: data.status,
1131
+ download_url: data.download_url,
1132
+ error: data.error
1133
+ });
1134
+ }
1135
+ })
1136
+ .catch(err => console.error(`Could not check status for ${task.id}:`, err));
1137
+ }
1138
+ });
1139
  }
1140
 
1141
+ clearHistory() {
1142
+ if (confirm("Êtes-vous sûr de vouloir vider tout l'historique ? Cette action est irréversible.")) {
1143
  localStorage.removeItem('mariamTaskHistory');
1144
+ this.renderHistory();
1145
+ this.showToast('Historique vidé', 'success');
1146
  }
1147
+ }
1148
+
1149
+ // Toast Notifications
1150
+ showToast(message, type = 'success') {
1151
+ // Remove existing toasts
1152
+ const existingToasts = document.querySelectorAll('.toast');
1153
+ existingToasts.forEach(toast => toast.remove());
1154
+
1155
+ const toast = document.createElement('div');
1156
+ toast.className = `toast ${type}`;
1157
+ toast.textContent = message;
1158
+ document.body.appendChild(toast);
1159
+
1160
+ // Show toast
1161
+ setTimeout(() => toast.classList.add('show'), 100);
1162
+
1163
+ // Hide toast
1164
+ setTimeout(() => {
1165
+ toast.classList.remove('show');
1166
+ setTimeout(() => toast.remove(), 300);
1167
+ }, 3000);
1168
+ }
1169
+ }
1170
+
1171
+ // Initialize app when DOM is ready
1172
+ document.addEventListener('DOMContentLoaded', () => {
1173
+ new MariamApp();
1174
+ });
1175
+
1176
+ // Add touch feedback for better mobile experience
1177
+ document.addEventListener('touchstart', function() {}, {passive: true});
1178
+
1179
+ // Prevent zoom on double tap for better mobile UX
1180
+ let lastTouchEnd = 0;
1181
+ document.addEventListener('touchend', function (event) {
1182
+ const now = (new Date()).getTime();
1183
+ if (now - lastTouchEnd <= 300) {
1184
+ event.preventDefault();
1185
+ }
1186
+ lastTouchEnd = now;
1187
+ }, false);
1188
+
1189
+ // Add pull-to-refresh indication (visual feedback only)
1190
+ let startY = 0;
1191
+ document.addEventListener('touchstart', e => {
1192
+ startY = e.touches[0].pageY;
1193
+ });
1194
+
1195
+ document.addEventListener('touchmove', e => {
1196
+ const y = e.touches[0].pageY;
1197
+ if (document.scrollingElement.scrollTop === 0 && y > startY && (y - startY) > 80) {
1198
+ document.body.style.paddingTop = '10px';
1199
+ }
1200
+ });
1201
+
1202
+ document.addEventListener('touchend', () => {
1203
+ document.body.style.paddingTop = '';
1204
  });
1205
  </script>
1206
  </body>