Docfile commited on
Commit
5111edf
·
verified ·
1 Parent(s): 0b26c94

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +429 -1055
templates/index.html CHANGED
@@ -6,841 +6,459 @@
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,45 +466,42 @@
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,311 +511,70 @@
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>
 
6
  <title>Résolveur d'Images & PDF - Mariam</title>
7
  <style>
8
  :root {
9
+ --primary-color: #3498db;
10
+ --primary-hover: #2980b9;
11
+ --secondary-color: #2ecc71;
12
+ --secondary-hover: #27ae60;
13
+ --danger-color: #e74c3c;
14
+ --danger-hover: #c0392b;
15
+ --background-color: #f4f7f6;
16
+ --text-color: #333;
17
+ --border-color: #e0e0e0;
18
+ --card-bg: #ffffff;
19
+ --shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
20
+ --spacing-unit: 1rem;
21
+ }
22
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  * {
24
  box-sizing: border-box;
25
  margin: 0;
26
  padding: 0;
27
  }
28
 
29
+ html {
30
+ /* Améliore la fluidité du défilement */
31
+ scroll-behavior: smooth;
 
 
 
 
 
 
 
32
  }
33
 
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
36
+ max-width: 600px; /* Largeur max plus adaptée pour du contenu centré */
37
+ margin: 0 auto;
38
+ padding: var(--spacing-unit);
39
+ line-height: 1.6;
40
+ background-color: var(--background-color);
41
+ color: var(--text-color);
42
  }
