ANESBENYELLES commited on
Commit
fb63210
·
verified ·
1 Parent(s): f7a0dad
Files changed (5) hide show
  1. Dockerfile +44 -0
  2. backend_fastapi.py +293 -0
  3. frontend/index.html +169 -0
  4. frontend/script.js +277 -0
  5. frontend/styles.css +316 -0
Dockerfile ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Install essential tools
4
+ RUN apt-get update && apt-get install -y \
5
+ poppler-utils \
6
+ pandoc \
7
+ && apt-get clean \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Set working directory
11
+ WORKDIR /app
12
+
13
+ # Copy requirements file first for better caching
14
+ COPY requirements.txt ./
15
+
16
+ # Install dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt \
18
+ && pip install python-multipart uvicorn fastapi pydantic requests jinja2
19
+
20
+ # Create directories and set permissions
21
+ RUN mkdir -p /app/frontend \
22
+ && mkdir -p /tmp/.matplotlib \
23
+ && chmod 777 /tmp/.matplotlib
24
+
25
+ # Set environment variables
26
+ ENV MPLCONFIGDIR=/tmp/.matplotlib
27
+ ENV PYTHONUNBUFFERED=1
28
+ # Uncomment and set your HF API key if needed
29
+ # ENV HF_API_KEY=your_huggingface_api_key
30
+
31
+ # Copy application code and static files
32
+ COPY backend_fastapi.py ./
33
+ COPY frontend ./frontend/
34
+
35
+ # Create requirements.txt if it doesn't exist yet
36
+ RUN if [ ! -f requirements.txt ]; then \
37
+ echo "fastapi==0.103.1\nuvicorn==0.23.2\npython-multipart==0.0.6\nrequests==2.31.0\nJinja2==3.1.2\npydantic==2.4.2" > requirements.txt; \
38
+ fi
39
+
40
+ # Expose port
41
+ EXPOSE 7860
42
+
43
+ # Start command
44
+ CMD ["uvicorn", "backend_fastapi:app", "--host", "0.0.0.0", "--port", "7860"]
backend_fastapi.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.templating import Jinja2Templates
4
+ from fastapi.responses import HTMLResponse, JSONResponse
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from typing import Optional
7
+ import requests
8
+ import os
9
+ import io
10
+ import tempfile
11
+ import logging
12
+ import subprocess
13
+
14
+ # Configuration du logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ app = FastAPI()
19
+
20
+ # Configuration CORS
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # Configuration des fichiers statiques
30
+ app.mount("/static", StaticFiles(directory="frontend"), name="static")
31
+ templates = Jinja2Templates(directory="frontend")
32
+
33
+ @app.get("/", response_class=HTMLResponse)
34
+ async def serve_frontend(request: Request):
35
+ return templates.TemplateResponse("index.html", {"request": request})
36
+
37
+ # Configuration Hugging Face
38
+ HF_API_KEY = os.getenv("HF_API_KEY", "")
39
+ HEADERS = {"Authorization": f"Bearer {HF_API_KEY}"} if HF_API_KEY else {}
40
+
41
+ HF_MODELS = {
42
+ "summary": "facebook/bart-large-cnn",
43
+ "qa": "deepset/roberta-base-squad2"
44
+ }
45
+
46
+ HF_API_URL = "https://api-inference.huggingface.co/models/"
47
+
48
+ def query_huggingface(model: str, payload: dict):
49
+ try:
50
+ api_url = f"{HF_API_URL}{model}"
51
+ logger.info(f"Requête à {api_url}")
52
+
53
+ response = requests.post(
54
+ api_url,
55
+ headers=HEADERS,
56
+ json=payload,
57
+ timeout=60
58
+ )
59
+
60
+ if response.status_code != 200:
61
+ logger.error(f"Erreur API Hugging Face: {response.status_code}, {response.text}")
62
+ return {"error": f"Erreur API: {response.status_code}"}
63
+
64
+ return response.json()
65
+ except Exception as e:
66
+ logger.error(f"Erreur API: {str(e)}")
67
+ return {"error": str(e)}
68
+
69
+ async def convert_to_text(file: UploadFile):
70
+ """Convertit différents formats de fichiers en texte avec gestion robuste des erreurs"""
71
+ try:
72
+ # Vérification du type de fichier
73
+ if not file.filename:
74
+ return "Aucun fichier fourni"
75
+
76
+ ext = os.path.splitext(file.filename)[1].lower()
77
+
78
+ # Lecture du contenu
79
+ content = await file.read()
80
+
81
+ # Traitement des fichiers texte
82
+ if ext == '.txt':
83
+ return content.decode('utf-8', errors='replace')
84
+
85
+ # Traitement des PDF avec pdftotext
86
+ elif ext == '.pdf':
87
+ try:
88
+ with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_pdf:
89
+ tmp_pdf.write(content)
90
+ tmp_pdf.flush()
91
+ tmp_pdf_path = tmp_pdf.name
92
+
93
+ try:
94
+ result = subprocess.run(
95
+ ["pdftotext", tmp_pdf_path, "-"],
96
+ capture_output=True,
97
+ text=True,
98
+ timeout=30
99
+ )
100
+
101
+ os.unlink(tmp_pdf_path) # Supprimer le fichier temporaire
102
+
103
+ if result.returncode == 0:
104
+ return result.stdout
105
+ else:
106
+ error_msg = result.stderr or "Erreur inconnue lors de la conversion PDF"
107
+ logger.error(f"PDF conversion failed: {error_msg}")
108
+ return f"Erreur de conversion PDF: {error_msg}"
109
+ except:
110
+ # S'assurer que le fichier temporaire est supprimé en cas d'erreur
111
+ if os.path.exists(tmp_pdf_path):
112
+ os.unlink(tmp_pdf_path)
113
+ raise
114
+
115
+ except FileNotFoundError:
116
+ logger.warning("pdftotext non installé")
117
+ return "Conversion PDF non disponible (pdftotext requis)"
118
+ except subprocess.TimeoutExpired:
119
+ return "Timeout lors de la conversion PDF"
120
+
121
+ # Traitement des fichiers Word avec pandoc
122
+ elif ext in ('.docx', '.doc'):
123
+ try:
124
+ with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_doc:
125
+ tmp_doc.write(content)
126
+ tmp_doc.flush()
127
+ tmp_doc_path = tmp_doc.name
128
+
129
+ try:
130
+ result = subprocess.run(
131
+ ["pandoc", "-t", "plain", tmp_doc_path],
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=30
135
+ )
136
+
137
+ os.unlink(tmp_doc_path) # Supprimer le fichier temporaire
138
+
139
+ if result.returncode == 0:
140
+ return result.stdout
141
+ else:
142
+ error_msg = result.stderr or "Erreur inconnue lors de la conversion DOCX"
143
+ logger.error(f"DOCX conversion failed: {error_msg}")
144
+ return f"Erreur de conversion DOCX: {error_msg}"
145
+ except:
146
+ # S'assurer que le fichier temporaire est supprimé en cas d'erreur
147
+ if os.path.exists(tmp_doc_path):
148
+ os.unlink(tmp_doc_path)
149
+ raise
150
+
151
+ except FileNotFoundError:
152
+ logger.warning("pandoc non installé")
153
+ return "Conversion DOCX non disponible (pandoc requis)"
154
+ except subprocess.TimeoutExpired:
155
+ return "Timeout lors de la conversion DOCX"
156
+
157
+ else:
158
+ return f"Format de fichier non supporté: {ext}"
159
+
160
+ except Exception as e:
161
+ logger.error(f"Erreur de conversion: {str(e)}")
162
+ return f"Erreur lors de la conversion du fichier: {str(e)}"
163
+
164
+ @app.post("/summarize", response_class=JSONResponse)
165
+ async def summarize_document(file: UploadFile = File(...)):
166
+ """Endpoint pour résumer des documents avec gestion améliorée des PDF"""
167
+ try:
168
+ logger.info(f"Traitement du fichier: {file.filename}")
169
+
170
+ text = await convert_to_text(file)
171
+ if not text or not text.strip():
172
+ raise HTTPException(400, "Fichier vide ou problème de conversion")
173
+
174
+ # Si le texte est un message d'erreur
175
+ if text.startswith(("Erreur de conversion", "Conversion", "Format non supporté")):
176
+ return {
177
+ "filename": file.filename,
178
+ "summary": text, # Retourne le message d'erreur comme "résumé"
179
+ "text_length": len(text),
180
+ "warning": True
181
+ }
182
+
183
+ # Limite la taille pour l'API
184
+ text_to_process = text[:3000] # Réduire pour plus de fiabilité
185
+
186
+ response = query_huggingface(HF_MODELS["summary"], {
187
+ "inputs": text_to_process,
188
+ "parameters": {"max_length": 150, "min_length": 30}
189
+ })
190
+
191
+ if "error" in response:
192
+ logger.error(f"Erreur de l'API HF: {response['error']}")
193
+ return {
194
+ "filename": file.filename,
195
+ "summary": f"Erreur lors de la génération du résumé: {response['error']}",
196
+ "text_length": len(text),
197
+ "warning": True
198
+ }
199
+
200
+ # Gérer différents formats de réponse possibles
201
+ summary_text = ""
202
+ if isinstance(response, list) and len(response) > 0:
203
+ if isinstance(response[0], dict) and "summary_text" in response[0]:
204
+ summary_text = response[0]["summary_text"]
205
+ elif isinstance(response[0], str):
206
+ summary_text = response[0]
207
+ elif isinstance(response, dict) and "summary_text" in response:
208
+ summary_text = response["summary_text"]
209
+
210
+ if not summary_text:
211
+ summary_text = "Le modèle n'a pas pu générer de résumé. Essayez avec un texte plus court ou plus clair."
212
+
213
+ return {
214
+ "filename": file.filename,
215
+ "summary": summary_text,
216
+ "text_length": len(text),
217
+ "warning": False
218
+ }
219
+ except HTTPException:
220
+ raise
221
+ except Exception as e:
222
+ logger.error(f"Erreur dans summarize: {str(e)}")
223
+ raise HTTPException(500, f"Erreur interne: {str(e)}")
224
+
225
+ @app.post("/answer-question", response_class=JSONResponse)
226
+ async def answer_question(
227
+ question: str = Form(...),
228
+ file: Optional[UploadFile] = File(None)
229
+ ):
230
+ """Endpoint pour répondre à des questions basées sur un document"""
231
+ try:
232
+ logger.info(f"Question reçue: {question}")
233
+ context = ""
234
+
235
+ if file:
236
+ logger.info(f"Traitement du fichier: {file.filename}")
237
+ context = await convert_to_text(file)
238
+
239
+ # Si le contexte est un message d'erreur
240
+ if context.startswith(("Erreur de conversion", "Conversion", "Format non supporté")):
241
+ return {
242
+ "question": question,
243
+ "answer": f"Problème avec le document: {context}",
244
+ "warning": True
245
+ }
246
+
247
+ # Si aucun fichier fourni, on répond juste à la question
248
+ if not context or not context.strip():
249
+ context = "Pas de contexte disponible."
250
+
251
+ # Limite la taille du contexte pour l'API
252
+ context_to_process = context[:3000] # Réduire pour plus de fiabilité
253
+
254
+ response = query_huggingface(HF_MODELS["qa"], {
255
+ "inputs": {
256
+ "question": question,
257
+ "context": context_to_process
258
+ }
259
+ })
260
+
261
+ if "error" in response:
262
+ logger.error(f"Erreur de l'API HF: {response['error']}")
263
+ return {
264
+ "question": question,
265
+ "answer": f"Erreur lors de l'analyse: {response['error']}",
266
+ "warning": True
267
+ }
268
+
269
+ # Gérer différents formats de réponse possibles
270
+ answer = ""
271
+ if isinstance(response, dict):
272
+ if "answer" in response:
273
+ answer = response["answer"]
274
+ elif "answer_text" in response:
275
+ answer = response["answer_text"]
276
+ elif "answers" in response and len(response["answers"]) > 0:
277
+ answer = response["answers"][0]["text"]
278
+
279
+ if not answer:
280
+ answer = "Je n'ai pas trouvé de réponse précise à cette question dans le document fourni."
281
+
282
+ return {
283
+ "question": question,
284
+ "answer": answer,
285
+ "warning": False
286
+ }
287
+ except Exception as e:
288
+ logger.error(f"Erreur dans answer-question: {str(e)}")
289
+ raise HTTPException(500, f"Erreur interne: {str(e)}")
290
+
291
+ if __name__ == "__main__":
292
+ import uvicorn
293
+ uvicorn.run(app, host="0.0.0.0", port=7860)
frontend/index.html ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AI Tools Interface</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <header>
13
+ <h1>AI Tools Interface</h1>
14
+ <p class="subtitle">Outils d'IA pour le traitement de documents et d'images</p>
15
+ </header>
16
+
17
+ <div class="tab-container">
18
+ <!-- Navigation des onglets -->
19
+ <nav class="tab-nav">
20
+ <button onclick="openTab('summarize')" class="tab-btn active" data-tab="summarize" aria-selected="true" aria-controls="summarize">
21
+ <i class="icon">📝</i> Résumer
22
+ </button>
23
+ <button onclick="openTab('interpret-image')" class="tab-btn" data-tab="interpret-image" aria-selected="false" aria-controls="interpret-image">
24
+ <i class="icon">🖼️</i> Légende Image
25
+ </button>
26
+ <button onclick="openTab('answer-question')" class="tab-btn" data-tab="answer-question" aria-selected="false" aria-controls="answer-question">
27
+ <i class="icon">❓</i> Réponse Question
28
+ </button>
29
+ <button onclick="openTab('generate-visualization')" class="tab-btn" data-tab="generate-visualization" aria-selected="false" aria-controls="generate-visualization">
30
+ <i class="icon">📊</i> Visualisation
31
+ </button>
32
+ <button onclick="openTab('translate-document')" class="tab-btn" data-tab="translate-document" aria-selected="false" aria-controls="translate-document">
33
+ <i class="icon">🌐</i> Traduire
34
+ </button>
35
+ </nav>
36
+
37
+ <!-- Contenu des onglets -->
38
+ <div class="tab-content-wrapper">
39
+ <!-- Onglet Résumer -->
40
+ <section id="summarize" class="tab-content" role="tabpanel" aria-labelledby="summarize-tab">
41
+ <h2>Résumer un document</h2>
42
+ <form id="summarize-form" class="form">
43
+ <div class="file-upload-container">
44
+ <input type="file" id="summarize-file" accept=".txt,.pdf,.docx,.pptx" class="hidden-file" required />
45
+ <label for="summarize-file" class="file-upload">
46
+ <span class="file-icon">📁</span>
47
+ <span class="file-label">Sélectionner un fichier (TXT, PDF, DOCX, PPTX)</span>
48
+ </label>
49
+ <div id="summarize-filename" class="filename-display"></div>
50
+ </div>
51
+ <button type="submit" class="btn btn-blue">
52
+ <span class="btn-icon">✨</span> Générer le résumé
53
+ </button>
54
+ </form>
55
+ <div id="summarize-result" class="result-box">
56
+ <div class="placeholder">Votre résumé apparaîtra ici...</div>
57
+ </div>
58
+ </section>
59
+
60
+ <!-- Onglet Légende Image -->
61
+ <section id="interpret-image" class="tab-content hidden" role="tabpanel" aria-labelledby="interpret-image-tab">
62
+ <h2>Générer une légende d'image</h2>
63
+ <form id="interpret-image-form" class="form">
64
+ <div class="file-upload-container">
65
+ <input type="file" id="interpret-image-file" accept="image/*" class="hidden-file" required />
66
+ <label for="interpret-image-file" class="file-upload">
67
+ <span class="file-icon">🖼️</span>
68
+ <span class="file-label">Sélectionner une image (JPG, PNG, etc.)</span>
69
+ </label>
70
+ <div id="interpret-image-filename" class="filename-display"></div>
71
+ </div>
72
+ <button type="submit" class="btn btn-purple">
73
+ <span class="btn-icon">🔍</span> Générer la légende
74
+ </button>
75
+ </form>
76
+ <div id="interpret-image-result" class="result-box">
77
+ <div class="placeholder">La description de votre image apparaîtra ici...</div>
78
+ </div>
79
+ </section>
80
+
81
+ <!-- Onglet Réponse Question -->
82
+ <section id="answer-question" class="tab-content hidden" role="tabpanel" aria-labelledby="answer-question-tab">
83
+ <h2>Poser une question à un document</h2>
84
+ <form id="answer-question-form" class="form">
85
+ <div class="form-group">
86
+ <label for="question-input" class="form-label">Votre question</label>
87
+ <textarea id="question-input" placeholder="Posez votre question ici..." class="textarea" rows="3" required></textarea>
88
+ </div>
89
+ <div class="file-upload-container">
90
+ <input type="file" id="answer-question-file" accept=".txt,.pdf,.docx" class="hidden-file" />
91
+ <label for="answer-question-file" class="file-upload">
92
+ <span class="file-icon">📄</span>
93
+ <span class="file-label">Document de référence (optionnel)</span>
94
+ </label>
95
+ <div id="answer-question-filename" class="filename-display"></div>
96
+ </div>
97
+ <button type="submit" class="btn btn-green">
98
+ <span class="btn-icon">💡</span> Obtenir la réponse
99
+ </button>
100
+ </form>
101
+ <div id="answer-question-result" class="result-box">
102
+ <div class="placeholder">La réponse à votre question apparaîtra ici...</div>
103
+ </div>
104
+ </section>
105
+
106
+ <!-- Onglet Visualisation -->
107
+ <section id="generate-visualization" class="tab-content hidden" role="tabpanel" aria-labelledby="generate-visualization-tab">
108
+ <h2>Générer une visualisation</h2>
109
+ <form id="generate-visualization-form" class="form">
110
+ <div class="file-upload-container">
111
+ <input type="file" id="visualization-file" accept=".xlsx,.xls,.csv" class="hidden-file" required />
112
+ <label for="visualization-file" class="file-upload">
113
+ <span class="file-icon">📈</span>
114
+ <span class="file-label">Sélectionner un fichier Excel/CSV</span>
115
+ </label>
116
+ <div id="visualization-filename" class="filename-display"></div>
117
+ </div>
118
+ <div class="form-group">
119
+ <label for="visualization-request" class="form-label">Type de visualisation</label>
120
+ <input type="text" id="visualization-request" placeholder="Ex: histogramme des ventes par mois" class="input-text" required>
121
+ </div>
122
+ <button type="submit" class="btn btn-indigo">
123
+ <span class="btn-icon">📊</span> Générer le graphique
124
+ </button>
125
+ </form>
126
+ <div id="generate-visualization-result" class="result-box">
127
+ <div class="placeholder">Votre visualisation apparaîtra ici...</div>
128
+ </div>
129
+ </section>
130
+
131
+ <!-- Onglet Traduction -->
132
+ <section id="translate-document" class="tab-content hidden" role="tabpanel" aria-labelledby="translate-document-tab">
133
+ <h2>Traduire un document</h2>
134
+ <form id="translate-document-form" class="form">
135
+ <div class="file-upload-container">
136
+ <input type="file" id="translate-file" accept=".txt,.docx,.pdf" class="hidden-file" required />
137
+ <label for="translate-file" class="file-upload">
138
+ <span class="file-icon">📑</span>
139
+ <span class="file-label">Sélectionner un document</span>
140
+ </label>
141
+ <div id="translate-filename" class="filename-display"></div>
142
+ </div>
143
+ <div class="form-group">
144
+ <label for="target-language" class="form-label">Langue cible</label>
145
+ <select id="target-language" class="select" required>
146
+ <option value="" disabled selected>Choisir une langue</option>
147
+ <option value="en">Anglais</option>
148
+ <option value="fr">Français</option>
149
+ <option value="es">Espagnol</option>
150
+ <option value="de">Allemand</option>
151
+ <option value="it">Italien</option>
152
+ <option value="pt">Portugais</option>
153
+ </select>
154
+ </div>
155
+ <button type="submit" class="btn btn-pink">
156
+ <span class="btn-icon">🔤</span> Traduire
157
+ </button>
158
+ </form>
159
+ <div id="translate-document-result" class="result-box">
160
+ <div class="placeholder">La traduction apparaîtra ici...</div>
161
+ </div>
162
+ </section>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <script src="/static/script.js"></script>
168
+ </body>
169
+ </html>
frontend/script.js ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Constante pour l'URL de base
2
+ const BASE_URL = window.location.origin; // Utiliser l'URL complète pour éviter les erreurs CORS
3
+
4
+ // Gestion des onglets
5
+ function openTab(tabName) {
6
+ // Liste des onglets disponibles
7
+ const tabs = ['summarize', 'answer-question', 'interpret-image', 'generate-visualization', 'translate-document'];
8
+
9
+ // Cacher/montrer les sections appropriées
10
+ tabs.forEach(tab => {
11
+ const element = document.getElementById(tab);
12
+ if (element) {
13
+ element.classList.toggle('hidden', tab !== tabName);
14
+ }
15
+ });
16
+
17
+ // Marquer l'onglet actif
18
+ document.querySelectorAll('.tab-btn').forEach(btn => {
19
+ btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
20
+ btn.setAttribute('aria-selected', btn.getAttribute('data-tab') === tabName);
21
+ });
22
+ }
23
+
24
+ // Gestion optimisée des uploads de fichiers
25
+ function setupFileUpload(inputId) {
26
+ const input = document.getElementById(inputId);
27
+ if (!input) return;
28
+
29
+ const filenameDisplay = document.getElementById(`${inputId}-filename`);
30
+
31
+ input.addEventListener('change', function() {
32
+ if (this.files && this.files[0]) {
33
+ // Afficher le nom du fichier
34
+ const fileName = this.files[0].name;
35
+ if (filenameDisplay) {
36
+ filenameDisplay.textContent = fileName;
37
+ filenameDisplay.title = fileName;
38
+ }
39
+
40
+ // Changer le texte du label
41
+ const label = this.nextElementSibling;
42
+ const fileLabel = label?.querySelector('.file-label');
43
+ if (fileLabel) {
44
+ fileLabel.textContent = "Fichier sélectionné";
45
+ }
46
+ } else {
47
+ // Réinitialiser
48
+ if (filenameDisplay) {
49
+ filenameDisplay.textContent = "";
50
+ }
51
+
52
+ const label = this.nextElementSibling;
53
+ const fileLabel = label?.querySelector('.file-label');
54
+ if (fileLabel) {
55
+ fileLabel.textContent = "Sélectionner un fichier";
56
+ }
57
+ }
58
+ });
59
+ }
60
+
61
+ // Indicateur de chargement
62
+ function showLoading(resultDiv) {
63
+ resultDiv.innerHTML = `
64
+ <div class="loading-container">
65
+ <div class="loading-spinner"></div>
66
+ <p>Traitement en cours...</p>
67
+ </div>
68
+ `;
69
+ }
70
+
71
+ // Gestion des erreurs
72
+ function showError(resultDiv, message) {
73
+ resultDiv.innerHTML = `
74
+ <div class="error-message">
75
+ <span class="error-icon">⚠️</span>
76
+ <p>${message}</p>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ // Initialisation du document
82
+ document.addEventListener('DOMContentLoaded', () => {
83
+ try {
84
+ // Activer le premier onglet par défaut
85
+ openTab('summarize');
86
+
87
+ // Configurer les uploads de fichiers
88
+ setupFileUpload('summarize-file');
89
+ setupFileUpload('answer-question-file');
90
+ setupFileUpload('interpret-image-file');
91
+ setupFileUpload('visualization-file');
92
+ setupFileUpload('translate-file');
93
+
94
+ // Configurer les onglets
95
+ document.querySelectorAll('.tab-btn').forEach(btn => {
96
+ btn.addEventListener('click', () => {
97
+ const tabName = btn.getAttribute('data-tab');
98
+ if (tabName) openTab(tabName);
99
+ });
100
+ });
101
+
102
+ // Configurer le formulaire de résumé
103
+ const summarizeForm = document.getElementById('summarize-form');
104
+ if (summarizeForm) {
105
+ summarizeForm.addEventListener('submit', async (e) => {
106
+ e.preventDefault();
107
+ handleSummarizeSubmit();
108
+ });
109
+ }
110
+
111
+ // Configurer le formulaire de question-réponse
112
+ const qaForm = document.getElementById('answer-question-form');
113
+ if (qaForm) {
114
+ qaForm.addEventListener('submit', async (e) => {
115
+ e.preventDefault();
116
+ handleQASubmit();
117
+ });
118
+ }
119
+
120
+ // Désactiver temporairement les onglets non implémentés
121
+ document.querySelectorAll('.tab-btn').forEach(btn => {
122
+ const tabName = btn.getAttribute('data-tab');
123
+ if (tabName !== 'summarize' && tabName !== 'answer-question') {
124
+ btn.addEventListener('click', (e) => {
125
+ e.preventDefault();
126
+ alert("Cette fonctionnalité n'est pas encore implémentée.");
127
+ // Ne pas changer d'onglet
128
+ return false;
129
+ });
130
+ }
131
+ });
132
+
133
+ } catch (error) {
134
+ console.error('Erreur d\'initialisation:', error);
135
+ }
136
+ });
137
+
138
+ // Gestion du formulaire de résumé
139
+ async function handleSummarizeSubmit() {
140
+ const fileInput = document.getElementById('summarize-file');
141
+ const resultDiv = document.getElementById('summarize-result');
142
+
143
+ if (!fileInput || !resultDiv) return;
144
+
145
+ if (!fileInput.files || fileInput.files.length === 0) {
146
+ showError(resultDiv, "Veuillez sélectionner un fichier à résumer.");
147
+ return;
148
+ }
149
+
150
+ showLoading(resultDiv);
151
+
152
+ try {
153
+ const formData = new FormData();
154
+ formData.append('file', fileInput.files[0]);
155
+
156
+ // Utiliser fetch avec timeout et gestion d'erreur améliorée
157
+ const controller = new AbortController();
158
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
159
+
160
+ const response = await fetch(`${BASE_URL}/summarize`, {
161
+ method: 'POST',
162
+ body: formData,
163
+ signal: controller.signal
164
+ }).finally(() => clearTimeout(timeoutId));
165
+
166
+ if (!response.ok) {
167
+ const errorText = await response.text();
168
+ throw new Error(`Erreur: ${response.status} ${response.statusText}\n${errorText}`);
169
+ }
170
+
171
+ const data = await response.json();
172
+
173
+ if (data.warning) {
174
+ // Afficher l'avertissement
175
+ resultDiv.innerHTML = `
176
+ <div class="warning-message">
177
+ <h3>Problème détecté</h3>
178
+ <p>${data.summary}</p>
179
+ </div>
180
+ `;
181
+ } else {
182
+ // Afficher le résumé
183
+ resultDiv.innerHTML = `
184
+ <div class="result-content">
185
+ <h3>Résumé de: ${data.filename}</h3>
186
+ <div class="summary-text">${data.summary}</div>
187
+ <p class="text-meta">Longueur du texte original: ${data.text_length} caractères</p>
188
+ </div>
189
+ `;
190
+ }
191
+ } catch (error) {
192
+ console.error('Erreur lors du résumé:', error);
193
+
194
+ // Message d'erreur plus descriptif
195
+ let errorMessage = error.message;
196
+ if (error.name === 'AbortError') {
197
+ errorMessage = "La requête a pris trop de temps. Veuillez réessayer avec un fichier plus petit.";
198
+ } else if (error.message.includes("Failed to fetch")) {
199
+ errorMessage = "Impossible de contacter le serveur. Veuillez vérifier votre connexion ou contactez l'administrateur.";
200
+ }
201
+
202
+ showError(resultDiv, `Erreur: ${errorMessage}`);
203
+ }
204
+ }
205
+
206
+ // Gestion du formulaire de question-réponse
207
+ async function handleQASubmit() {
208
+ const questionInput = document.getElementById('question-input');
209
+ const fileInput = document.getElementById('answer-question-file');
210
+ const resultDiv = document.getElementById('answer-question-result');
211
+
212
+ if (!questionInput || !resultDiv) return;
213
+
214
+ if (!questionInput.value.trim()) {
215
+ showError(resultDiv, "Veuillez entrer une question.");
216
+ return;
217
+ }
218
+
219
+ showLoading(resultDiv);
220
+
221
+ try {
222
+ const formData = new FormData();
223
+ formData.append('question', questionInput.value);
224
+
225
+ if (fileInput && fileInput.files && fileInput.files.length > 0) {
226
+ formData.append('file', fileInput.files[0]);
227
+ }
228
+
229
+ // Utiliser fetch avec timeout et gestion d'erreur améliorée
230
+ const controller = new AbortController();
231
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
232
+
233
+ const response = await fetch(`${BASE_URL}/answer-question`, {
234
+ method: 'POST',
235
+ body: formData,
236
+ signal: controller.signal
237
+ }).finally(() => clearTimeout(timeoutId));
238
+
239
+ if (!response.ok) {
240
+ const errorText = await response.text();
241
+ throw new Error(`Erreur: ${response.status} ${response.statusText}\n${errorText}`);
242
+ }
243
+
244
+ const data = await response.json();
245
+
246
+ if (data.warning) {
247
+ // Afficher l'avertissement
248
+ resultDiv.innerHTML = `
249
+ <div class="warning-message">
250
+ <h3>Problème détecté</h3>
251
+ <p>${data.answer}</p>
252
+ </div>
253
+ `;
254
+ } else {
255
+ // Afficher la réponse
256
+ resultDiv.innerHTML = `
257
+ <div class="result-content">
258
+ <h3>Réponse</h3>
259
+ <div class="question-text"><strong>Question:</strong> ${data.question}</div>
260
+ <div class="answer-text"><strong>Réponse:</strong> ${data.answer}</div>
261
+ </div>
262
+ `;
263
+ }
264
+ } catch (error) {
265
+ console.error('Erreur de question-réponse:', error);
266
+
267
+ // Message d'erreur plus descriptif
268
+ let errorMessage = error.message;
269
+ if (error.name === 'AbortError') {
270
+ errorMessage = "La requête a pris trop de temps. Veuillez réessayer avec une question plus courte ou un fichier plus petit.";
271
+ } else if (error.message.includes("Failed to fetch")) {
272
+ errorMessage = "Impossible de contacter le serveur. Veuillez vérifier votre connexion ou contactez l'administrateur.";
273
+ }
274
+
275
+ showError(resultDiv, `Erreur: ${errorMessage}`);
276
+ }
277
+ }
frontend/styles.css ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Réinitialisation de base */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ /* Corps de la page */
9
+ body {
10
+ background-color: #f7fafc;
11
+ font-family: Arial, sans-serif;
12
+ min-height: 100vh;
13
+ }
14
+
15
+ /* Conteneur principal */
16
+ .container {
17
+ max-width: 1024px;
18
+ margin: 0 auto;
19
+ padding: 2rem 1rem;
20
+ }
21
+
22
+ /* Titre principal */
23
+ h1 {
24
+ font-size: 2rem;
25
+ font-weight: bold;
26
+ text-align: center;
27
+ color: #2d3748;
28
+ margin-bottom: 1rem;
29
+ }
30
+
31
+ /* Sous-titre */
32
+ .subtitle {
33
+ text-align: center;
34
+ color: #718096;
35
+ margin-bottom: 2rem;
36
+ }
37
+
38
+ /* Conteneur des onglets */
39
+ .tab-container {
40
+ background-color: #fff;
41
+ border-radius: 0.5rem;
42
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
43
+ overflow: hidden;
44
+ }
45
+
46
+ /* Navigation des onglets */
47
+ .tab-nav {
48
+ display: flex;
49
+ border-bottom: 1px solid #e2e8f0;
50
+ position: relative;
51
+ }
52
+
53
+ .tab-nav .tab-btn {
54
+ flex: 1;
55
+ padding: 1rem;
56
+ background-color: #f7fafc;
57
+ border: none;
58
+ cursor: pointer;
59
+ transition: background-color 0.3s ease, transform 0.2s ease;
60
+ text-align: center;
61
+ font-size: 1rem;
62
+ }
63
+
64
+ .tab-nav .tab-btn:hover {
65
+ background-color: #edf2f7;
66
+ transform: scale(1.05);
67
+ }
68
+
69
+ .tab-nav .tab-btn.active {
70
+ background-color: #edf2f7;
71
+ font-weight: bold;
72
+ }
73
+
74
+ /* Effet de soulignement dynamique sur les onglets */
75
+ .tab-nav .tab-btn {
76
+ position: relative;
77
+ }
78
+
79
+ .tab-nav .tab-btn::after {
80
+ content: "";
81
+ position: absolute;
82
+ left: 50%;
83
+ bottom: 0;
84
+ width: 0;
85
+ height: 3px;
86
+ background-color: #4299e1;
87
+ transition: width 0.3s ease, left 0.3s ease;
88
+ }
89
+
90
+ .tab-nav .tab-btn:hover::after,
91
+ .tab-nav .tab-btn.active::after {
92
+ width: 100%;
93
+ left: 0;
94
+ }
95
+
96
+ /* Contenu des onglets */
97
+ .tab-content-wrapper {
98
+ padding: 1.5rem;
99
+ }
100
+
101
+ .tab-content {
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ /* Masquer les éléments */
106
+ .hidden {
107
+ display: none;
108
+ }
109
+
110
+ /* Styles des formulaires */
111
+ .form {
112
+ margin-bottom: 1rem;
113
+ }
114
+
115
+ .form input[type="text"],
116
+ .form textarea,
117
+ .form select {
118
+ width: 100%;
119
+ padding: 0.5rem;
120
+ border: 1px solid #cbd5e0;
121
+ border-radius: 0.25rem;
122
+ font-size: 1rem;
123
+ }
124
+
125
+ .textarea {
126
+ resize: vertical;
127
+ min-height: 80px;
128
+ }
129
+
130
+ /* Input type file caché */
131
+ .hidden-file {
132
+ display: none;
133
+ }
134
+
135
+ /* Label pour le chargement de fichier */
136
+ .file-upload {
137
+ display: block;
138
+ width: 100%;
139
+ padding: 1rem;
140
+ border: 2px dashed #cbd5e0;
141
+ border-radius: 0.5rem;
142
+ text-align: center;
143
+ cursor: pointer;
144
+ transition: background-color 0.3s ease;
145
+ margin-bottom: 1rem;
146
+ }
147
+
148
+ .file-upload:hover {
149
+ background-color: #f7fafc;
150
+ }
151
+
152
+ /* Affichage du nom du fichier */
153
+ .filename-display {
154
+ text-align: center;
155
+ color: #718096;
156
+ font-size: 0.875rem;
157
+ margin-bottom: 1rem;
158
+ }
159
+
160
+ /* Boutons */
161
+ .btn {
162
+ width: 100%;
163
+ padding: 0.75rem;
164
+ border: none;
165
+ border-radius: 0.25rem;
166
+ color: #fff;
167
+ cursor: pointer;
168
+ transition: background-color 0.3s ease;
169
+ font-size: 1rem;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ gap: 0.5rem;
174
+ }
175
+
176
+ /* Boutons de couleur */
177
+ .btn-blue {
178
+ background-color: #4299e1;
179
+ }
180
+
181
+ .btn-blue:hover {
182
+ background-color: #3182ce;
183
+ }
184
+
185
+ .btn-green {
186
+ background-color: #48bb78;
187
+ }
188
+
189
+ .btn-green:hover {
190
+ background-color: #38a169;
191
+ }
192
+
193
+ .btn-purple {
194
+ background-color: #9f7aea;
195
+ }
196
+
197
+ .btn-purple:hover {
198
+ background-color: #805ad5;
199
+ }
200
+
201
+ .btn-indigo {
202
+ background-color: #667eea;
203
+ }
204
+
205
+ .btn-indigo:hover {
206
+ background-color: #5a67d8;
207
+ }
208
+
209
+ .btn-pink {
210
+ background-color: #ed64a6;
211
+ }
212
+
213
+ .btn-pink:hover {
214
+ background-color: #d53f8c;
215
+ }
216
+
217
+ /* Boîte de résultat */
218
+ .result-box {
219
+ margin-top: 1rem;
220
+ padding: 1rem;
221
+ background-color: #f7fafc;
222
+ border-radius: 0.25rem;
223
+ min-height: 100px;
224
+ }
225
+
226
+ /* Placeholder dans la boîte de résultat */
227
+ .result-box .placeholder {
228
+ color: #a0aec0;
229
+ font-style: italic;
230
+ }
231
+
232
+ /* Sélecteur de formulaire */
233
+ .select {
234
+ width: 100%;
235
+ padding: 0.5rem;
236
+ border: 1px solid #cbd5e0;
237
+ border-radius: 0.25rem;
238
+ }
239
+
240
+ /* Styles pour les messages d'erreur et d'information */
241
+ .error {
242
+ color: #e53e3e;
243
+ font-weight: bold;
244
+ margin: 0;
245
+ }
246
+
247
+ .info {
248
+ color: #3182ce;
249
+ font-style: italic;
250
+ margin: 0;
251
+ }
252
+
253
+ .small {
254
+ font-size: 0.875rem;
255
+ color: #718096;
256
+ margin-top: 0.5rem;
257
+ }
258
+
259
+ /* Style pour les images dans les visualisations */
260
+ .result-box img {
261
+ max-width: 100%;
262
+ height: auto;
263
+ margin-top: 1rem;
264
+ border-radius: 0.25rem;
265
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
266
+ }
267
+
268
+ /* Navbar avec animation d'apparition */
269
+ nav {
270
+ filter: drop-shadow(0.25rem 0.25rem 0.25rem rgba(0, 0, 0, 0.3));
271
+ width: 100%;
272
+ max-width: 14rem;
273
+ opacity: 0;
274
+ transform: translateY(-20px);
275
+ animation: fadeInDown 0.5s ease-out forwards;
276
+ }
277
+
278
+ /* Animation keyframes pour la navbar */
279
+ @keyframes fadeInDown {
280
+ from {
281
+ opacity: 0;
282
+ transform: translateY(-20px);
283
+ }
284
+ to {
285
+ opacity: 1;
286
+ transform: translateY(0);
287
+ }
288
+ }
289
+
290
+ /* Préférence de réduction des animations */
291
+ @media (prefers-reduced-motion: reduce) {
292
+ nav * {
293
+ transition: 5s !important;
294
+ animation: 10ms !important;
295
+ }
296
+ }
297
+
298
+ /* Responsivité pour petits écrans */
299
+ @media (max-width: 768px) {
300
+ .tab-nav {
301
+ flex-direction: column;
302
+ }
303
+
304
+ .tab-nav .tab-btn {
305
+ padding: 0.75rem;
306
+ font-size: 0.875rem;
307
+ }
308
+
309
+ h1 {
310
+ font-size: 1.5rem;
311
+ }
312
+
313
+ .container {
314
+ padding: 1rem;
315
+ }
316
+ }