SamiKLN commited on
Commit
70f0b76
·
verified ·
1 Parent(s): 0a872e7

Upload 4 files

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. background.mp4 +3 -0
  3. index.html +150 -0
  4. script.js +416 -0
  5. style.css +435 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ background.mp4 filter=lfs diff=lfs merge=lfs -text
background.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48a9cbba7573da31a6ff705f89e3d7e7b998c34d3433da3edd738ba0a626de61
3
+ size 4349541
index.html ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DocImageAI Explorer - Analyse de Documents et Images</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <!-- Fond vidéo -->
12
+ <video autoplay muted loop id="bg-video">
13
+ <source src="background.mp4" type="video/mp4">
14
+ Votre navigateur ne supporte pas les vidéos HTML5.
15
+ </video>
16
+
17
+ <!-- Conteneur principal -->
18
+ <div class="container">
19
+ <header>
20
+ <h1>DocImageAI Explorer</h1>
21
+ <p class="subtitle">Analyse intelligente de documents et images avec IA</p>
22
+ </header>
23
+
24
+ <!-- Navigation par onglets -->
25
+ <div class="tabs">
26
+ <button class="tab-button active" onclick="openTab('upload')">Upload</button>
27
+ <button class="tab-button" onclick="openTab('summarize')">Résumé</button>
28
+ <button class="tab-button" onclick="openTab('caption')">Images</button>
29
+ <button class="tab-button" onclick="openTab('qa')">Questions</button>
30
+ <button class="tab-button" onclick="openTab('about')">À propos</button>
31
+ </div>
32
+
33
+ <!-- Onglet Upload -->
34
+ <div id="upload" class="tab-content active">
35
+ <h2><i class="icon">📤</i> Upload de Fichiers</h2>
36
+ <div class="upload-area" id="dropArea">
37
+ <input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.xlsx,.jpg,.jpeg,.png">
38
+ <div class="upload-icon">⬆️</div>
39
+ <p>Glissez-déposez vos fichiers ici ou cliquez pour parcourir</p>
40
+ <p class="file-types">Formats supportés : PDF, DOCX, PPTX, XLSX, JPG, PNG</p>
41
+ </div>
42
+ <div id="fileList" class="file-list">
43
+ <p class="empty-message">Aucun fichier uploadé</p>
44
+ </div>
45
+ <div id="uploadStatus" class="status-message"></div>
46
+ </div>
47
+
48
+ <!-- Onglet Résumé -->
49
+ <div id="summarize" class="tab-content">
50
+ <h2><i class="icon">📝</i> Résumé de Document</h2>
51
+ <div class="form-group">
52
+ <label for="summaryFile">Sélectionnez un document :</label>
53
+ <select id="summaryFile" class="file-selector">
54
+ <option value="">-- Choisir un document --</option>
55
+ </select>
56
+ </div>
57
+ <div class="action-area">
58
+ <button id="summarizeBtn" class="action-btn">
59
+ <span class="btn-icon">✍️</span> Générer le Résumé
60
+ </button>
61
+ <div class="loading-spinner" id="summarySpinner"></div>
62
+ </div>
63
+ <div id="summaryResult" class="result-box">
64
+ <p class="placeholder">Votre résumé apparaîtra ici...</p>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Onglet Images -->
69
+ <div id="caption" class="tab-content">
70
+ <h2><i class="icon">🖼️</i> Analyse d'Image</h2>
71
+ <div class="form-group">
72
+ <label for="captionFile">Sélectionnez une image :</label>
73
+ <select id="captionFile" class="file-selector">
74
+ <option value="">-- Choisir une image --</option>
75
+ </select>
76
+ </div>
77
+ <div id="imagePreview" class="image-preview">
78
+ <div class="preview-placeholder">
79
+ <div class="placeholder-icon">🖼️</div>
80
+ <p>Aperçu de l'image</p>
81
+ </div>
82
+ </div>
83
+ <div class="action-area">
84
+ <button id="captionBtn" class="action-btn">
85
+ <span class="btn-icon">🔍</span> Analyser l'Image
86
+ </button>
87
+ <div class="loading-spinner" id="captionSpinner"></div>
88
+ </div>
89
+ <div id="captionResult" class="result-box">
90
+ <p class="placeholder">La description de votre image apparaîtra ici...</p>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Onglet Questions -->
95
+ <div id="qa" class="tab-content">
96
+ <h2><i class="icon">❓</i> Questions & Réponses</h2>
97
+ <div class="form-group">
98
+ <label for="qaFile">Document/Image (optionnel) :</label>
99
+ <select id="qaFile" class="file-selector">
100
+ <option value="">-- Aucun --</option>
101
+ </select>
102
+ </div>
103
+ <div class="form-group">
104
+ <label for="questionInput">Votre question :</label>
105
+ <textarea id="questionInput" rows="4" placeholder="Posez une question sur le document ou l'image..."></textarea>
106
+ </div>
107
+ <div class="action-area">
108
+ <button id="askBtn" class="action-btn">
109
+ <span class="btn-icon">💡</span> Poser la Question
110
+ </button>
111
+ <div class="loading-spinner" id="qaSpinner"></div>
112
+ </div>
113
+ <div id="answerResult" class="result-box">
114
+ <p class="placeholder">La réponse à votre question apparaîtra ici...</p>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Onglet À propos -->
119
+ <div id="about" class="tab-content">
120
+ <h2><i class="icon">ℹ️</i> À propos</h2>
121
+ <div class="about-content">
122
+ <div class="about-card">
123
+ <h3>Fonctionnalités</h3>
124
+ <ul>
125
+ <li>📄 Résumé automatique de documents</li>
126
+ <li>🖼️ Description d'images par IA</li>
127
+ <li>❓ Réponses à vos questions</li>
128
+ <li>⚡ Traitement rapide</li>
129
+ </ul>
130
+ </div>
131
+ <div class="about-card">
132
+ <h3>Technologies</h3>
133
+ <ul>
134
+ <li>🧠 Modèles Hugging Face</li>
135
+ <li>🐍 Backend FastAPI</li>
136
+ <li>⚛️ Frontend moderne</li>
137
+ <li>🐳 Dockerisé</li>
138
+ </ul>
139
+ </div>
140
+ </div>
141
+ <div class="footer">
142
+ <p>Projet développé par Kaloun & Tif - © 2025</p>
143
+ <p class="version">Version 1.0.0</p>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <script src="script.js"></script>
149
+ </body>
150
+ </html>
script.js ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Configuration Axios
2
+ axios.defaults.baseURL = window.location.origin.includes('hf.space')
3
+ ? window.location.origin
4
+ : 'http://localhost:8000';
5
+
6
+ console.log("API base URL:", axios.defaults.baseURL);
7
+
8
+ // Variables globales
9
+ let uploadedFiles = [];
10
+ const MAX_FILE_SIZE_MB = 50;
11
+ const SUPPORTED_TYPES = ['pdf', 'docx', 'pptx', 'xlsx', 'jpg', 'jpeg', 'png'];
12
+
13
+ // Éléments DOM
14
+ const elements = {
15
+ fileInput: document.getElementById('fileInput'),
16
+ dropArea: document.getElementById('dropArea'),
17
+ fileList: document.getElementById('fileList'),
18
+ uploadStatus: document.getElementById('uploadStatus'),
19
+ summaryFile: document.getElementById('summaryFile'),
20
+ captionFile: document.getElementById('captionFile'),
21
+ qaFile: document.getElementById('qaFile'),
22
+ summarizeBtn: document.getElementById('summarizeBtn'),
23
+ captionBtn: document.getElementById('captionBtn'),
24
+ askBtn: document.getElementById('askBtn'),
25
+ questionInput: document.getElementById('questionInput'),
26
+ summaryResult: document.getElementById('summaryResult'),
27
+ captionResult: document.getElementById('captionResult'),
28
+ answerResult: document.getElementById('answerResult'),
29
+ imagePreview: document.getElementById('imagePreview'),
30
+ summarySpinner: document.getElementById('summarySpinner'),
31
+ captionSpinner: document.getElementById('captionSpinner'),
32
+ qaSpinner: document.getElementById('qaSpinner')
33
+ };
34
+
35
+ // Initialisation
36
+ document.addEventListener('DOMContentLoaded', () => {
37
+ initVideoBackground();
38
+ setupEventListeners();
39
+ updateFileSelectors();
40
+ });
41
+
42
+ function initVideoBackground() {
43
+ const video = document.getElementById('bg-video');
44
+ if (video) {
45
+ try {
46
+ video.playbackRate = 0.8;
47
+ video.muted = true;
48
+ video.playsInline = true;
49
+ video.play().catch(e => console.log("Video play error:", e));
50
+ } catch (e) {
51
+ console.log("Video initialization error:", e);
52
+ }
53
+
54
+ video.onerror = () => {
55
+ document.body.style.background = "linear-gradient(135deg, #4a6fa5 0%, #166088 100%)";
56
+ };
57
+ }
58
+ }
59
+
60
+ function setupEventListeners() {
61
+ // Gestion de l'upload
62
+ if (elements.dropArea) {
63
+ elements.dropArea.addEventListener('click', () => elements.fileInput.click());
64
+ elements.dropArea.addEventListener('dragover', handleDragOver);
65
+ elements.dropArea.addEventListener('dragleave', handleDragLeave);
66
+ elements.dropArea.addEventListener('drop', handleDrop);
67
+ }
68
+
69
+ elements.fileInput.addEventListener('change', handleFileSelect);
70
+
71
+ // Boutons d'action
72
+ if (elements.summarizeBtn) {
73
+ elements.summarizeBtn.addEventListener('click', summarizeDocument);
74
+ }
75
+
76
+ if (elements.captionBtn) {
77
+ elements.captionBtn.addEventListener('click', generateCaption);
78
+ }
79
+
80
+ if (elements.askBtn) {
81
+ elements.askBtn.addEventListener('click', answerQuestion);
82
+ }
83
+ }
84
+
85
+ // Gestion des fichiers
86
+ function handleDragOver(e) {
87
+ e.preventDefault();
88
+ elements.dropArea.style.borderColor = '#4a6fa5';
89
+ elements.dropArea.style.backgroundColor = 'rgba(74, 111, 165, 0.1)';
90
+ }
91
+
92
+ function handleDragLeave() {
93
+ elements.dropArea.style.borderColor = '#4a6fa5';
94
+ elements.dropArea.style.backgroundColor = 'rgba(74, 111, 165, 0.05)';
95
+ }
96
+
97
+ function handleDrop(e) {
98
+ e.preventDefault();
99
+ handleDragLeave();
100
+
101
+ if (e.dataTransfer.files.length) {
102
+ elements.fileInput.files = e.dataTransfer.files;
103
+ handleFiles(elements.fileInput.files);
104
+ }
105
+ }
106
+
107
+ function handleFileSelect() {
108
+ if (elements.fileInput.files.length) {
109
+ handleFiles(elements.fileInput.files);
110
+ }
111
+ }
112
+
113
+ async function handleFiles(files) {
114
+ const validFiles = Array.from(files).filter(file => {
115
+ const ext = file.name.split('.').pop().toLowerCase();
116
+ const isValidType = SUPPORTED_TYPES.includes(ext);
117
+ const isValidSize = file.size <= MAX_FILE_SIZE_MB * 1024 * 1024;
118
+
119
+ if (!isValidType) {
120
+ showError(elements.uploadStatus, `Type non supporté: ${file.name}`);
121
+ return false;
122
+ }
123
+
124
+ if (!isValidSize) {
125
+ showError(elements.uploadStatus, `Fichier trop volumineux (max ${MAX_FILE_SIZE_MB}MB): ${file.name}`);
126
+ return false;
127
+ }
128
+
129
+ return true;
130
+ });
131
+
132
+ if (!validFiles.length) return;
133
+
134
+ showLoading(elements.uploadStatus, `Upload de ${validFiles.length} fichier(s)...`);
135
+
136
+ const formData = new FormData();
137
+ validFiles.forEach(file => formData.append('files', file));
138
+
139
+ try {
140
+ const response = await axios.post('/api/upload', formData, {
141
+ headers: { 'Content-Type': 'multipart/form-data' },
142
+ timeout: 60000
143
+ });
144
+
145
+ uploadedFiles = [...uploadedFiles, ...response.data];
146
+ updateFileSelectors();
147
+ renderFileList();
148
+ showSuccess(elements.uploadStatus, `${validFiles.length} fichier(s) uploadé(s) avec succès !`);
149
+ } catch (error) {
150
+ handleUploadError(error);
151
+ } finally {
152
+ elements.fileInput.value = '';
153
+ }
154
+ }
155
+
156
+ function renderFileList() {
157
+ if (!elements.fileList) return;
158
+
159
+ if (uploadedFiles.length === 0) {
160
+ elements.fileList.innerHTML = '<p class="empty-message">Aucun fichier uploadé</p>';
161
+ return;
162
+ }
163
+
164
+ elements.fileList.innerHTML = uploadedFiles.map(file => `
165
+ <div class="file-item" data-file-id="${file.file_id}">
166
+ <div class="file-info">
167
+ <span class="file-icon">${getFileIcon(file.file_type)}</span>
168
+ <span class="file-name">${file.file_name}</span>
169
+ <span class="file-type">${file.file_type.toUpperCase()}</span>
170
+ </div>
171
+ </div>
172
+ `).join('');
173
+ }
174
+
175
+ function updateFileSelectors() {
176
+ const selectors = [
177
+ { element: elements.summaryFile, types: ['pdf', 'docx', 'pptx', 'xlsx'] },
178
+ { element: elements.captionFile, types: ['jpg', 'jpeg', 'png'] },
179
+ { element: elements.qaFile, types: ['pdf', 'docx', 'pptx', 'xlsx', 'jpg', 'jpeg', 'png'] }
180
+ ];
181
+
182
+ selectors.forEach(({ element, types }) => {
183
+ if (!element) return;
184
+
185
+ const currentValue = element.value;
186
+ element.innerHTML = element.id === 'qaFile'
187
+ ? '<option value="">-- Aucun --</option>'
188
+ : `<option value="">-- Choisir ${element.id.includes('summary') ? 'un document' : 'une image'} --</option>`;
189
+
190
+ uploadedFiles
191
+ .filter(file => types.includes(file.file_type.toLowerCase()))
192
+ .forEach(file => {
193
+ const option = new Option(
194
+ `${file.file_name} (${file.file_type.toUpperCase()})`,
195
+ file.file_id
196
+ );
197
+ element.add(option);
198
+ });
199
+
200
+ element.value = currentValue;
201
+ });
202
+ }
203
+
204
+ // Fonctions de traitement
205
+ async function summarizeDocument() {
206
+ if (!elements.summaryFile || !elements.summaryResult) return;
207
+
208
+ const fileId = elements.summaryFile.value;
209
+ if (!fileId) {
210
+ showError(elements.summaryResult, 'Veuillez sélectionner un document');
211
+ return;
212
+ }
213
+
214
+ showLoading(elements.summaryResult, 'Génération du résumé en cours...');
215
+ toggleSpinner(elements.summarySpinner, true);
216
+ disableButton(elements.summarizeBtn, true);
217
+
218
+ try {
219
+ const response = await axios.post('/api/summarize', {
220
+ file_id: fileId,
221
+ max_length: 200
222
+ }, { timeout: 120000 });
223
+
224
+ elements.summaryResult.innerHTML = `
225
+ <h3>Résumé du document</h3>
226
+ <div class="summary-content">${response.data.summary}</div>
227
+ `;
228
+ } catch (error) {
229
+ handleProcessingError(error, elements.summaryResult, 'résumé');
230
+ } finally {
231
+ toggleSpinner(elements.summarySpinner, false);
232
+ disableButton(elements.summarizeBtn, false);
233
+ }
234
+ }
235
+
236
+ async function generateCaption() {
237
+ if (!elements.captionFile || !elements.captionResult) return;
238
+
239
+ const fileId = elements.captionFile.value;
240
+ if (!fileId) {
241
+ showError(elements.captionResult, 'Veuillez sélectionner une image');
242
+ return;
243
+ }
244
+
245
+ // Afficher l'aperçu
246
+ const file = uploadedFiles.find(f => f.file_id === fileId);
247
+ if (file && elements.imagePreview) {
248
+ try {
249
+ const response = await axios.get(`/api/file/${fileId}`, { responseType: 'blob' });
250
+ const url = URL.createObjectURL(response.data);
251
+ elements.imagePreview.innerHTML = `<img src="${url}" alt="Image sélectionnée">`;
252
+ } catch (error) {
253
+ console.error("Error loading image preview:", error);
254
+ }
255
+ }
256
+
257
+ showLoading(elements.captionResult, 'Analyse de l\'image en cours...');
258
+ toggleSpinner(elements.captionSpinner, true);
259
+ disableButton(elements.captionBtn, true);
260
+
261
+ try {
262
+ const response = await axios.post('/api/caption', { file_id: fileId }, { timeout: 60000 });
263
+ elements.captionResult.innerHTML = `
264
+ <h3>Description de l'image</h3>
265
+ <p>${response.data.caption}</p>
266
+ `;
267
+ } catch (error) {
268
+ handleProcessingError(error, elements.captionResult, 'description');
269
+ } finally {
270
+ toggleSpinner(elements.captionSpinner, false);
271
+ disableButton(elements.captionBtn, false);
272
+ }
273
+ }
274
+
275
+ async function answerQuestion() {
276
+ if (!elements.questionInput || !elements.answerResult) return;
277
+
278
+ const question = elements.questionInput.value.trim();
279
+ if (!question) {
280
+ showError(elements.answerResult, 'Veuillez saisir une question');
281
+ return;
282
+ }
283
+
284
+ const fileId = elements.qaFile ? elements.qaFile.value : null;
285
+ showLoading(elements.answerResult, 'Recherche de la réponse...');
286
+ toggleSpinner(elements.qaSpinner, true);
287
+ disableButton(elements.askBtn, true);
288
+
289
+ try {
290
+ const response = await axios.post('/api/answer', {
291
+ question,
292
+ file_id: fileId || undefined
293
+ }, { timeout: 120000 });
294
+
295
+ elements.answerResult.innerHTML = `
296
+ <h3>Réponse</h3>
297
+ <div class="answer-content">${response.data.answer}</div>
298
+ `;
299
+ } catch (error) {
300
+ handleProcessingError(error, elements.answerResult, 'réponse');
301
+ } finally {
302
+ toggleSpinner(elements.qaSpinner, false);
303
+ disableButton(elements.askBtn, false);
304
+ }
305
+ }
306
+
307
+ // Fonctions utilitaires
308
+ function getFileIcon(fileType) {
309
+ const icons = {
310
+ pdf: '📄',
311
+ docx: '📝',
312
+ pptx: '📊',
313
+ xlsx: '📈',
314
+ jpg: '🖼️',
315
+ jpeg: '🖼️',
316
+ png: '🖼️'
317
+ };
318
+ return icons[fileType.toLowerCase()] || '📁';
319
+ }
320
+
321
+ function showLoading(element, message) {
322
+ if (!element) return;
323
+ element.innerHTML = `
324
+ <div class="status-message">
325
+ <div class="loading-spinner"></div>
326
+ ${message}
327
+ </div>
328
+ `;
329
+ }
330
+
331
+ function showSuccess(element, message) {
332
+ if (!element) return;
333
+ element.innerHTML = `
334
+ <div class="status-message success">
335
+ ✓ ${message}
336
+ </div>
337
+ `;
338
+
339
+ if (element === elements.uploadStatus) {
340
+ setTimeout(() => {
341
+ element.innerHTML = '';
342
+ }, 3000);
343
+ }
344
+ }
345
+
346
+ function showError(element, message) {
347
+ if (!element) return;
348
+ element.innerHTML = `
349
+ <div class="status-message error">
350
+ ✗ ${message}
351
+ </div>
352
+ `;
353
+ }
354
+
355
+ function handleUploadError(error) {
356
+ let message = 'Échec de l\'upload';
357
+
358
+ if (error.response) {
359
+ message += `: ${error.response.data.detail || error.response.statusText}`;
360
+ } else if (error.code === 'ECONNABORTED') {
361
+ message = 'Délai d\'attente dépassé (60s) - essayez avec des fichiers plus petits';
362
+ } else if (error.message.includes('Network Error')) {
363
+ message = 'Impossible de se connecter au serveur - vérifiez votre connexion';
364
+ } else {
365
+ message += `: ${error.message}`;
366
+ }
367
+
368
+ showError(elements.uploadStatus, message);
369
+ }
370
+
371
+ function handleProcessingError(error, element, action) {
372
+ let message = `Échec de la génération du ${action}`;
373
+
374
+ if (error.response) {
375
+ message += `: ${error.response.data.detail || error.response.statusText}`;
376
+ } else if (error.code === 'ECONNABORTED') {
377
+ message = `Délai d'attente dépassé pour le ${action}`;
378
+ } else {
379
+ message += `: ${error.message}`;
380
+ }
381
+
382
+ showError(element, message);
383
+ }
384
+
385
+ function toggleSpinner(spinnerElement, show) {
386
+ if (spinnerElement) {
387
+ spinnerElement.style.display = show ? 'block' : 'none';
388
+ }
389
+ }
390
+
391
+ function disableButton(buttonElement, disable) {
392
+ if (buttonElement) {
393
+ buttonElement.disabled = disable;
394
+ buttonElement.style.opacity = disable ? 0.7 : 1;
395
+ buttonElement.style.cursor = disable ? 'not-allowed' : 'pointer';
396
+ }
397
+ }
398
+
399
+ function openTab(tabName) {
400
+ // Masquer tous les onglets
401
+ document.querySelectorAll('.tab-content').forEach(tab => {
402
+ tab.classList.remove('active');
403
+ });
404
+
405
+ // Désactiver tous les boutons
406
+ document.querySelectorAll('.tab-button').forEach(btn => {
407
+ btn.classList.remove('active');
408
+ });
409
+
410
+ // Activer l'onglet sélectionné
411
+ const tab = document.getElementById(tabName);
412
+ if (tab) tab.classList.add('active');
413
+
414
+ // Activer le bouton sélectionné
415
+ event.currentTarget.classList.add('active');
416
+ }
style.css ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #4a6fa5;
3
+ --secondary-color: #166088;
4
+ --accent-color: #4fc3f7;
5
+ --background-color: #f8f9fa;
6
+ --text-color: #333;
7
+ --light-gray: #e9ecef;
8
+ --dark-gray: #6c757d;
9
+ --success-color: #28a745;
10
+ --error-color: #dc3545;
11
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
12
+ }
13
+
14
+ /* Reset et styles de base */
15
+ * {
16
+ box-sizing: border-box;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
23
+ line-height: 1.6;
24
+ color: var(--text-color);
25
+ overflow-x: hidden;
26
+ }
27
+
28
+ /* Fond vidéo */
29
+ #bg-video {
30
+ position: fixed;
31
+ right: 0;
32
+ bottom: 0;
33
+ min-width: 100%;
34
+ min-height: 100%;
35
+ z-index: -1;
36
+ opacity: 1;
37
+ object-fit: cover;
38
+ }
39
+
40
+ /* Conteneur principal */
41
+ .container {
42
+ max-width: 1200px;
43
+ margin: 2rem auto;
44
+ background-color: rgba(255, 255, 255, 0.863);
45
+ padding: 2rem;
46
+ border-radius: 10px;
47
+ box-shadow: var(--box-shadow);
48
+ backdrop-filter: blur(2px);
49
+ }
50
+
51
+ header {
52
+ text-align: center;
53
+ margin-bottom: 2rem;
54
+ padding-bottom: 1rem;
55
+ border-bottom: 1px solid var(--light-gray);
56
+ }
57
+
58
+ header h1 {
59
+ color: var(--primary-color);
60
+ font-size: 2.5rem;
61
+ margin-bottom: 0.5rem;
62
+ }
63
+
64
+ .subtitle {
65
+ color: var(--dark-gray);
66
+ font-size: 1.1rem;
67
+ }
68
+
69
+ /* Onglets */
70
+ .tabs {
71
+ display: flex;
72
+ border-bottom: 1px solid var(--light-gray);
73
+ margin-bottom: 1.5rem;
74
+ overflow-x: auto;
75
+ }
76
+
77
+ .tab-button {
78
+ padding: 0.75rem 1.5rem;
79
+ background: none;
80
+ border: none;
81
+ cursor: pointer;
82
+ font-size: 1rem;
83
+ color: var(--dark-gray);
84
+ transition: all 0.3s;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .tab-button:hover {
89
+ color: var(--primary-color);
90
+ }
91
+
92
+ .tab-button.active {
93
+ color: var(--primary-color);
94
+ border-bottom: 3px solid var(--primary-color);
95
+ font-weight: 600;
96
+ }
97
+
98
+ .tab-content {
99
+ display: none;
100
+ padding: 1rem 0;
101
+ animation: fadeIn 0.5s ease;
102
+ }
103
+
104
+ .tab-content.active {
105
+ display: block;
106
+ }
107
+
108
+ @keyframes fadeIn {
109
+ from { opacity: 0; transform: translateY(10px); }
110
+ to { opacity: 1; transform: translateY(0); }
111
+ }
112
+
113
+ /* Zone d'upload */
114
+ .upload-area {
115
+ border: 2px dashed var(--primary-color);
116
+ border-radius: 8px;
117
+ padding: 3rem;
118
+ text-align: center;
119
+ cursor: pointer;
120
+ transition: all 0.3s;
121
+ margin-bottom: 1.5rem;
122
+ background-color: rgba(74, 111, 165, 0.05);
123
+ }
124
+
125
+ .upload-area:hover {
126
+ background-color: rgba(74, 111, 165, 0.1);
127
+ transform: translateY(-2px);
128
+ }
129
+
130
+ .upload-icon {
131
+ font-size: 2.5rem;
132
+ margin-bottom: 1rem;
133
+ color: var(--primary-color);
134
+ }
135
+
136
+ .file-types {
137
+ color: var(--dark-gray);
138
+ font-size: 0.9rem;
139
+ margin-top: 0.5rem;
140
+ }
141
+
142
+ #fileInput {
143
+ display: none;
144
+ }
145
+
146
+ /* Liste de fichiers */
147
+ .file-list {
148
+ margin: 1.5rem 0;
149
+ }
150
+
151
+ .file-item {
152
+ display: flex;
153
+ justify-content: space-between;
154
+ align-items: center;
155
+ padding: 0.75rem 1rem;
156
+ border: 1px solid var(--light-gray);
157
+ border-radius: 6px;
158
+ margin-bottom: 0.75rem;
159
+ background-color: white;
160
+ transition: transform 0.2s;
161
+ }
162
+
163
+ .file-item:hover {
164
+ transform: translateX(5px);
165
+ }
166
+
167
+ .file-info {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.75rem;
171
+ }
172
+
173
+ .file-icon {
174
+ font-size: 1.2rem;
175
+ }
176
+
177
+ .file-name {
178
+ font-weight: 500;
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ white-space: nowrap;
182
+ max-width: 300px;
183
+ }
184
+
185
+ .file-type {
186
+ color: var(--dark-gray);
187
+ font-size: 0.8rem;
188
+ background-color: var(--light-gray);
189
+ padding: 0.2rem 0.5rem;
190
+ border-radius: 4px;
191
+ margin-left: 0.5rem;
192
+ }
193
+
194
+ /* Formulaires */
195
+ .form-group {
196
+ margin-bottom: 1.5rem;
197
+ }
198
+
199
+ .form-group label {
200
+ display: block;
201
+ margin-bottom: 0.5rem;
202
+ font-weight: 500;
203
+ color: var(--primary-color);
204
+ }
205
+
206
+ .file-selector, textarea {
207
+ width: 100%;
208
+ padding: 0.75rem;
209
+ border: 1px solid var(--light-gray);
210
+ border-radius: 6px;
211
+ font-size: 1rem;
212
+ transition: border-color 0.3s;
213
+ }
214
+
215
+ .file-selector:focus, textarea:focus {
216
+ outline: none;
217
+ border-color: var(--primary-color);
218
+ box-shadow: 0 0 0 2px rgba(74, 111, 165, 0.2);
219
+ }
220
+
221
+ textarea {
222
+ resize: vertical;
223
+ min-height: 120px;
224
+ }
225
+
226
+ /* Boutons */
227
+ .action-btn {
228
+ background-color: var(--primary-color);
229
+ color: white;
230
+ border: none;
231
+ padding: 0.75rem 1.5rem;
232
+ border-radius: 6px;
233
+ cursor: pointer;
234
+ font-size: 1rem;
235
+ font-weight: 500;
236
+ transition: all 0.3s;
237
+ display: inline-flex;
238
+ align-items: center;
239
+ gap: 0.5rem;
240
+ }
241
+
242
+ .action-btn:hover {
243
+ background-color: var(--secondary-color);
244
+ transform: translateY(-2px);
245
+ box-shadow: var(--box-shadow);
246
+ }
247
+
248
+ .action-btn:disabled {
249
+ background-color: var(--light-gray);
250
+ cursor: not-allowed;
251
+ transform: none;
252
+ }
253
+
254
+ .btn-icon {
255
+ font-size: 1.2rem;
256
+ }
257
+
258
+ .action-area {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 1rem;
262
+ margin: 1.5rem 0;
263
+ }
264
+
265
+ /* Résultats */
266
+ .result-box {
267
+ margin: 1.5rem 0;
268
+ padding: 1.5rem;
269
+ border-radius: 8px;
270
+ background-color: white;
271
+ border: 1px solid var(--light-gray);
272
+ min-height: 150px;
273
+ }
274
+
275
+ .result-box h3 {
276
+ color: var(--primary-color);
277
+ margin-bottom: 1rem;
278
+ }
279
+
280
+ .placeholder {
281
+ color: var(--dark-gray);
282
+ font-style: italic;
283
+ text-align: center;
284
+ margin: 2rem 0;
285
+ }
286
+
287
+ /* Aperçu image */
288
+ .image-preview {
289
+ margin: 1.5rem 0;
290
+ text-align: center;
291
+ }
292
+
293
+ .image-preview img {
294
+ max-width: 100%;
295
+ max-height: 400px;
296
+ border-radius: 8px;
297
+ box-shadow: var(--box-shadow);
298
+ }
299
+
300
+ .preview-placeholder {
301
+ padding: 3rem;
302
+ border: 2px dashed var(--light-gray);
303
+ border-radius: 8px;
304
+ color: var(--dark-gray);
305
+ }
306
+
307
+ .placeholder-icon {
308
+ font-size: 3rem;
309
+ margin-bottom: 1rem;
310
+ opacity: 0.5;
311
+ }
312
+
313
+ /* Messages d'état */
314
+ .status-message {
315
+ padding: 1rem;
316
+ border-radius: 6px;
317
+ margin: 1rem 0;
318
+ display: flex;
319
+ align-items: center;
320
+ gap: 0.75rem;
321
+ }
322
+
323
+ .success {
324
+ background-color: rgba(40, 167, 69, 0.15);
325
+ color: var(--success-color);
326
+ border: 1px solid rgba(40, 167, 69, 0.3);
327
+ }
328
+
329
+ .error {
330
+ background-color: rgba(220, 53, 69, 0.15);
331
+ color: var(--error-color);
332
+ border: 1px solid rgba(220, 53, 69, 0.3);
333
+ }
334
+
335
+ .loading-spinner {
336
+ width: 24px;
337
+ height: 24px;
338
+ border: 3px solid rgba(74, 111, 165, 0.3);
339
+ border-top-color: var(--primary-color);
340
+ border-radius: 50%;
341
+ animation: spin 1s linear infinite;
342
+ display: none;
343
+ }
344
+
345
+ @keyframes spin {
346
+ to { transform: rotate(360deg); }
347
+ }
348
+
349
+ /* Page À propos */
350
+ .about-content {
351
+ display: grid;
352
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
353
+ gap: 1.5rem;
354
+ margin: 2rem 0;
355
+ }
356
+
357
+ .about-card {
358
+ background-color: white;
359
+ border-radius: 8px;
360
+ padding: 1.5rem;
361
+ box-shadow: var(--box-shadow);
362
+ }
363
+
364
+ .about-card h3 {
365
+ color: var(--primary-color);
366
+ margin-bottom: 1rem;
367
+ padding-bottom: 0.5rem;
368
+ border-bottom: 1px solid var(--light-gray);
369
+ }
370
+
371
+ .about-card ul {
372
+ list-style-type: none;
373
+ }
374
+
375
+ .about-card li {
376
+ padding: 0.5rem 0;
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 0.75rem;
380
+ }
381
+
382
+ .footer {
383
+ text-align: center;
384
+ margin-top: 3rem;
385
+ color: var(--dark-gray);
386
+ font-size: 0.9rem;
387
+ }
388
+
389
+ .version {
390
+ font-size: 0.8rem;
391
+ opacity: 0.7;
392
+ }
393
+
394
+ /* Responsive */
395
+ @media (max-width: 768px) {
396
+ .container {
397
+ margin: 1rem;
398
+ padding: 1.5rem;
399
+ }
400
+
401
+ header h1 {
402
+ font-size: 2rem;
403
+ }
404
+
405
+ .upload-area {
406
+ padding: 2rem 1rem;
407
+ }
408
+
409
+ .file-name {
410
+ max-width: 200px;
411
+ }
412
+
413
+ .about-content {
414
+ grid-template-columns: 1fr;
415
+ }
416
+
417
+ #bg-video {
418
+ display: none;
419
+ }
420
+
421
+ body {
422
+ background: linear-gradient(135deg, #4a6fa5 0%, #166088 100%);
423
+ }
424
+ }
425
+
426
+ @media (max-width: 480px) {
427
+ .tab-button {
428
+ padding: 0.75rem;
429
+ font-size: 0.9rem;
430
+ }
431
+
432
+ .file-name {
433
+ max-width: 150px;
434
+ }
435
+ }