43
 
 
44
  .header {
 
 
45
  text-align: center;
46
+ margin-bottom: calc(var(--spacing-unit) * 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
 
49
  .header h1 {
50
+ /* MODIFIÉ: Typographie réactive pour s'adapter à toutes les tailles d'écrans */
51
+ font-size: clamp(1.75rem, 7vw, 2.5rem);
52
+ color: #2c3e50;
53
+ margin-bottom: calc(var(--spacing-unit) * 0.25);
54
  }
55
 
56
  .header .subtitle {
57
+ font-size: clamp(1rem, 4vw, 1.1rem);
58
+ color: #555;
 
 
 
 
 
 
 
 
59
  }
60
 
61
+ /* <!-- SUPPRIMÉ: Le conteneur du bouton Telegram a été retiré --> */
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ .container {
64
+ background-color: var(--card-bg);
65
+ padding: calc(var(--spacing-unit) * 1.5);
66
+ border-radius: 12px; /* Bords plus arrondis pour un look moderne */
67
+ box-shadow: var(--shadow);
68
+ margin-bottom: calc(var(--spacing-unit) * 2);
69
  }
70
 
71
+ .style-selection h3 {
72
+ margin-bottom: var(--spacing-unit);
73
+ color: #2c3e50;
74
+ font-size: 1.2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
+ .radio-group {
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: var(--spacing-unit);
 
 
 
 
82
  }
83
 
84
+ /* MODIFIÉ: Amélioration sémantique et UX des boutons radio */
85
+ .radio-option label {
86
+ display: flex;
87
+ align-items: flex-start;
88
+ padding: var(--spacing-unit);
89
+ border-radius: 8px;
90
+ transition: background-color 0.2s, box-shadow 0.2s;
91
  cursor: pointer;
92
+ border: 1px solid var(--border-color);
 
 
93
  }
94
 
95
+ .radio-option label:hover {
96
+ border-color: var(--primary-color);
 
 
 
 
 
97
  }
98
+
99
+ .radio-option input[type="radio"]:checked + .radio-content-wrapper {
100
+ border-color: var(--primary-color);
101
+ box-shadow: 0 0 0 2px var(--primary-color);
102
+ background-color: #eaf5ff;
 
 
 
103
  }
104
 
105
+ .radio-option input[type="radio"] {
106
+ /* Caché car on stylise le label à la place */
107
+ display: none;
 
 
108
  }
109
 
110
+ .radio-content-wrapper {
 
 
 
 
 
 
 
 
 
111
  display: flex;
112
  align-items: center;
113
+ width: 100%;
114
+ gap: var(--spacing-unit);
 
 
 
 
 
 
 
 
115
  }
116
+
117
+ .radio-icon { font-size: 1.5rem; }
118
+ .radio-label { font-weight: 500; display: block; }
119
+ .radio-description { font-size: 0.9rem; color: #666; }
120
 
 
 
 
 
121
 
122
+ .upload-section {
123
+ border: 2px dashed var(--border-color);
124
+ padding: calc(var(--spacing-unit) * 2);
 
 
125
  text-align: center;
126
+ border-radius: 8px;
 
127
  cursor: pointer;
128
+ transition: all 0.3s ease;
129
+ background-color: #f8f9fa;
130
+ margin: calc(var(--spacing-unit) * 1.5) 0;
131
  }
132
+
133
+ .upload-section:hover {
134
+ border-color: var(--primary-color);
135
+ background-color: #e8f4fb;
 
136
  }
137
 
138
  .upload-icon {
139
  font-size: 3rem;
140
+ margin-bottom: var(--spacing-unit);
141
+ color: var(--primary-color);
142
+ }
143
+
144
+ #file-input { display: none; }
145
+ #file-preview-area { margin-top: var(--spacing-unit); display: flex; flex-wrap: wrap; gap: var(--spacing-unit); justify-content: center; }
146
+ .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: 8px; background-color: #fdfdfd; }
147
+ .preview-item img { width: 80px; height: 80px; border-radius: 4px; object-fit: cover; }
148
+ .preview-item .pdf-icon { font-size: 3rem; color: var(--danger-color); line-height: 1; padding: 12px 0; }
149
+ .preview-item span { font-size: 0.8rem; color: #555; word-break: break-all; max-width: 80px; text-align: center; }
150
+
151
+ .button {
152
+ width: 100%;
153
+ padding: 0.9rem 1rem; /* MODIFIÉ: Plus haut pour être plus facile à taper */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  border: none;
155
+ border-radius: 8px;
156
+ font-size: 1.1rem; /* Police plus grande */
157
+ font-weight: 600; /* Police plus affirmée */
158
  cursor: pointer;
159
  transition: all 0.3s ease;
160
+ margin-top: calc(var(--spacing-unit) * 0.5);
161
+ background-color: var(--primary-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  color: white;
163
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
164
+ text-decoration: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  display: inline-block;
166
+ text-align:center;
167
+ }
168
+ .button:hover:not(:disabled) { transform: translateY(-2px); background-color: var(--primary-hover); }
169
+ .button:disabled { background-color: #bdc3c7; cursor: not-allowed; box-shadow: none; }
170
+
171
+ .clear-button { background-color: #7f8c8d; } /* Couleur moins agressive pour une action secondaire */
172
+ .clear-button:hover:not(:disabled) { background-color: #95a5a6; }
173
+
174
+ .download-button { background-color: var(--secondary-color); }
175
+ .download-button:hover:not(:disabled) { background-color: var(--secondary-hover); }
176
+
177
+ .cooldown-notice { background-color: #fff3cd; border-left: 4px solid #ffeaa7; border-radius: 8px; padding: var(--spacing-unit); margin-bottom: var(--spacing-unit); text-align: center; color: #856404; }
178
+ .cooldown-timer { font-weight: bold; }
179
+
180
+ #solving-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); }
181
+ .status { text-align: center; margin-bottom: var(--spacing-unit); font-weight: bold; color: #2c3e50; font-size: 1.1rem;}
182
+ .status.error { color: var(--danger-color); }
183
+ .status.completed { color: var(--secondary-color); }
184
+
185
+ /* MODIFIÉ: Le message de patience pour l'utilisateur */
186
+ .processing-notice {
187
+ background-color: #eaf5ff;
188
+ border-left: 4px solid var(--primary-color);
189
+ padding: var(--spacing-unit);
190
+ margin: var(--spacing-unit) 0;
191
+ border-radius: 8px;
192
+ text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }
194
+ .processing-notice p { margin: 0; font-weight: 600; color: #2c3e50;}
195
+ .processing-notice small { color: #555; }
196
+
197
+ .response-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); }
198
+ #response { background-color: #fdfdfd; padding: var(--spacing-unit); border-radius: 8px; border: 1px solid #eee; min-height: 50px; white-space: pre-wrap; word-wrap: break-word; }
199
+
200
+ #history-container h2 { text-align:center; margin-bottom: var(--spacing-unit); color: #2c3e50;}
201
+ #history-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: calc(var(--spacing-unit)*0.75);}
202
+ .history-item { display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-unit); background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 8px; transition: box-shadow 0.2s; gap: var(--spacing-unit); }
203
+ .history-info { flex-grow: 1; min-width: 0; } /* Permet au nom de fichier de se couper correctement */
204
+ .history-filename { font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
205
+ .history-status { font-size: 0.85rem; }
206
+ .history-status-pending { color: #f39c12; } .history-status-completed { color: var(--secondary-color); } .history-status-error { color: var(--danger-color); }
207
+ .history-actions { flex-shrink: 0; }
208
+ .history-actions .button { width: auto; padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0; }
209
+
210
+ #clear-history-button { background-color: var(--danger-color); margin-top: 1rem; }
211
+ #clear-history-button:hover:not(:disabled) { background-color: var(--danger-hover); }
212
  </style>
213
  </head>
214
  <body>
215
+ <div class="header">
216
+ <h1>🖼️ Science (Math, Physique, Chimie) 🧠</h1>
217
+ <p class="subtitle">Avec Mariam, votre assistante IA</p>
218
+ </div>
 
 
 
 
 
 
 
219
 
220
+ <!-- SUPPRIMÉ : Le bouton pour rejoindre le groupe Telegram a été enlevé. -->
221
+
222
+ <div class="container">
223
+ <div class="style-selection">
224
+ <h3>🎨 Choisissez le style de résolution</h3>
225
+ <div class="radio-group">
226
+ <!-- MODIFIÉ: Structure sémantique et accessible des boutons radio -->
227
+ <div class="radio-option">
228
+ <input type="radio" id="style-light" name="resolution-style" value="light">
229
+ <label for="style-light" class="radio-content-wrapper">
230
+ <div class="radio-icon">📝</div>
231
+ <div class="radio-content">
232
+ <span class="radio-label">Résolution Light</span>
233
+ <div class="radio-description">Simple et épuré, pour une lecture rapide.</div>
234
  </div>
235
+ </label>
236
+ </div>
237
+ <div class="radio-option">
238
+ <input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked>
239
+ <label for="style-colorful" class="radio-content-wrapper">
240
+ <div class="radio-icon">🌈</div>
241
+ <div class="radio-content">
242
+ <span class="radio-label">Résolution Colorée</span>
243
+ <div class="radio-description">Format riche avec couleurs et mise en page.</div>
244
  </div>
245
+ </label>
246
  </div>
247
  </div>
248
+ </div>
249
+
250
+ <div id="cooldown-notice" class="cooldown-notice" style="display: none;">
251
+ ⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer"></span> avant une nouvelle soumission.
252
+ </div>
253
+
254
+ <div id="upload-section" class="upload-section">
255
+ <div class="upload-icon">📤</div>
256
+ <p><strong>Cliquez ou déposez vos fichiers ici</strong></p>
257
+ <small>Images (PNG, JPG...) ou 1 PDF</small>
258
+ <input type="file" id="file-input" accept="image/*,application/pdf" multiple>
259
+ </div>
260
+
261
+ <div id="file-preview-area"></div>
262
+
263
+ <button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer la sélection</button>
264
+ <button id="solve-button" class="button" disabled>🔍 Résoudre</button>
265
+
266
+ <div id="solving-container">
267
+ <!-- MODIFIÉ: Message clair pour informer l'utilisateur de l'attente -->
268
+ <div class="processing-notice">
269
+ <p>⏳ La génération a commencé...</p>
270
+ <small>Cela peut prendre quelques minutes. Vous pouvez fermer cette page et revenir plus tard, votre résultat apparaîtra dans l'historique ci-dessous.</small>
271
  </div>
272
+ <div class="status" id="status"></div>
273
+ <div class="response-container" id="response-container">
274
+ <div id="response"></div>
275
+ <a id="download-button" class="button download-button" style="display: none;">📥 Télécharger le PDF</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  </div>
277
+ </div>
278
+ </div>
279
 
280
+ <div id="history-container" class="container">
281
+ <h2>Historique des Tâches</h2>
282
+ <ul id="history-list">
283
+ <!-- L'historique sera rempli par le JavaScript -->
284
+ </ul>
285
+ <button id="clear-history-button" class="button">🗑️ Vider l'historique</button>
 
 
 
 
 
 
 
286
  </div>
287
 
288
  <script>
289
+ // Le JavaScript reste identique car il est déjà bien conçu pour gérer cette logique.
290
+ // Les modifications sont purement dans le HTML et le CSS.
291
+ document.addEventListener('DOMContentLoaded', function() {
292
+ const uploadSection = document.getElementById('upload-section');
293
+ const fileInput = document.getElementById('file-input');
294
+ const filePreviewArea = document.getElementById('file-preview-area');
295
+ const solveButton = document.getElementById('solve-button');
296
+ const clearFilesButton = document.getElementById('clear-files-button');
297
+ const solvingContainer = document.getElementById('solving-container');
298
+ const responseContainer = document.getElementById('response-container');
299
+ const responseDiv = document.getElementById('response');
300
+ const statusElement = document.getElementById('status');
301
+ const downloadButton = document.getElementById('download-button');
302
+ const cooldownNotice = document.getElementById('cooldown-notice');
303
+ const cooldownTimer = document.getElementById('cooldown-timer');
304
+ const historyList = document.getElementById('history-list');
305
+ const clearHistoryButton = document.getElementById('clear-history-button');
306
+
307
+ let selectedFiles = [];
308
+ let cooldownEndTime = 0;
309
+ let cooldownInterval = null;
310
+ const eventSources = {};
311
+
312
+ const getHistory = () => JSON.parse(localStorage.getItem('mariamTaskHistory')) || [];
313
+ const saveHistory = (history) => localStorage.setItem('mariamTaskHistory', JSON.stringify(history));
314
+
315
+ function renderHistory() {
316
+ historyList.innerHTML = '';
317
+ const history = getHistory();
318
+ if (history.length === 0) {
319
+ historyList.innerHTML = '<p style="text-align:center; color:#777;">Aucune tâche dans votre historique.</p>';
320
+ clearHistoryButton.style.display = 'none';
321
+ return;
322
+ }
323
+ clearHistoryButton.style.display = 'block';
324
 
325
+ history.sort((a, b) => b.timestamp - a.timestamp).forEach(task => {
326
+ const li = document.createElement('li');
327
+ li.classList.add('history-item');
328
+ li.dataset.taskId = task.id;
 
 
 
329
 
330
+ let statusText = 'En attente...';
331
+ let statusClass = 'history-status-pending';
332
+ if (task.status === 'completed') {
333
+ statusText = 'Terminé';
334
+ statusClass = 'history-status-completed';
335
+ } else if (task.status === 'error') {
336
+ statusText = 'Erreur';
337
+ statusClass = 'history-status-error';
338
+ } else if (task.status && task.status.startsWith('generating')) {
339
+ statusText = 'Génération en cours...';
340
+ } else if (task.status) {
341
+ statusText = task.status.charAt(0).toUpperCase() + task.status.slice(1);
342
+ }
343
 
344
+ li.innerHTML = `
345
+ <div class="history-info">
346
+ <span class="history-filename">${task.filename}</span>
347
+ <small class="history-status ${statusClass}">${statusText} - ${new Date(task.timestamp).toLocaleString('fr-FR')}</small>
348
+ </div>
349
+ <div class="history-actions" id="actions-${task.id}"></div>
350
+ `;
351
+ historyList.appendChild(li);
352
+ updateHistoryItemActions(task);
353
  });
354
  }
355
 
356
+ function updateHistoryItemActions(task) {
357
+ const container = document.getElementById(`actions-${task.id}`);
358
+ if (!container) return;
359
+
360
+ if (task.status === 'completed' && task.download_url) {
361
+ container.innerHTML = `<a href="${task.download_url}" class="button download-button">Télécharger</a>`;
362
+ } else if (task.status === 'error') {
363
+ container.innerHTML = `<span style="color:var(--danger-color); font-weight:bold;">Échec</span>`;
364
+ } else {
365
+ container.innerHTML = `<span style="color:var(--primary-color); font-style:italic;">En cours...</span>`;
366
+ }
367
  }
368
 
369
+ function updateTaskInHistory(taskId, updates) {
370
+ let history = getHistory();
371
+ const taskIndex = history.findIndex(t => t.id === taskId);
372
+ if (taskIndex > -1) {
373
+ history[taskIndex] = { ...history[taskIndex], ...updates };
374
+ saveHistory(history);
375
+ renderHistory();
376
+ }
377
  }
378
 
379
+ function checkHistoryStatus() {
380
+ getHistory().forEach(task => {
381
+ if (task.status && !['completed', 'error'].includes(task.status)) {
382
+ fetch(`/task/${task.id}`)
383
+ .then(response => response.json())
384
+ .then(data => {
385
+ if (data.status && data.status !== task.status) {
386
+ updateTaskInHistory(task.id, { status: data.status, download_url: data.download_url, error: data.error });
387
+ }
388
+ }).catch(err => console.error(`Could not check status for ${task.id}:`, err));
389
+ }
390
+ });
391
  }
392
 
393
+ // La fonction selectStyle n'est plus nécessaire grâce à l'utilisation des <label>
394
+ // window.selectStyle = (style) => document.getElementById(`style-${style}`).checked = true;
395
+
396
+ function checkCooldownOnLoad() {
397
+ const savedCooldown = localStorage.getItem('mariamCooldownEndTime');
398
+ if (savedCooldown && parseInt(savedCooldown) > Date.now()) {
399
+ cooldownEndTime = parseInt(savedCooldown);
400
+ startCooldownTimer();
401
  }
402
  }
403
+
404
+ function startCooldown() {
405
+ cooldownEndTime = Date.now() + 2 * 60 * 1000;
406
+ localStorage.setItem('mariamCooldownEndTime', cooldownEndTime.toString());
407
+ startCooldownTimer();
408
+ }
409
+
410
+ function startCooldownTimer() {
411
+ cooldownNotice.style.display = 'block';
412
+ solveButton.disabled = true;
413
+ if (cooldownInterval) clearInterval(cooldownInterval);
414
+ cooldownInterval = setInterval(() => {
415
+ const remaining = Math.max(0, cooldownEndTime - Date.now());
416
+ if (remaining <= 0) {
417
+ clearInterval(cooldownInterval);
418
+ cooldownNotice.style.display = 'none';
419
+ updateButtonsState();
420
+ return;
421
+ }
422
+ const minutes = Math.floor(remaining / 60000);
423
+ const seconds = Math.floor((remaining % 60000) / 1000);
424
+ cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
425
+ }, 1000);
426
+ }
427
+
428
+ const isCooldownActive = () => Date.now() < cooldownEndTime;
429
 
430
+ const handleFileSelection = (files) => {
431
  const newFiles = Array.from(files);
432
+ let pdfSelected = selectedFiles.some(f => f.type === 'application/pdf');
 
433
  newFiles.forEach(file => {
434
  if (file.type.startsWith('image/')) {
435
+ if (!selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) selectedFiles.push(file);
 
 
436
  } else if (file.type === 'application/pdf') {
437
  if (!pdfSelected) {
438
+ selectedFiles = selectedFiles.filter(f => f.type !== 'application/pdf');
439
+ selectedFiles.push(file);
440
  pdfSelected = true;
441
  }
442
  }
443
  });
444
+ updateFilePreviews();
445
+ updateButtonsState();
446
+ };
447
+
448
+ uploadSection.addEventListener('click', () => fileInput.click());
449
+ uploadSection.addEventListener('dragover', (e) => { e.preventDefault(); uploadSection.classList.add('hover'); });
450
+ uploadSection.addEventListener('dragleave', (e) => uploadSection.classList.remove('hover'));
451
+ uploadSection.addEventListener('drop', (e) => { e.preventDefault(); uploadSection.classList.remove('hover'); if (e.dataTransfer.files.length) handleFileSelection(e.dataTransfer.files); });
452
+ fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFileSelection(e.target.files); });
453
+
454
+ function updateFilePreviews() {
455
+ filePreviewArea.innerHTML = '';
456
+ if (selectedFiles.length === 0) return;
457
+ selectedFiles.forEach(file => {
458
  const item = document.createElement('div');
459
  item.className = 'preview-item';
460
+ const name = document.createElement('span');
461
+ name.textContent = file.name.length > 15 ? file.name.substring(0, 12) + "..." : file.name;
 
 
 
462
  if (file.type.startsWith('image/')) {
463
  const img = document.createElement('img');
464
  img.src = URL.createObjectURL(file);
 
466
  } else {
467
  item.innerHTML = '<div class="pdf-icon">📄</div>';
468
  }
469
+ item.appendChild(name);
470
+ filePreviewArea.appendChild(item);
 
 
 
 
 
471
  });
472
  }
473
+
474
+ function updateButtonsState() {
475
+ const hasFiles = selectedFiles.length > 0;
476
+ solveButton.disabled = !hasFiles || isCooldownActive();
477
+ solveButton.textContent = hasFiles ? `🔍 Résoudre (${selectedFiles.length} fichier(s))` : '🔍 Résoudre';
478
+ clearFilesButton.style.display = hasFiles ? 'block' : 'none';
479
+ }
480
+
481
+ clearFilesButton.addEventListener('click', () => {
482
+ selectedFiles = [];
483
+ fileInput.value = '';
484
+ updateFilePreviews();
485
+ updateButtonsState();
486
+ solvingContainer.style.display = 'none';
487
+ });
488
+
489
+ solveButton.addEventListener('click', () => {
490
+ if (selectedFiles.length === 0 || isCooldownActive()) return;
491
 
492
+ startCooldown();
493
+ solveButton.disabled = true;
494
+ solveButton.textContent = '⏳ Traitement...';
 
495
 
496
+ solvingContainer.style.display = 'block';
497
+ responseContainer.style.display = 'none';
498
+ downloadButton.style.display = 'none';
499
+ statusElement.className = 'status';
500
+ statusElement.textContent = 'Préparation...';
501
+ responseDiv.innerHTML = '';
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  const formData = new FormData();
504
+ selectedFiles.forEach(file => formData.append('user_files', file));
505
  formData.append('style', document.querySelector('input[name="resolution-style"]:checked').value);
506
 
507
  fetch('/solve', { method: 'POST', body: formData })
 
511
  })
512
  .then(data => {
513
  const { task_id, first_filename } = data;
514
+
515
+ let history = getHistory();
516
+ history.push({ id: task_id, filename: first_filename, status: 'pending', timestamp: Date.now() });
517
+ saveHistory(history);
518
+ renderHistory();
519
 
520
+ statusElement.textContent = 'Traitement en arrière-plan...';
521
+ listenToTask(task_id);
 
 
 
 
 
 
 
 
 
 
522
  })
523
+ .catch(error => handleError(error.message));
524
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
 
526
+ function listenToTask(taskId) {
527
+ if (eventSources[taskId]) eventSources[taskId].close();
528
 
529
  const eventSource = new EventSource('/stream/' + taskId);
530
+ eventSources[taskId] = eventSource;
531
 
532
+ eventSource.onmessage = function(event) {
533
  const data = JSON.parse(event.data);
534
 
535
+ updateTaskInHistory(taskId, { status: data.status, download_url: data.download_url, error: data.error });
536
+
537
+ statusElement.textContent = `Statut: ${data.status}`;
 
 
538
 
539
  if (data.status === 'completed') {
540
+ statusElement.className = 'status completed';
541
+ statusElement.textContent = 'Traitement terminé ! 🎉';
542
+ responseDiv.innerHTML = `<p style="color: #2ecc71; text-align: center;">Votre PDF est prêt.</p>`;
543
+ downloadButton.href = data.download_url;
544
+ downloadButton.style.display = 'block';
545
+ responseContainer.style.display = 'block';
546
  eventSource.close();
 
 
 
 
 
547
  } else if (data.status === 'error') {
548
+ handleError(data.error || 'Une erreur inattendue est survenue.', taskId);
549
  eventSource.close();
 
 
550
  }
551
  };
552
 
553
+ eventSource.onerror = function() {
554
  eventSource.close();
555
+ checkHistoryStatus();
556
  };
557
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
+ function handleError(errorMessage, taskId = null) {
560
+ statusElement.className = 'status error';
561
+ statusElement.textContent = 'Erreur:';
562
+ responseDiv.innerHTML = `<p style="color:red;">${errorMessage}</p>`;
563
+ responseContainer.style.display = 'block';
564
+ downloadButton.style.display = 'none';
565
+ if (taskId) updateTaskInHistory(taskId, { status: 'error', error: errorMessage });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  }
567
 
568
+ clearHistoryButton.addEventListener('click', () => {
569
+ if(confirm("Êtes-vous sûr de vouloir vider tout l'historique ? Cette action est irréversible.")) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  localStorage.removeItem('mariamTaskHistory');
571
+ renderHistory();
 
572
  }
573
+ });
574
+
575
+ checkCooldownOnLoad();
576
+ renderHistory();
577
+ checkHistoryStatus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  });
579
  </script>
580
  </body>