Docfile commited on
Commit
5c5997d
·
verified ·
1 Parent(s): 4d925a0

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +339 -190
templates/index.html CHANGED
@@ -6,11 +6,19 @@
6
  <title>Mariam Espagnol - Analyse de Documents</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/[email protected]/marked.min.js"></script>
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.24.1/prism.min.js"></script>
10
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.24.1/themes/prism.min.css">
 
11
  <style>
12
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Poppins:wght@300;400;500;600&display=swap');
13
 
 
 
 
 
 
 
 
14
  .fade-in { animation: fadeIn 0.5s ease-in; }
15
  .slide-up { animation: slideUp 0.5s ease-out; }
16
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@@ -23,121 +31,172 @@
23
  color: transparent;
24
  }
25
  .upload-zone {
26
- background: linear-gradient(145deg, #ffffff, #f3f4f6);
27
- box-shadow: 20px 20px 60px #d1d1d1, -20px -20px 60px #ffffff;
28
  transition: all 0.3s ease;
29
  }
30
- .upload-zone:hover { transform: scale(1.01); }
31
- .waves { position: absolute; bottom: 0; left: 0; width: 100%; overflow: hidden; line-height: 0; transform: rotate(180deg); }
 
 
 
 
32
  .waves svg { position: relative; display: block; width: calc(100% + 1.3px); height: 150px; }
33
- .prose { max-width: none; color: #374151; }
34
- .prose h1, .prose h2, .prose h3 { color: #1F2937; font-family: 'Playfair Display', serif; margin-top: 2em; margin-bottom: 1em; }
35
- .prose p { margin-bottom: 1.5em; line-height: 1.8; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  /* Style pour le bouton désactivé */
37
  button:disabled {
38
  opacity: 0.5;
39
  cursor: not-allowed;
 
 
 
 
 
40
  }
41
  </style>
42
  </head>
43
- <body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen relative">
44
  <div class="waves">
45
  <svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 120" preserveAspectRatio="none">
46
  <path d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V0H0V27.35A600.21,600.21,0,0,0,321.39,56.44Z" class="fill-indigo-50"></path>
47
  </svg>
48
  </div>
49
 
50
- <div class="max-w-6xl mx-auto px-4 py-12 relative z-10">
51
  <!-- Header -->
52
- <header class="text-center mb-16 fade-in">
53
- <h1 class="text-5xl md:text-6xl font-bold mb-4 font-['Playfair_Display'] gradient-text">
54
  Mariam Espagnol
55
  </h1>
56
- <p class="text-xl text-gray-600 font-light max-w-2xl mx-auto">
57
- Analysez vos documents en espagnol avec une traduction française précise et élégante
58
  </p>
59
  </header>
60
 
61
  <!-- Main Content -->
62
  <div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 mb-8 slide-up">
63
  <!-- File Type Selection -->
64
- <div class="mb-8">
65
- <label class="block text-lg font-medium text-gray-700 mb-4">Type de document</label>
66
- <div class="flex gap-6">
67
- <label class="relative flex items-center group cursor-pointer">
68
  <input type="radio" name="fileType" value="image" checked
69
- class="peer sr-only">
70
- <div class="w-6 h-6 border-2 border-gray-300 rounded-full peer-checked:border-indigo-500 peer-checked:bg-indigo-500 transition-all"></div>
71
- <!-- Changement de label -->
72
- <span class="ml-3 text-gray-700 group-hover:text-indigo-500 transition-colors">Document iconographique</span>
73
  </label>
74
- <label class="relative flex items-center group cursor-pointer">
75
  <input type="radio" name="fileType" value="text"
76
- class="peer sr-only">
77
- <div class="w-6 h-6 border-2 border-gray-300 rounded-full peer-checked:border-indigo-500 peer-checked:bg-indigo-500 transition-all"></div>
78
- <!-- Changement de label -->
79
- <span class="ml-3 text-gray-700 group-hover:text-indigo-500 transition-colors">Document textuel</span>
80
  </label>
81
  </div>
82
  </div>
83
 
84
  <!-- Upload Zone -->
85
- <div id="upload-zone" class="upload-zone border-2 border-dashed border-gray-300 rounded-2xl p-8 text-center cursor-pointer transition-all duration-300 relative overflow-hidden">
86
- <div class="space-y-4">
87
- <div class="w-20 h-20 mx-auto rounded-full bg-indigo-50 flex items-center justify-center">
88
- <svg class="w-10 h-10 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
90
- </svg>
91
- </div>
92
- <div class="text-sm text-gray-600">
93
- <!-- Le clic sur ce label ouvrira la galerie grâce au "for" -->
94
- <label for="file-upload" class="relative cursor-pointer rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
95
- <span>Déposez votre fichier ici</span>
96
- </label>
97
- <p class="pl-1">ou <label for="file-upload" class="cursor-pointer text-indigo-600 hover:text-indigo-500 font-medium">cliquez pour sélectionner</label></p>
98
- <!-- Input caché qui ouvre la galerie -->
99
- <input id="file-upload" name="file" type="file" class="sr-only">
 
 
 
100
  </div>
101
- <p class="text-xs text-gray-500">PNG, JPG, GIF, PDF, TXT jusqu'à 10MB</p>
102
- </div>
103
  </div>
104
 
105
  <!-- Preview -->
106
- <div id="preview" class="mt-8 hidden slide-up"> <!-- Ajustement marge -->
107
- <h3 class="text-xl font-medium text-gray-900 mb-6">Aperçu du document</h3>
108
- <div class="preview-container border rounded-xl p-6 bg-gray-50">
109
- <img id="image-preview" class="max-w-full h-auto hidden rounded-lg shadow-lg" alt="Aperçu">
110
- <pre id="text-preview" class="text-sm text-gray-700 whitespace-pre-wrap hidden bg-white p-4 rounded-lg shadow-inner max-h-60 overflow-auto"></pre> <!-- Ajout max-height et overflow -->
111
- <p id="file-info" class="text-sm text-gray-600 mt-4"></p> <!-- Pour afficher le nom du fichier -->
 
 
 
 
 
112
  </div>
113
  </div>
114
 
115
  <!-- Submit Button -->
116
- <div class="mt-8 text-center"> <!-- Ajout marge -->
117
  <button id="submit-button"
118
  disabled
119
- class="bg-indigo-600 text-white font-medium py-3 px-8 rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed">
120
- Soumettre pour analyse
 
 
 
 
 
 
 
121
  </button>
122
  </div>
123
 
124
  <!-- Analysis Results -->
125
- <div id="results" class="mt-12 hidden slide-up">
126
- <h3 class="text-xl font-medium text-gray-900 mb-6">Analyse détaillée</h3>
127
- <div class="bg-white rounded-xl p-6 shadow-inner">
128
- <div id="analysis-content" class="prose prose-indigo"></div>
 
 
 
 
 
 
 
 
 
129
  </div>
130
  </div>
131
- </div>
132
- </div>
133
 
134
- <!-- Loading Spinner -->
135
- <div id="loading" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden backdrop-blur-sm z-50">
136
- <div class="bg-white p-8 rounded-2xl shadow-2xl">
137
- <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-indigo-500 mx-auto"></div>
138
- <p class="text-gray-700 mt-4">Analyse en cours...</p>
139
- </div>
140
- </div>
 
 
 
141
 
142
  <script>
143
  document.addEventListener('DOMContentLoaded', function() {
@@ -146,100 +205,191 @@
146
  const preview = document.getElementById('preview');
147
  const imagePreview = document.getElementById('image-preview');
148
  const textPreview = document.getElementById('text-preview');
149
- const fileInfo = document.getElementById('file-info'); // Élément pour nom fichier
150
- const submitButton = document.getElementById('submit-button'); // Le bouton soumettre
 
 
 
151
  const results = document.getElementById('results');
152
  const analysisContent = document.getElementById('analysis-content');
153
- const loading = document.getElementById('loading');
 
 
 
154
 
155
- let selectedFile = null; // Variable pour stocker le fichier sélectionné
156
 
157
- // Configuration de marked pour le rendu Markdown
158
  marked.setOptions({
159
- breaks: true, gfm: true, headerIds: true, sanitize: false
 
 
 
 
 
 
 
 
 
 
160
  });
161
 
162
- // --- Gestion de la sélection du fichier ---
163
-
164
- // Clic sur la zone d'upload ouvre l'input file
165
- uploadZone.addEventListener('click', (e) => {
166
- // Empêche le clic sur le label interne de déclencher deux fois l'ouverture
167
- if (e.target.tagName !== 'LABEL' && e.target.tagName !== 'INPUT') {
168
- fileInput.click();
 
 
 
169
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  });
171
 
172
- // Gestion du Drag & Drop
 
173
  uploadZone.addEventListener('dragover', (e) => {
174
  e.preventDefault();
175
- uploadZone.classList.add('border-indigo-500', 'bg-indigo-50');
176
  });
177
  uploadZone.addEventListener('dragleave', () => {
178
- uploadZone.classList.remove('border-indigo-500', 'bg-indigo-50');
179
  });
180
  uploadZone.addEventListener('drop', (e) => {
181
  e.preventDefault();
182
- uploadZone.classList.remove('border-indigo-500', 'bg-indigo-50');
183
- const files = e.dataTransfer.files;
184
- if (files.length) {
185
- handleFileSelection(files[0]); // Gérer la sélection du fichier
186
  }
187
  });
188
 
189
- // Gestion du changement dans l'input file (sélection via galerie)
190
  fileInput.addEventListener('change', (e) => {
191
  if (e.target.files.length) {
192
- handleFileSelection(e.target.files[0]); // Gérer la sélection du fichier
193
  }
194
  });
195
 
196
- // Fonction pour gérer la SELECTION et l'APERÇU (sans soumettre)
 
 
 
 
 
 
 
 
 
 
 
197
  function handleFileSelection(file) {
198
- selectedFile = file; // Stocker le fichier
199
  const fileType = document.querySelector('input[name="fileType"]:checked').value;
 
200
 
201
- // Afficher l'aperçu
202
- preview.classList.remove('hidden');
203
- preview.classList.add('fade-in');
 
 
 
204
 
205
- // Cacher les anciens résultats si présents
206
- results.classList.add('hidden');
207
- analysisContent.innerHTML = '';
208
 
209
- // Afficher le nom du fichier
210
- fileInfo.textContent = `Fichier sélectionné : ${file.name}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
 
 
 
 
 
 
 
 
 
 
212
  if (fileType === 'image') {
213
- imagePreview.classList.remove('hidden', 'fade-in'); // Reset animation
214
  textPreview.classList.add('hidden');
215
  const reader = new FileReader();
216
- reader.onload = (e) => {
217
- imagePreview.src = e.target.result;
218
- void imagePreview.offsetWidth; // Force reflow pour rejouer animation
219
- imagePreview.classList.add('fade-in');
220
- };
221
  reader.readAsDataURL(file);
222
  } else { // text
223
  imagePreview.classList.add('hidden');
224
- textPreview.classList.remove('hidden', 'fade-in'); // Reset animation
225
- const reader = new FileReader();
226
- reader.onload = (e) => {
227
- textPreview.textContent = e.target.result;
228
- void textPreview.offsetWidth; // Force reflow
229
- textPreview.classList.add('fade-in');
230
- };
231
- reader.readAsText(file);
232
  }
233
 
234
- // Activer le bouton Soumettre
235
- submitButton.disabled = false;
236
  }
237
 
238
- // --- Gestion de la SOUMISSION ---
239
-
240
- submitButton.addEventListener('click', () => {
241
  if (!selectedFile) {
242
- alert("Veuillez d'abord sélectionner un fichier.");
243
  return;
244
  }
245
 
@@ -248,91 +398,90 @@
248
  formData.append('file', selectedFile);
249
  formData.append('fileType', fileType);
250
 
251
- // Afficher le spinner de chargement
252
- loading.classList.remove('hidden');
253
- loading.classList.add('fade-in');
254
- submitButton.disabled = true; // Désactiver pendant le chargement
255
 
256
- // Simuler un appel API (remplacez par votre appel fetch réel)
257
- console.log("Envoi du fichier:", selectedFile.name, "Type:", fileType);
258
- // Décommentez et adaptez cette partie pour votre appel réel
259
- /*
260
  fetch('/upload', {
261
  method: 'POST',
262
  body: formData
263
  })
264
  .then(response => {
265
  if (!response.ok) {
266
- throw new Error(`Erreur HTTP: ${response.status}`);
 
 
 
 
 
 
267
  }
268
  return response.json();
269
  })
270
  .then(data => {
271
- loading.classList.add('hidden');
272
- results.classList.remove('hidden');
273
- results.classList.add('slide-up');
274
- analysisContent.innerHTML = marked.parse(data.analysis || "Aucune analyse reçue.");
275
- Prism.highlightAll();
276
- // Optionnel: réinitialiser après succès
277
- // selectedFile = null;
278
- // fileInput.value = ''; // Réinitialiser l'input file
279
- // preview.classList.add('hidden');
280
- // fileInfo.textContent = '';
281
- // submitButton.disabled = true;
 
 
 
 
 
282
  })
283
  .catch(error => {
284
- loading.classList.add('hidden');
285
- submitButton.disabled = false; // Réactiver en cas d'erreur
286
- alert(`Une erreur est survenue lors de l'analyse : ${error.message}. Veuillez réessayer.`);
287
- console.error('Error:', error);
288
- // Cacher les résultats si une erreur survient
289
- results.classList.add('hidden');
290
- analysisContent.innerHTML = '';
291
  });
292
- */
293
-
294
- // --- Simulation (à remplacer par le fetch ci-dessus) ---
295
- setTimeout(() => {
296
- loading.classList.add('hidden');
297
- results.classList.remove('hidden');
298
- results.classList.add('slide-up');
299
- // Exemple de contenu Markdown simulé
300
- const simulatedMarkdown = `
301
- # Analyse du document : ${selectedFile.name}
302
-
303
- Ceci est une **simulation** de l'analyse.
304
-
305
- ## Section 1: Résumé
306
-
307
- Le document de type *${fileType}* a été traité.
308
-
309
- ## Section 2: Traduction (Exemple)
310
-
311
- * **Original (Espagnol):** Hola Mundo
312
- * **Traduction (Français):** Bonjour le Monde
313
-
314
- \`\`\`javascript
315
- // Exemple de bloc de code
316
- function greet() {
317
- console.log("Simulation terminée !");
318
- }
319
- \`\`\`
320
- `;
321
- analysisContent.innerHTML = marked.parse(simulatedMarkdown);
322
- Prism.highlightAll(); // Appliquer PrismJS au contenu généré
323
- // Ne pas réinitialiser ici pour pouvoir voir le résultat de la simulation
324
- // selectedFile = null;
325
- // fileInput.value = '';
326
- // preview.classList.add('hidden');
327
- // fileInfo.textContent = '';
328
- // submitButton.disabled = true; // Garder désactivé après simulation ou le réactiver si besoin
329
- submitButton.disabled = false; // Réactiver après la simulation pour permettre une autre soumission
330
- }, 2000); // Simule un délai réseau de 2 secondes
331
- // --- Fin de la Simulation ---
332
-
333
- }); // Fin de l'event listener du bouton submit
334
-
335
- }); // Fin du DOMContentLoaded
336
  </script>
337
  </body>
338
  </html>
 
6
  <title>Mariam Espagnol - Analyse de Documents</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/[email protected]/marked.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.24.1/prism.min.js" integrity="sha512-a63LMgq4XH5N+DXkrHCxBUC9vOlflXd+hNt6sJOUFr10a9IFJd3r1J52W3Ak296j7b5fVz/AhJK5dGgLNZwdtw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.24.1/themes/prism-tomorrow.min.css" integrity="sha512-vswe+cgvic/XBoF1OcM/TeJ2FW0OofqAVdCZiEYkd6dwGXOyEVrd0XB9aLhtFOUUjLz90GzvnBCgXWLSntyo7w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
11
+ <!-- Using Prism Tomorrow theme for better contrast -->
12
  <style>
13
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Poppins:wght@300;400;500;600&display=swap');
14
 
15
+ body {
16
+ font-family: 'Poppins', sans-serif;
17
+ }
18
+ h1, h2, h3, h4, h5, h6 {
19
+ font-family: 'Playfair Display', serif;
20
+ }
21
+
22
  .fade-in { animation: fadeIn 0.5s ease-in; }
23
  .slide-up { animation: slideUp 0.5s ease-out; }
24
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
 
31
  color: transparent;
32
  }
33
  .upload-zone {
34
+ background: linear-gradient(145deg, #f9fafb, #f3f4f6); /* Slightly adjusted gradient */
35
+ box-shadow: 5px 5px 15px #d1d9e6, -5px -5px 15px #ffffff; /* Adjusted shadow */
36
  transition: all 0.3s ease;
37
  }
38
+ .upload-zone.dragover { /* Style when dragging over */
39
+ border-color: #4f46e5; /* Indigo */
40
+ background: #e0e7ff; /* Lighter Indigo */
41
+ transform: scale(1.02);
42
+ }
43
+ .waves { position: absolute; bottom: 0; left: 0; width: 100%; overflow: hidden; line-height: 0; transform: rotate(180deg); z-index: 0; }
44
  .waves svg { position: relative; display: block; width: calc(100% + 1.3px); height: 150px; }
45
+ .waves .fill-indigo-50 { fill: #eef2ff; } /* Ensure Tailwind color is applied */
46
+
47
+ /* --- Responsive Analysis Box --- */
48
+ /* Ensure prose content wraps and code blocks are scrollable if needed */
49
+ .prose { max-width: 100%; /* Allow prose to fill container */ }
50
+ .prose pre {
51
+ background-color: #2d2d2d; /* Match Prism Tomorrow */
52
+ color: #ccc;
53
+ padding: 1em;
54
+ overflow-x: auto; /* Allow horizontal scroll for code */
55
+ border-radius: 0.375rem; /* Tailwind's rounded-md */
56
+ }
57
+ .prose code[class*="language-"] {
58
+ font-family: 'Courier New', Courier, monospace; /* Consistent code font */
59
+ color: #ccc;
60
+ background: none; /* Prism handles background */
61
+ text-shadow: none;
62
+ }
63
+ .prose h1, .prose h2, .prose h3 { color: #1F2937; margin-top: 1.5em; margin-bottom: 0.8em; padding-bottom: 0.3em; border-bottom: 1px solid #e5e7eb; }
64
+ .prose p { margin-bottom: 1.25em; line-height: 1.7; }
65
+ .prose ul, .prose ol { margin-left: 1.5em; margin-bottom: 1.25em; }
66
+ .prose li > p { margin-bottom: 0.5em; } /* Less space inside list items */
67
  /* Style pour le bouton désactivé */
68
  button:disabled {
69
  opacity: 0.5;
70
  cursor: not-allowed;
71
+ background-color: #a5b4fc; /* Lighter indigo when disabled */
72
+ }
73
+ /* Ensure Prism highlights correctly */
74
+ :not(pre) > code[class*="language-"], pre[class*="language-"] {
75
+ background: #2d2d2d; /* Match theme */
76
  }
77
  </style>
78
  </head>
79
+ <body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen relative overflow-x-hidden"> <!-- Prevent body scrollbars -->
80
  <div class="waves">
81
  <svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 120" preserveAspectRatio="none">
82
  <path d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V0H0V27.35A600.21,600.21,0,0,0,321.39,56.44Z" class="fill-indigo-50"></path>
83
  </svg>
84
  </div>
85
 
86
+ <div class="max-w-4xl mx-auto px-4 py-12 relative z-10"> <!-- Reduced max-width for better focus -->
87
  <!-- Header -->
88
+ <header class="text-center mb-12 fade-in"> <!-- Reduced margin -->
89
+ <h1 class="text-4xl md:text-5xl font-bold mb-3 font-['Playfair_Display'] gradient-text">
90
  Mariam Espagnol
91
  </h1>
92
+ <p class="text-lg text-gray-600 font-light max-w-xl mx-auto">
93
+ Analysez vos documents iconographiques ou textuels en espagnol
94
  </p>
95
  </header>
96
 
97
  <!-- Main Content -->
98
  <div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 mb-8 slide-up">
99
  <!-- File Type Selection -->
100
+ <div class="mb-6">
101
+ <label class="block text-base font-medium text-gray-700 mb-3">1. Choisissez le type de document</label>
102
+ <div class="flex flex-col sm:flex-row gap-4">
103
+ <label class="relative flex items-center group cursor-pointer p-3 border border-gray-200 rounded-lg hover:bg-indigo-50 transition-colors w-full">
104
  <input type="radio" name="fileType" value="image" checked
105
+ class="peer sr-only file-type-radio">
106
+ <div class="w-5 h-5 mr-3 border-2 border-gray-300 rounded-full flex-shrink-0 peer-checked:border-indigo-500 peer-checked:bg-indigo-500 transition-all"></div>
107
+ <span class="text-sm text-gray-700 group-hover:text-indigo-600">Document iconographique (Image, Photo, Dessin...)</span>
 
108
  </label>
109
+ <label class="relative flex items-center group cursor-pointer p-3 border border-gray-200 rounded-lg hover:bg-indigo-50 transition-colors w-full">
110
  <input type="radio" name="fileType" value="text"
111
+ class="peer sr-only file-type-radio">
112
+ <div class="w-5 h-5 mr-3 border-2 border-gray-300 rounded-full flex-shrink-0 peer-checked:border-indigo-500 peer-checked:bg-indigo-500 transition-all"></div>
113
+ <span class="text-sm text-gray-700 group-hover:text-indigo-600">Document textuel (Texte, PDF lisible...)</span>
 
114
  </label>
115
  </div>
116
  </div>
117
 
118
  <!-- Upload Zone -->
119
+ <div class="mb-6">
120
+ <label class="block text-base font-medium text-gray-700 mb-3">2. Sélectionnez votre fichier</label>
121
+ <div id="upload-zone" class="upload-zone border-2 border-dashed border-gray-300 rounded-xl p-6 text-center cursor-pointer transition-all duration-300 relative overflow-hidden">
122
+ <input id="file-upload" name="file" type="file" class="sr-only" accept="image/*"> <!-- Accept attribute will be updated by JS -->
123
+ <div class="space-y-3">
124
+ <div class="w-16 h-16 mx-auto rounded-full bg-indigo-100 flex items-center justify-center border-4 border-white shadow-sm">
125
+ <svg class="w-8 h-8 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
126
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
127
+ </svg>
128
+ </div>
129
+ <div class="text-sm text-gray-600">
130
+ <label for="file-upload" class="relative cursor-pointer rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
131
+ <span>Déposez votre fichier ici</span>
132
+ </label>
133
+ <span class="text-gray-500"> ou </span>
134
+ <label for="file-upload" class="cursor-pointer text-indigo-600 hover:text-indigo-500 font-medium">cliquez pour sélectionner</label>
135
+ </div>
136
+ <p class="text-xs text-gray-500" id="file-types-info">Images (PNG, JPG, GIF) ou Textes (TXT, PDF) - Max 16MB</p>
137
  </div>
138
+ </div>
 
139
  </div>
140
 
141
  <!-- Preview -->
142
+ <div id="preview" class="mt-6 hidden slide-up">
143
+ <h3 class="text-base font-medium text-gray-800 mb-3">Aperçu</h3>
144
+ <div class="preview-container border rounded-lg p-4 bg-gray-50 max-h-72 overflow-auto relative"> <!-- Added max-height and overflow -->
145
+ <img id="image-preview" class="max-w-full h-auto hidden rounded block mx-auto" alt="Aperçu Image"> <!-- Centered image -->
146
+ <pre id="text-preview" class="text-sm text-gray-700 whitespace-pre-wrap hidden bg-white p-3 rounded shadow-inner"></pre>
147
+ <p id="file-info" class="text-xs text-gray-500 mt-2 text-center"></p> <!-- Centered file info -->
148
+ <button id="remove-file-button" class="absolute top-2 right-2 bg-red-100 text-red-600 hover:bg-red-200 rounded-full p-1 text-xs hidden" title="Retirer le fichier">
149
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
150
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
151
+ </svg>
152
+ </button>
153
  </div>
154
  </div>
155
 
156
  <!-- Submit Button -->
157
+ <div class="mt-6 text-center">
158
  <button id="submit-button"
159
  disabled
160
+ class="bg-indigo-600 text-white font-medium py-2.5 px-6 rounded-lg shadow hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-indigo-300">
161
+ <span id="submit-button-text">3. Lancer l'analyse</span>
162
+ <span id="submit-button-spinner" class="hidden">
163
+ <svg class="animate-spin inline-block h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
164
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
165
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
166
+ </svg>
167
+ Analyse en cours...
168
+ </span>
169
  </button>
170
  </div>
171
 
172
  <!-- Analysis Results -->
173
+ <div id="results" class="mt-10 hidden slide-up"> <!-- Increased margin -->
174
+ <h3 class="text-lg font-semibold text-gray-900 mb-4 border-b pb-2">Analyse détaillée</h3>
175
+ <!-- Added overflow-x-auto here -->
176
+ <div class="bg-gray-50 rounded-lg p-4 md:p-6 shadow-inner overflow-x-auto border border-gray-200">
177
+ <div id="analysis-content" class="prose prose-sm sm:prose-base max-w-none prose-indigo">
178
+ <!-- Analysis will be inserted here -->
179
+ <p class="text-gray-500">L'analyse apparaîtra ici...</p> <!-- Placeholder -->
180
+ </div>
181
+ </div>
182
+ <div class="mt-4 text-right">
183
+ <button id="copy-analysis-button" class="text-sm bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1.5 px-3 rounded-md transition duration-150 ease-in-out">
184
+ Copier l'analyse
185
+ </button>
186
  </div>
187
  </div>
188
+ </div> <!-- End main content block -->
 
189
 
190
+ <footer class="text-center mt-12 text-gray-500 text-sm fade-in">
191
+ <p>© 2025 Mariam Espagnol - Outil d'analyse</p>
192
+ </footer>
193
+
194
+ </div> <!-- End max-width container -->
195
+
196
+ <!-- Global Loading/Alert (Optional, could replace simple loading) -->
197
+ <!-- <div id="global-message" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden backdrop-blur-sm z-50 p-4"> -->
198
+ <!-- Content for loading or alerts -->
199
+ <!-- </div> -->
200
 
201
  <script>
202
  document.addEventListener('DOMContentLoaded', function() {
 
205
  const preview = document.getElementById('preview');
206
  const imagePreview = document.getElementById('image-preview');
207
  const textPreview = document.getElementById('text-preview');
208
+ const fileInfo = document.getElementById('file-info');
209
+ const removeFileButton = document.getElementById('remove-file-button');
210
+ const submitButton = document.getElementById('submit-button');
211
+ const submitButtonText = document.getElementById('submit-button-text');
212
+ const submitButtonSpinner = document.getElementById('submit-button-spinner');
213
  const results = document.getElementById('results');
214
  const analysisContent = document.getElementById('analysis-content');
215
+ const copyAnalysisButton = document.getElementById('copy-analysis-button');
216
+ // const loading = document.getElementById('loading'); // Using spinner in button now
217
+ const fileTypeRadios = document.querySelectorAll('.file-type-radio');
218
+ const fileTypesInfo = document.getElementById('file-types-info');
219
 
220
+ let selectedFile = null;
221
 
222
+ // Configure marked
223
  marked.setOptions({
224
+ breaks: true, // Convert GFM line breaks to <br>
225
+ gfm: true, // Use GitHub Flavored Markdown
226
+ headerIds: false, // Don't add IDs to headers automatically
227
+ mangle: false, // Don't obscure email addresses
228
+ highlight: function(code, lang) {
229
+ const language = Prism.languages[lang] || Prism.languages.markup;
230
+ if (language) {
231
+ return Prism.highlight(code, language, lang);
232
+ }
233
+ return code; // Return unhighlighted code if language not found
234
+ }
235
  });
236
 
237
+ // --- Helper Functions ---
238
+ function showLoading(isLoading) {
239
+ if (isLoading) {
240
+ submitButtonText.classList.add('hidden');
241
+ submitButtonSpinner.classList.remove('hidden');
242
+ submitButton.disabled = true;
243
+ } else {
244
+ submitButtonText.classList.remove('hidden');
245
+ submitButtonSpinner.classList.add('hidden');
246
+ submitButton.disabled = (selectedFile === null); // Only enable if file is selected
247
  }
248
+ }
249
+
250
+ function resetUI() {
251
+ selectedFile = null;
252
+ fileInput.value = ''; // Clear the file input
253
+ preview.classList.add('hidden');
254
+ imagePreview.classList.add('hidden');
255
+ textPreview.classList.add('hidden');
256
+ imagePreview.src = '#'; // Reset image source
257
+ textPreview.textContent = '';
258
+ fileInfo.textContent = '';
259
+ removeFileButton.classList.add('hidden');
260
+ results.classList.add('hidden');
261
+ analysisContent.innerHTML = '<p class="text-gray-500">L\'analyse apparaîtra ici...</p>'; // Reset analysis area
262
+ submitButton.disabled = true;
263
+ showLoading(false); // Ensure loading state is reset
264
+ updateAcceptAttribute(); // Reset accept attribute
265
+ }
266
+
267
+ function updateAcceptAttribute() {
268
+ const selectedType = document.querySelector('input[name="fileType"]:checked').value;
269
+ if (selectedType === 'image') {
270
+ fileInput.accept = "image/png, image/jpeg, image/gif, image/webp";
271
+ fileTypesInfo.textContent = "Images (PNG, JPG, GIF, WEBP) - Max 16MB";
272
+ } else { // text
273
+ fileInput.accept = "text/plain, application/pdf, .txt"; // Allow PDF and TXT
274
+ fileTypesInfo.textContent = "Textes (TXT, PDF lisible) - Max 16MB";
275
+ }
276
+ // Reset if type changes after selecting a file
277
+ if (selectedFile) {
278
+ resetUI();
279
+ alert("Le type de document a été changé. Veuillez resélectionner votre fichier.");
280
+ }
281
+ }
282
+
283
+
284
+ // --- Event Listeners ---
285
+
286
+ // File Type Change
287
+ fileTypeRadios.forEach(radio => {
288
+ radio.addEventListener('change', updateAcceptAttribute);
289
  });
290
 
291
+ // Upload Zone Interactions
292
+ uploadZone.addEventListener('click', () => fileInput.click());
293
  uploadZone.addEventListener('dragover', (e) => {
294
  e.preventDefault();
295
+ uploadZone.classList.add('dragover');
296
  });
297
  uploadZone.addEventListener('dragleave', () => {
298
+ uploadZone.classList.remove('dragover');
299
  });
300
  uploadZone.addEventListener('drop', (e) => {
301
  e.preventDefault();
302
+ uploadZone.classList.remove('dragover');
303
+ if (e.dataTransfer.files.length) {
304
+ handleFileSelection(e.dataTransfer.files[0]);
 
305
  }
306
  });
307
 
308
+ // File Input Change
309
  fileInput.addEventListener('change', (e) => {
310
  if (e.target.files.length) {
311
+ handleFileSelection(e.target.files[0]);
312
  }
313
  });
314
 
315
+ // Remove File Button
316
+ removeFileButton.addEventListener('click', resetUI);
317
+
318
+ // Submit Button Click
319
+ submitButton.addEventListener('click', handleSubmit);
320
+
321
+ // Copy Button Click
322
+ copyAnalysisButton.addEventListener('click', copyAnalysisToClipboard);
323
+
324
+
325
+ // --- Core Logic Functions ---
326
+
327
  function handleFileSelection(file) {
 
328
  const fileType = document.querySelector('input[name="fileType"]:checked').value;
329
+ const maxFileSize = 16 * 1024 * 1024; // 16 MB
330
 
331
+ // Basic Validation (Type based on selection, Size)
332
+ if (file.size > maxFileSize) {
333
+ alert(`Le fichier est trop volumineux (${(file.size / 1024 / 1024).toFixed(1)} Mo). La taille maximale est de 16 Mo.`);
334
+ resetUI();
335
+ return;
336
+ }
337
 
338
+ const allowedImageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
339
+ const allowedTextTypes = ['text/plain', 'application/pdf'];
 
340
 
341
+ if (fileType === 'image' && !allowedImageTypes.includes(file.type)) {
342
+ // Allow if no type but ends with common image extension (browser might not set type correctly)
343
+ const isImageTypeByName = /\.(jpe?g|png|gif|webp)$/i.test(file.name);
344
+ if (!isImageTypeByName) {
345
+ alert("Type de fichier invalide. Veuillez sélectionner une image (PNG, JPG, GIF, WEBP).");
346
+ resetUI();
347
+ return;
348
+ }
349
+ } else if (fileType === 'text' && !allowedTextTypes.includes(file.type)) {
350
+ // Allow if no type but ends with .txt (PDF type is usually reliable)
351
+ const isTextTypeByName = /\.txt$/i.test(file.name);
352
+ if (!isTextTypeByName && file.type !== 'application/pdf') {
353
+ alert("Type de fichier invalide. Veuillez sélectionner un fichier texte (TXT) ou PDF.");
354
+ resetUI();
355
+ return;
356
+ }
357
+ }
358
 
359
+ // If validation passes:
360
+ selectedFile = file;
361
+ preview.classList.remove('hidden');
362
+ preview.classList.add('fade-in');
363
+ results.classList.add('hidden'); // Hide previous results
364
+ analysisContent.innerHTML = '<p class="text-gray-500">L\'analyse apparaîtra ici...</p>'; // Reset placeholder
365
+ fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} Ko)`;
366
+ removeFileButton.classList.remove('hidden');
367
+
368
+ // Show preview based on selected type
369
  if (fileType === 'image') {
370
+ imagePreview.classList.remove('hidden');
371
  textPreview.classList.add('hidden');
372
  const reader = new FileReader();
373
+ reader.onload = (e) => imagePreview.src = e.target.result;
 
 
 
 
374
  reader.readAsDataURL(file);
375
  } else { // text
376
  imagePreview.classList.add('hidden');
377
+ textPreview.classList.remove('hidden');
378
+ if (file.type === 'application/pdf') {
379
+ textPreview.textContent = "Aperçu non disponible pour les fichiers PDF. Le contenu sera analysé.";
380
+ } else {
381
+ const reader = new FileReader();
382
+ reader.onload = (e) => textPreview.textContent = e.target.result;
383
+ reader.readAsText(file);
384
+ }
385
  }
386
 
387
+ submitButton.disabled = false; // Enable submit button
 
388
  }
389
 
390
+ function handleSubmit() {
 
 
391
  if (!selectedFile) {
392
+ alert("Veuillez sélectionner un fichier.");
393
  return;
394
  }
395
 
 
398
  formData.append('file', selectedFile);
399
  formData.append('fileType', fileType);
400
 
401
+ showLoading(true);
402
+ results.classList.add('hidden'); // Hide old results before new request
 
 
403
 
404
+ // *** THE ACTUAL FETCH CALL ***
 
 
 
405
  fetch('/upload', {
406
  method: 'POST',
407
  body: formData
408
  })
409
  .then(response => {
410
  if (!response.ok) {
411
+ // Try to get error message from backend JSON response
412
+ return response.json().then(errData => {
413
+ throw new Error(errData.error || `Erreur HTTP: ${response.status}`);
414
+ }).catch(() => {
415
+ // Fallback if response is not JSON or doesn't have 'error' key
416
+ throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
417
+ });
418
  }
419
  return response.json();
420
  })
421
  .then(data => {
422
+ showLoading(false);
423
+ if (data.success && data.analysis) {
424
+ results.classList.remove('hidden');
425
+ results.classList.add('slide-up');
426
+ // Use marked to parse the Markdown response
427
+ analysisContent.innerHTML = marked.parse(data.analysis);
428
+ // Re-apply Prism highlighting AFTER content is added
429
+ Prism.highlightAllUnder(analysisContent);
430
+ // Optional: Scroll to results
431
+ results.scrollIntoView({ behavior: 'smooth', block: 'start' });
432
+ } else {
433
+ // Handle cases where backend responds with success:false or missing analysis
434
+ throw new Error(data.error || "L'analyse n'a pas pu être générée.");
435
+ }
436
+ // Decide if you want to reset after success
437
+ // resetUI(); // Or keep the file selected
438
  })
439
  .catch(error => {
440
+ showLoading(false);
441
+ // Display error message to the user
442
+ analysisContent.innerHTML = `<p class="text-red-600 font-medium">Erreur lors de l'analyse :</p><p class="text-red-500 text-sm">${error.message}</p>`;
443
+ results.classList.remove('hidden'); // Show the results area to display the error
444
+ console.error('Error during analysis:', error);
445
+ // Don't reset the file input on error, allow retry
446
+ submitButton.disabled = false; // Re-enable button on error
447
  });
448
+ // *** END OF FETCH CALL ***
449
+ }
450
+
451
+ function copyAnalysisToClipboard() {
452
+ const analysisHtml = analysisContent.innerHTML;
453
+ // Convert HTML to plain text for better clipboard compatibility
454
+ const tempDiv = document.createElement('div');
455
+ tempDiv.innerHTML = analysisHtml;
456
+ // Basic text extraction, might need refinement depending on Markdown complexity
457
+ let textToCopy = tempDiv.textContent || tempDiv.innerText || "";
458
+
459
+ // Attempt to preserve some Markdown structure (like line breaks)
460
+ textToCopy = analysisHtml
461
+ .replace(/<br\s*\/?>/gi, '\n') // HTML breaks to newlines
462
+ .replace(/<\/p>/gi, '\n') // End of paragraphs to newlines
463
+ .replace(/<\/li>/gi, '\n') // End of list items to newlines
464
+ .replace(/<\/(h[1-6])>/gi, '\n\n') // Headers get double newline
465
+ .replace(/<[^>]+>/g, '') // Remove remaining HTML tags
466
+ .replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&') // Decode HTML entities
467
+ .trim(); // Trim whitespace
468
+
469
+ navigator.clipboard.writeText(textToCopy)
470
+ .then(() => {
471
+ alert('Analyse copiée dans le presse-papiers !');
472
+ })
473
+ .catch(err => {
474
+ console.error('Erreur de copie: ', err);
475
+ alert('Impossible de copier l\'analyse.');
476
+ });
477
+ }
478
+
479
+
480
+ // --- Initial Setup ---
481
+ updateAcceptAttribute(); // Set initial accept value
482
+ resetUI(); // Start with a clean state
483
+
484
+ }); // End DOMContentLoaded
 
 
 
 
 
 
 
485
  </script>
486
  </body>
487
  </html>