hey
Browse files- Dockerfile +18 -0
- app.py +417 -0
- requirements.txt +5 -0
- static/pdfs/j.txt +0 -0
- templates/index.html +797 -0
Dockerfile
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11
|
2 |
+
|
3 |
+
# Installer poppler-utils, texlive-latex-extra et texlive-full
|
4 |
+
USER root
|
5 |
+
RUN apt-get update && apt-get install -y poppler-utils texlive-latex-extra texlive-full && rm -rf /var/lib/apt/lists/*
|
6 |
+
|
7 |
+
# Créer un utilisateur non-root
|
8 |
+
RUN useradd -m -u 1000 user
|
9 |
+
USER user
|
10 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
11 |
+
|
12 |
+
WORKDIR /app
|
13 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
14 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
15 |
+
COPY --chown=user . /app
|
16 |
+
|
17 |
+
# Commande de démarrage Flask
|
18 |
+
CMD ["flask", "run", "--host=0.0.0.0", "--port=7860"]
|
app.py
ADDED
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, send_file, jsonify
|
2 |
+
from google import genai
|
3 |
+
from google.genai import types
|
4 |
+
from PIL import Image
|
5 |
+
import subprocess
|
6 |
+
import tempfile
|
7 |
+
import os
|
8 |
+
import io
|
9 |
+
import shutil
|
10 |
+
import base64
|
11 |
+
|
12 |
+
app = Flask(__name__)
|
13 |
+
|
14 |
+
# --- Cognvstantes ---
|
15 |
+
MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" # Modèle avec "thinking" pour la génération complète
|
16 |
+
LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
|
17 |
+
|
18 |
+
# --- Configuration des secrets ---
|
19 |
+
# Dans un environnement de production, utilisez les variables d'environnement ou un fichier .env
|
20 |
+
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY', '')
|
21 |
+
|
22 |
+
# --- Fonctions Utilitaires ---
|
23 |
+
|
24 |
+
def check_latex_installation():
|
25 |
+
"""Vérifie si pdflatex est accessible dans le PATH système."""
|
26 |
+
try:
|
27 |
+
subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
|
28 |
+
return True, "✅ Installation LaTeX (pdflatex) trouvée."
|
29 |
+
except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
|
30 |
+
return False, f"❌ pdflatex introuvable ou ne répond pas. Veuillez installer une distribution LaTeX (comme TeX Live ou MiKTeX) et vous assurer qu'elle est dans le PATH système. Erreur: {e}"
|
31 |
+
|
32 |
+
def initialize_genai_client():
|
33 |
+
"""Initialise et retourne le client Google GenAI."""
|
34 |
+
try:
|
35 |
+
if not GOOGLE_API_KEY:
|
36 |
+
return None, "Clé API Google non trouvée dans les variables d'environnement."
|
37 |
+
|
38 |
+
client = genai.Client(api_key=GOOGLE_API_KEY)
|
39 |
+
return client, "✅ Client Mariam AI initialisé."
|
40 |
+
except Exception as e:
|
41 |
+
return None, f"❌ Erreur lors de l'initialisation du client: {str(e)}"
|
42 |
+
|
43 |
+
def clean_latex(raw_latex_text):
|
44 |
+
"""Nettoie le code LaTeX brut potentiellement fourni par Gemini."""
|
45 |
+
if not isinstance(raw_latex_text, str):
|
46 |
+
return "" # Retourne une chaîne vide si l'entrée n'est pas une chaîne
|
47 |
+
|
48 |
+
cleaned = raw_latex_text.strip()
|
49 |
+
|
50 |
+
# Supprime les marqueurs de bloc de code (```latex ... ``` ou ``` ... ```)
|
51 |
+
if cleaned.startswith("```latex"):
|
52 |
+
cleaned = cleaned[8:]
|
53 |
+
elif cleaned.startswith("```"):
|
54 |
+
# Gère le cas où il y a un saut de ligne après ```
|
55 |
+
lines = cleaned.split('\n', 1)
|
56 |
+
if len(lines) > 1:
|
57 |
+
cleaned = lines[1]
|
58 |
+
else:
|
59 |
+
cleaned = cleaned[3:] # Si pas de saut de ligne, juste enlever ```
|
60 |
+
|
61 |
+
# Supprime les marqueurs de fin de bloc de code
|
62 |
+
if cleaned.endswith("```"):
|
63 |
+
cleaned = cleaned[:-3].strip() # Enlève ``` et les espaces potentiels avant
|
64 |
+
|
65 |
+
# Assure que le document commence correctement
|
66 |
+
if not cleaned.startswith("\\documentclass"):
|
67 |
+
# On pourrait choisir de lever une erreur ou de tenter une correction
|
68 |
+
pass
|
69 |
+
|
70 |
+
# Assure que le document se termine exactement par \end{document}
|
71 |
+
end_doc_tag = "\\end{document}"
|
72 |
+
if end_doc_tag in cleaned:
|
73 |
+
end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
|
74 |
+
cleaned = cleaned[:end_idx]
|
75 |
+
else:
|
76 |
+
# Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
|
77 |
+
if cleaned.strip().startswith("\\documentclass"):
|
78 |
+
cleaned += f"\n{end_doc_tag}"
|
79 |
+
|
80 |
+
return cleaned.strip() # Retourne le résultat nettoyé
|
81 |
+
|
82 |
+
# --- Fonction Principale (API GenAI) ---
|
83 |
+
|
84 |
+
def generate_complete_latex(client, image_bytes):
|
85 |
+
"""
|
86 |
+
Génère une solution complète en LaTeX à partir de l'image en une seule étape.
|
87 |
+
"""
|
88 |
+
try:
|
89 |
+
# Convertir les bytes en image PIL
|
90 |
+
image = Image.open(io.BytesIO(image_bytes))
|
91 |
+
|
92 |
+
prompt =r"""
|
93 |
+
**PROMPT AMÉLIORÉ**
|
94 |
+
|
95 |
+
# RÔLE & OBJECTIF
|
96 |
+
Tu es un expert en mathématiques du supérieur, un tuteur pédagogue exceptionnel, et un maître utilisateur de LaTeX. Ton objectif est de générer une correction LaTeX complète, rigoureuse, pédagogique et visuellement impeccable pour l'exercice de mathématiques fourni. Le résultat doit être un fichier source `.tex` autonome, directement compilable via `pdflatex`, et d'une qualité de présentation professionnelle, similaire à l'exemple fourni précédemment.
|
97 |
+
|
98 |
+
# CONTEXTE
|
99 |
+
* **Input:** L'énoncé de l'exercice de mathématiques sera fourni sous forme de texte (copié/collé par l'utilisateur) immédiatement après ce prompt.
|
100 |
+
* **Niveau Cible:** Élève de Terminale (système français, filière scientifique ou équivalent).
|
101 |
+
* **Output Attendu:** **Uniquement** le code source LaTeX (`.tex`) brut et complet. Aucune explication ou texte d'accompagnement en dehors du code lui-même.
|
102 |
+
# TÂCHE PRINCIPALE
|
103 |
+
1. **Analyse:** Lis et comprends parfaitement l'énoncé de l'exercice fourni.
|
104 |
+
2. **Résolution:** Résous l'exercice de manière exhaustive, en détaillant chaque étape de raisonnement et de calcul.
|
105 |
+
3. **Rédaction LaTeX:** Rédige la solution complète *directement* en code LaTeX, en respectant **scrupuleusement** toutes les spécifications techniques, de style et de contenu ci-dessous.
|
106 |
+
|
107 |
+
# SPÉCIFICATIONS TECHNIQUES DU CODE LATEX
|
108 |
+
|
109 |
+
1. **Document de Base:**
|
110 |
+
* Classe: `\documentclass[12pt,a4paper]{article}`
|
111 |
+
* Encodage: `\usepackage[utf8]{inputenc}`, `\usepackage[T1]{fontenc}`
|
112 |
+
* Langue: `\usepackage[french]{babel}`
|
113 |
+
* Police: `\usepackage{lmodern}` (Latin Modern)
|
114 |
+
* Typographie Fine: `\usepackage{microtype}`
|
115 |
+
|
116 |
+
2. **Packages Indispensables (Configurer si nécessaire):**
|
117 |
+
* Maths: `amsmath`, `amssymb`, `amsfonts`, `mathtools`, `bm` (pour gras maths), `siunitx` (unités SI).
|
118 |
+
* Mise en Page: `geometry` (marges `a4paper, margin=2.5cm`), `setspace` (utiliser `\onehalfspacing`), `fancyhdr` (pour en-têtes/pieds), `titlesec` & `titletoc` (personnalisation titres/TDM), `multicol` (si besoin).
|
119 |
+
* Visuel & Couleurs: `xcolor` (définir les couleurs ci-dessous), `tcolorbox` (avec `theorems`, `skins`, `breakable`, `hooks`), `fontawesome5` (pour icônes).
|
120 |
+
* Liens & PDF: `hyperref` (configuré pour liens colorés, métadonnées PDF), `bookmark`.
|
121 |
+
* Graphiques: `tikz` (avec `calc`, `shapes`, `arrows.meta`, `positioning`), `pgfplots` (avec `compat=1.18`, `fillbetween`).
|
122 |
+
|
123 |
+
3. **Configuration Générale:**
|
124 |
+
* Paragraphes: Pas d'indentation (`\setlength{\parindent}{0pt}`), espacement entre paragraphes (`\setlength{\parskip}{1.2ex plus 0.5ex minus 0.2ex}`).
|
125 |
+
* Compilabilité: Code valide pour `pdflatex`.
|
126 |
+
|
127 |
+
# STYLE & PRÉSENTATION PROFESSIONNELLE (Inspiré de l'exemple)
|
128 |
+
|
129 |
+
1. **Couleurs à Définir (`\definecolor`):**
|
130 |
+
* `maincolor`: Bleu (e.g., `{RGB}{0, 90, 160}`)
|
131 |
+
* `secondcolor`: Vert-bleu (e.g., `{RGB}{0, 150, 136}`)
|
132 |
+
* `thirdcolor`: Violet (e.g., `{RGB}{140, 0, 140}`)
|
133 |
+
* `accentcolor`: Orange (e.g., `{RGB}{255, 140, 0}`)
|
134 |
+
* `ubgcolor`: Fond clair (e.g., `{RGB}{245, 247, 250}`)
|
135 |
+
* `codebackground`: Fond code (e.g., `{RGB}{245, 245, 245}`)
|
136 |
+
* `gridcolor`: Grille PGFPlots (e.g., `{RGB}{220, 220, 220}`)
|
137 |
+
* `asymptotecolor`: Asymptotes (e.g., `{RGB}{200, 0, 0}`)
|
138 |
+
|
139 |
+
2. **Page de Titre / Présentation Initiale:**
|
140 |
+
* Créer une page de titre distincte (`titlepage`) ou un bloc titre élégant après `\begin{document}` utilisant `\maketitle` (redéfini si nécessaire).
|
141 |
+
* Inclure titre clair, sous-titre (e.g., "Exercice X: Solution Détaillée"), auteur générique (e.g., "Solution Proposée"), date (`\today`).
|
142 |
+
* Optionnel : Page séparée pour l'énoncé encadré et un plan de résolution avant la table des matières.
|
143 |
+
|
144 |
+
3. **Structure & Navigation:**
|
145 |
+
* Table des matières (`\tableofcontents`) après la présentation initiale.
|
146 |
+
* En-têtes/Pieds de page (`fancyhdr`): Infos discrètes (titre exo, page), règles fines colorées.
|
147 |
+
* Titres (`titlesec`): Sections/sous-sections numérotées avec style distinctif (e.g., numéro dans cercle/rectangle coloré, ligne de séparation).
|
148 |
+
|
149 |
+
4. **Boîtes `tcolorbox` Personnalisées (avec icônes `fontawesome5`):**
|
150 |
+
* `enoncebox` (Grisâtre, `\faBook`): Pour l'énoncé principal.
|
151 |
+
* `definitionbox` (Couleur secondaire, `\faLightbulb`): Pour définitions, théorèmes clés.
|
152 |
+
* `resultbox` (Couleur accent, `\faCheckCircle`): Pour les résultats finaux importants.
|
153 |
+
* `notebox` (Couleur tertiaire, `\faInfoCircle`): Pour remarques, astuces, points de méthode.
|
154 |
+
* `examplebox` (Vert, `\faClipboard`): Pour exemples illustratifs ou rappels de méthode.
|
155 |
+
* Configurer ces boîtes pour être `breakable`, avec titre stylisé.
|
156 |
+
|
157 |
+
5. **Commandes Mathématiques Personnalisées (`\newcommand`):**
|
158 |
+
* Ensembles: `\R`, `\C`, `\N`, `\Z`, `\Q`.
|
159 |
+
* Limites: `\limx{}`, `\limxp{}`, `\limxm{}`, `\limsinf`, `\liminf`.
|
160 |
+
* Dérivées: `\derivee{}{} `, `\ddx{}`, `\dfdx{}`.
|
161 |
+
* Divers: `\abs{}`, `\norm{}`, `\vect{}`, `\ds` (`\displaystyle`).
|
162 |
+
* Mise en valeur: `\highlight{}` (fond jaune?), `\finalresult{}` (fond couleur accent?).
|
163 |
+
* Environnements: `importanteq` (pour équations clés encadrées, via `empheq`).
|
164 |
+
|
165 |
+
6. **Graphiques (`pgfplots`):**
|
166 |
+
* Si pertinent (étude de fonction, géométrie): Inclure un graphique propre.
|
167 |
+
* Configurer `pgfplotsset`: Axes centrés (`axis lines=middle`), flèches (`-Latex`), grille (`grid=both`), labels (`xlabel`, `ylabel`), légende (`legend pos=...`), couleurs définies plus haut.
|
168 |
+
* Tracer la courbe principale (trait plein, couleur principale), asymptotes (pointillés, couleurs dédiées), points remarquables.
|
169 |
+
|
170 |
+
# CONTENU PÉDAGOGIQUE DE LA SOLUTION
|
171 |
+
|
172 |
+
1. **Clarté et Rigueur:**
|
173 |
+
* Rappeler l'énoncé initialement dans `enoncebox`.
|
174 |
+
* Structurer la solution logiquement (par question, par étape).
|
175 |
+
* Justifier *chaque* étape de calcul ou de raisonnement.
|
176 |
+
* Utiliser un langage mathématique précis et une rédaction claire en français.
|
177 |
+
* Aligner soigneusement les équations (`align*`, `cases`).
|
178 |
+
|
179 |
+
2. **Pédagogie:**
|
180 |
+
* Expliquer les "pourquoi" derrière les méthodes utilisées.
|
181 |
+
* Utiliser les `notebox` pour des conseils, pièges à éviter, ou rappels de cours.
|
182 |
+
* Mettre en évidence les définitions/théorèmes clés dans `definitionbox`.
|
183 |
+
* Encapsuler les résultats finaux dans `resultbox` ou avec `\finalresult`.
|
184 |
+
|
185 |
+
3. **Complétude:**
|
186 |
+
* Traiter toutes les questions de l'énoncé.
|
187 |
+
* Inclure les interprétations graphiques demandées ou pertinentes.
|
188 |
+
* Conclure si nécessaire en résumant les points essentiels.
|
189 |
+
|
190 |
+
# CONTRAINTES STRICTES
|
191 |
+
* Le seul output doit être le code LaTeX brut.
|
192 |
+
* Aucun texte avant `\documentclass` ou après `\end{document}` (sauf la mention spéciale).
|
193 |
+
* Inclure la ligne `{LATEX_MENTION}` juste avant `\end{document}`.
|
194 |
+
|
195 |
+
|
196 |
+
"""
|
197 |
+
|
198 |
+
response = client.models.generate_content(
|
199 |
+
model=MODEL_SINGLE_GENERATION,
|
200 |
+
contents=[image, prompt],
|
201 |
+
config=genai.types.GenerateContentConfig(
|
202 |
+
temperature=1,
|
203 |
+
),
|
204 |
+
|
205 |
+
)
|
206 |
+
|
207 |
+
latex_content_raw = None
|
208 |
+
thinking_content = None
|
209 |
+
|
210 |
+
# Extrait le contenu LaTeX et le raisonnement (thought)
|
211 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
|
212 |
+
for part in response.candidates[0].content.parts:
|
213 |
+
if getattr(part, 'thought', None):
|
214 |
+
thinking_content = part.text
|
215 |
+
else:
|
216 |
+
latex_content_raw = part.text
|
217 |
+
|
218 |
+
if latex_content_raw:
|
219 |
+
latex_content_cleaned = clean_latex(latex_content_raw)
|
220 |
+
|
221 |
+
# Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
|
222 |
+
if LATEX_MENTION not in latex_content_cleaned:
|
223 |
+
# Tente de l'insérer avant \end{document}
|
224 |
+
end_doc_tag = "\\end{document}"
|
225 |
+
if end_doc_tag in latex_content_cleaned:
|
226 |
+
latex_content_cleaned = latex_content_cleaned.replace(end_doc_tag, f"{LATEX_MENTION}\n{end_doc_tag}")
|
227 |
+
else:
|
228 |
+
# Si \end{document} n'est pas là, ajoute les deux à la fin
|
229 |
+
latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
|
230 |
+
|
231 |
+
return latex_content_cleaned, thinking_content, None
|
232 |
+
else:
|
233 |
+
return None, thinking_content, "Aucun contenu LaTeX n'a été généré par le modèle."
|
234 |
+
|
235 |
+
except genai.types.StopCandidateException as e:
|
236 |
+
return None, None, "❌ Génération stoppée (possible contenu inapproprié)"
|
237 |
+
except Exception as e:
|
238 |
+
return None, None, f"❌ Erreur lors de la génération du LaTeX: {str(e)}"
|
239 |
+
|
240 |
+
# --- Fonction de Compilation LaTeX ---
|
241 |
+
|
242 |
+
def compile_latex_to_pdf(latex_content):
|
243 |
+
"""
|
244 |
+
Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
|
245 |
+
Utilise un répertoire temporaire pour les fichiers intermédiaires.
|
246 |
+
"""
|
247 |
+
if not latex_content:
|
248 |
+
return None, "Impossible de compiler : contenu LaTeX vide."
|
249 |
+
|
250 |
+
# Utilise un répertoire temporaire sécurisé qui sera nettoyé automatiquement
|
251 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
252 |
+
tex_filename = "solution.tex"
|
253 |
+
pdf_filename = "solution.pdf"
|
254 |
+
tex_filepath = os.path.join(temp_dir, tex_filename)
|
255 |
+
pdf_filepath = os.path.join(temp_dir, pdf_filename)
|
256 |
+
log_filepath = os.path.join(temp_dir, "solution.log") # Pour le débogage
|
257 |
+
|
258 |
+
# Écrit le contenu LaTeX dans le fichier .tex avec encodage UTF-8
|
259 |
+
try:
|
260 |
+
with open(tex_filepath, "w", encoding="utf-8") as f:
|
261 |
+
f.write(latex_content)
|
262 |
+
except IOError as e:
|
263 |
+
return None, f"❌ Erreur lors de l'écriture du fichier .tex temporaire: {str(e)}"
|
264 |
+
|
265 |
+
# Exécute pdflatex
|
266 |
+
command = [
|
267 |
+
"pdflatex",
|
268 |
+
"-interaction=nonstopmode",
|
269 |
+
f"-output-directory={temp_dir}",
|
270 |
+
f"-jobname={os.path.splitext(pdf_filename)[0]}", # Nom sans extension
|
271 |
+
tex_filepath
|
272 |
+
]
|
273 |
+
|
274 |
+
pdf_generated = False
|
275 |
+
compilation_output = ""
|
276 |
+
|
277 |
+
for pass_num in range(1, 3): # Lance jusqu'à 2 passes
|
278 |
+
try:
|
279 |
+
# Augmentation du timeout pour les compilations potentiellement longues
|
280 |
+
result = subprocess.run(command, capture_output=True, check=True, text=True, encoding='utf-8', errors='replace', timeout=60)
|
281 |
+
compilation_output += f"Passe {pass_num}: Succès\n"
|
282 |
+
|
283 |
+
# Vérifie si le PDF a été créé après la première passe (au moins)
|
284 |
+
if os.path.exists(pdf_filepath):
|
285 |
+
pdf_generated = True
|
286 |
+
|
287 |
+
except subprocess.CalledProcessError as e:
|
288 |
+
error_message = f"❌ Erreur lors de la compilation LaTeX (Passe {pass_num}).\n"
|
289 |
+
error_message += f"Code de retour: {e.returncode}\n"
|
290 |
+
error_message += f"Sortie: {e.stdout}\n"
|
291 |
+
error_message += f"Erreurs: {e.stderr}\n"
|
292 |
+
|
293 |
+
# Essayer de lire le fichier .log pour plus d'infos
|
294 |
+
try:
|
295 |
+
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
296 |
+
error_message += f"Extrait du fichier log (solution.log):\n{log_file.read()[-2000:]}"
|
297 |
+
except IOError:
|
298 |
+
error_message += "Impossible de lire le fichier log de LaTeX."
|
299 |
+
|
300 |
+
return None, error_message
|
301 |
+
|
302 |
+
except subprocess.TimeoutExpired:
|
303 |
+
return None, "❌ La compilation LaTeX a dépassé le délai imparti. Le document est peut-être trop complexe ou contient une boucle infinie."
|
304 |
+
|
305 |
+
except Exception as e:
|
306 |
+
return None, f"❌ Une erreur inattendue est survenue lors de l'exécution de pdflatex: {str(e)}"
|
307 |
+
|
308 |
+
# Vérifie si le fichier PDF existe après les passes
|
309 |
+
if pdf_generated and os.path.exists(pdf_filepath):
|
310 |
+
try:
|
311 |
+
# Lit le contenu binaire du PDF généré
|
312 |
+
with open(pdf_filepath, "rb") as f:
|
313 |
+
pdf_data = f.read()
|
314 |
+
return pdf_data, "✅ PDF généré avec succès !"
|
315 |
+
except IOError as e:
|
316 |
+
return None, f"❌ Erreur lors de la lecture du fichier PDF généré: {str(e)}"
|
317 |
+
else:
|
318 |
+
error_message = "❌ Le fichier PDF n'a pas été généré après la compilation LaTeX.\n"
|
319 |
+
# Tenter de lire le log
|
320 |
+
try:
|
321 |
+
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
322 |
+
error_message += f"Extrait du fichier log (solution.log) après échec de génération PDF:\n{log_file.read()[-2000:]}"
|
323 |
+
except IOError:
|
324 |
+
error_message += "Impossible de lire le fichier log de LaTeX."
|
325 |
+
return None, error_message
|
326 |
+
|
327 |
+
# --- Routes Flask ---
|
328 |
+
|
329 |
+
@app.route('/')
|
330 |
+
def index():
|
331 |
+
return render_template('index.html')
|
332 |
+
|
333 |
+
@app.route('/check-latex', methods=['GET'])
|
334 |
+
def check_latex():
|
335 |
+
latex_installed, message = check_latex_installation()
|
336 |
+
return jsonify({
|
337 |
+
'success': latex_installed,
|
338 |
+
'message': message
|
339 |
+
})
|
340 |
+
|
341 |
+
@app.route('/check-api', methods=['GET'])
|
342 |
+
def check_api():
|
343 |
+
client, message = initialize_genai_client()
|
344 |
+
return jsonify({
|
345 |
+
'success': client is not None,
|
346 |
+
'message': message
|
347 |
+
})
|
348 |
+
|
349 |
+
@app.route('/process', methods=['POST'])
|
350 |
+
def process():
|
351 |
+
if 'image' not in request.files:
|
352 |
+
return jsonify({'success': False, 'message': 'Aucune image téléchargée'})
|
353 |
+
|
354 |
+
file = request.files['image']
|
355 |
+
if file.filename == '':
|
356 |
+
return jsonify({'success': False, 'message': 'Aucun fichier sélectionné'})
|
357 |
+
|
358 |
+
# Lire l'image
|
359 |
+
image_bytes = file.read()
|
360 |
+
|
361 |
+
# Initialiser le client
|
362 |
+
client, message = initialize_genai_client()
|
363 |
+
if client is None:
|
364 |
+
return jsonify({'success': False, 'message': message})
|
365 |
+
|
366 |
+
# Générer le LaTeX
|
367 |
+
latex_content, thinking_content, error = generate_complete_latex(client, image_bytes)
|
368 |
+
if error:
|
369 |
+
return jsonify({'success': False, 'message': error})
|
370 |
+
|
371 |
+
# Compiler en PDF
|
372 |
+
pdf_data, message = compile_latex_to_pdf(latex_content)
|
373 |
+
if pdf_data is None:
|
374 |
+
return jsonify({
|
375 |
+
'success': False,
|
376 |
+
'message': message,
|
377 |
+
'latex': latex_content,
|
378 |
+
'thinking': thinking_content
|
379 |
+
})
|
380 |
+
|
381 |
+
# Encoder le PDF en base64 pour le retourner au front-end
|
382 |
+
pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
|
383 |
+
|
384 |
+
# Retourner les données
|
385 |
+
return jsonify({
|
386 |
+
'success': True,
|
387 |
+
'message': 'PDF généré avec succès',
|
388 |
+
'latex': latex_content,
|
389 |
+
'thinking': thinking_content,
|
390 |
+
'pdf_base64': pdf_base64
|
391 |
+
})
|
392 |
+
|
393 |
+
@app.route('/download-pdf', methods=['POST'])
|
394 |
+
def download_pdf():
|
395 |
+
if 'pdf_data' not in request.json:
|
396 |
+
return jsonify({'success': False, 'message': 'Données PDF manquantes'})
|
397 |
+
|
398 |
+
try:
|
399 |
+
pdf_data = base64.b64decode(request.json['pdf_data'])
|
400 |
+
|
401 |
+
# Créer un fichier temporaire pour le PDF
|
402 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
|
403 |
+
temp_file.write(pdf_data)
|
404 |
+
temp_path = temp_file.name
|
405 |
+
|
406 |
+
# Envoyer le fichier
|
407 |
+
return send_file(
|
408 |
+
temp_path,
|
409 |
+
mimetype='application/pdf',
|
410 |
+
as_attachment=True,
|
411 |
+
download_name='solution_mariam_ai.pdf'
|
412 |
+
)
|
413 |
+
except Exception as e:
|
414 |
+
return jsonify({'success': False, 'message': f'Erreur lors du téléchargement: {str(e)}'})
|
415 |
+
|
416 |
+
if __name__ == "__main__":
|
417 |
+
app.run(debug=True)
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask
|
2 |
+
phidata
|
3 |
+
google-genai>=0.6.0
|
4 |
+
pillow
|
5 |
+
python-dotenv
|
static/pdfs/j.txt
ADDED
File without changes
|
templates/index.html
ADDED
@@ -0,0 +1,797 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>Mariam AI - Correcteur d'Exercices</title>
|
7 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
8 |
+
<style>
|
9 |
+
:root {
|
10 |
+
--primary: #4f46e5;
|
11 |
+
--primary-hover: #4338ca;
|
12 |
+
--primary-light: #eef2ff;
|
13 |
+
--success: #10b981;
|
14 |
+
--success-light: #ecfdf5;
|
15 |
+
--error: #ef4444;
|
16 |
+
--error-light: #fef2f2;
|
17 |
+
--text: #1f2937;
|
18 |
+
--text-light: #6b7280;
|
19 |
+
--bg-light: #f9fafb;
|
20 |
+
--card-bg: #ffffff;
|
21 |
+
--border: #e5e7eb;
|
22 |
+
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
23 |
+
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
24 |
+
--radius: 0.5rem;
|
25 |
+
}
|
26 |
+
|
27 |
+
* {
|
28 |
+
box-sizing: border-box;
|
29 |
+
margin: 0;
|
30 |
+
padding: 0;
|
31 |
+
}
|
32 |
+
|
33 |
+
body {
|
34 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
35 |
+
line-height: 1.6;
|
36 |
+
color: var(--text);
|
37 |
+
background-color: var(--bg-light);
|
38 |
+
padding: 1.5rem;
|
39 |
+
}
|
40 |
+
|
41 |
+
.container {
|
42 |
+
max-width: 1000px;
|
43 |
+
margin: 0 auto;
|
44 |
+
}
|
45 |
+
|
46 |
+
.header {
|
47 |
+
text-align: center;
|
48 |
+
margin-bottom: 2rem;
|
49 |
+
}
|
50 |
+
|
51 |
+
h1 {
|
52 |
+
font-size: 2rem;
|
53 |
+
font-weight: 700;
|
54 |
+
margin-bottom: 0.5rem;
|
55 |
+
color: var(--primary);
|
56 |
+
}
|
57 |
+
|
58 |
+
h2 {
|
59 |
+
font-size: 1.25rem;
|
60 |
+
font-weight: 600;
|
61 |
+
margin-bottom: 1rem;
|
62 |
+
color: var(--text);
|
63 |
+
}
|
64 |
+
|
65 |
+
h3 {
|
66 |
+
font-size: 1rem;
|
67 |
+
font-weight: 600;
|
68 |
+
margin-bottom: 0.75rem;
|
69 |
+
color: var(--text);
|
70 |
+
}
|
71 |
+
|
72 |
+
.subheader {
|
73 |
+
color: var(--text-light);
|
74 |
+
font-size: 1rem;
|
75 |
+
}
|
76 |
+
|
77 |
+
.card {
|
78 |
+
background: var(--card-bg);
|
79 |
+
border-radius: var(--radius);
|
80 |
+
box-shadow: var(--shadow);
|
81 |
+
padding: 1.5rem;
|
82 |
+
margin-bottom: 1.5rem;
|
83 |
+
}
|
84 |
+
|
85 |
+
.status-checks {
|
86 |
+
display: grid;
|
87 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
88 |
+
gap: 1rem;
|
89 |
+
}
|
90 |
+
|
91 |
+
.status-item {
|
92 |
+
padding: 1rem;
|
93 |
+
border-radius: var(--radius);
|
94 |
+
display: flex;
|
95 |
+
align-items: center;
|
96 |
+
gap: 0.75rem;
|
97 |
+
}
|
98 |
+
|
99 |
+
.status-success {
|
100 |
+
background-color: var(--success-light);
|
101 |
+
border: 1px solid var(--success);
|
102 |
+
}
|
103 |
+
|
104 |
+
.status-error {
|
105 |
+
background-color: var(--error-light);
|
106 |
+
border: 1px solid var(--error);
|
107 |
+
}
|
108 |
+
|
109 |
+
.status-icon {
|
110 |
+
font-size: 1.25rem;
|
111 |
+
}
|
112 |
+
|
113 |
+
.status-success .status-icon {
|
114 |
+
color: var(--success);
|
115 |
+
}
|
116 |
+
|
117 |
+
.status-error .status-icon {
|
118 |
+
color: var(--error);
|
119 |
+
}
|
120 |
+
|
121 |
+
.status-text {
|
122 |
+
flex: 1;
|
123 |
+
}
|
124 |
+
|
125 |
+
.file-upload {
|
126 |
+
display: flex;
|
127 |
+
flex-direction: column;
|
128 |
+
gap: 1rem;
|
129 |
+
}
|
130 |
+
|
131 |
+
.file-input-container {
|
132 |
+
position: relative;
|
133 |
+
width: 100%;
|
134 |
+
height: 150px;
|
135 |
+
border: 2px dashed var(--border);
|
136 |
+
border-radius: var(--radius);
|
137 |
+
display: flex;
|
138 |
+
flex-direction: column;
|
139 |
+
justify-content: center;
|
140 |
+
align-items: center;
|
141 |
+
gap: 0.75rem;
|
142 |
+
padding: 1.5rem;
|
143 |
+
cursor: pointer;
|
144 |
+
overflow: hidden;
|
145 |
+
transition: border-color 0.3s ease, background 0.3s ease;
|
146 |
+
}
|
147 |
+
|
148 |
+
.file-input-container:hover, .file-input-container.dragover {
|
149 |
+
border-color: var(--primary);
|
150 |
+
background: var(--primary-light);
|
151 |
+
}
|
152 |
+
|
153 |
+
.file-input {
|
154 |
+
position: absolute;
|
155 |
+
top: 0;
|
156 |
+
left: 0;
|
157 |
+
width: 100%;
|
158 |
+
height: 100%;
|
159 |
+
opacity: 0;
|
160 |
+
cursor: pointer;
|
161 |
+
}
|
162 |
+
|
163 |
+
.upload-icon {
|
164 |
+
font-size: 2rem;
|
165 |
+
color: var(--primary);
|
166 |
+
}
|
167 |
+
|
168 |
+
.upload-label {
|
169 |
+
font-weight: 500;
|
170 |
+
}
|
171 |
+
|
172 |
+
.upload-hint {
|
173 |
+
font-size: 0.875rem;
|
174 |
+
color: var(--text-light);
|
175 |
+
}
|
176 |
+
|
177 |
+
.file-preview {
|
178 |
+
display: none;
|
179 |
+
position: absolute;
|
180 |
+
top: 0;
|
181 |
+
left: 0;
|
182 |
+
width: 100%;
|
183 |
+
height: 100%;
|
184 |
+
z-index: 1;
|
185 |
+
background: rgba(255, 255, 255, 0.9);
|
186 |
+
align-items: center;
|
187 |
+
justify-content: center;
|
188 |
+
}
|
189 |
+
|
190 |
+
.file-preview img {
|
191 |
+
max-width: 90%;
|
192 |
+
max-height: 90%;
|
193 |
+
object-fit: contain;
|
194 |
+
}
|
195 |
+
|
196 |
+
.preview-active .file-preview {
|
197 |
+
display: flex;
|
198 |
+
}
|
199 |
+
|
200 |
+
.file-name {
|
201 |
+
display: none;
|
202 |
+
position: absolute;
|
203 |
+
bottom: 0;
|
204 |
+
left: 0;
|
205 |
+
right: 0;
|
206 |
+
background: rgba(255, 255, 255, 0.8);
|
207 |
+
padding: 0.5rem;
|
208 |
+
font-size: 0.875rem;
|
209 |
+
text-align: center;
|
210 |
+
word-break: break-all;
|
211 |
+
}
|
212 |
+
|
213 |
+
.preview-active .file-name {
|
214 |
+
display: block;
|
215 |
+
}
|
216 |
+
|
217 |
+
.clear-file {
|
218 |
+
display: none;
|
219 |
+
position: absolute;
|
220 |
+
top: 0.5rem;
|
221 |
+
right: 0.5rem;
|
222 |
+
background: white;
|
223 |
+
border-radius: 50%;
|
224 |
+
width: 24px;
|
225 |
+
height: 24px;
|
226 |
+
align-items: center;
|
227 |
+
justify-content: center;
|
228 |
+
cursor: pointer;
|
229 |
+
box-shadow: var(--shadow);
|
230 |
+
z-index: 2;
|
231 |
+
}
|
232 |
+
|
233 |
+
.preview-active .clear-file {
|
234 |
+
display: flex;
|
235 |
+
}
|
236 |
+
|
237 |
+
.button {
|
238 |
+
display: inline-flex;
|
239 |
+
align-items: center;
|
240 |
+
justify-content: center;
|
241 |
+
gap: 0.5rem;
|
242 |
+
background-color: var(--primary);
|
243 |
+
color: white;
|
244 |
+
padding: 0.75rem 1.5rem;
|
245 |
+
border: none;
|
246 |
+
border-radius: var(--radius);
|
247 |
+
font-weight: 500;
|
248 |
+
font-size: 1rem;
|
249 |
+
cursor: pointer;
|
250 |
+
transition: background-color 0.3s, transform 0.2s;
|
251 |
+
width: 100%;
|
252 |
+
}
|
253 |
+
|
254 |
+
.button:hover {
|
255 |
+
background-color: var(--primary-hover);
|
256 |
+
}
|
257 |
+
|
258 |
+
.button:active {
|
259 |
+
transform: translateY(1px);
|
260 |
+
}
|
261 |
+
|
262 |
+
.button:disabled {
|
263 |
+
background-color: var(--text-light);
|
264 |
+
cursor: not-allowed;
|
265 |
+
opacity: 0.7;
|
266 |
+
}
|
267 |
+
|
268 |
+
.button-icon {
|
269 |
+
font-size: 1rem;
|
270 |
+
}
|
271 |
+
|
272 |
+
.loading {
|
273 |
+
display: flex;
|
274 |
+
flex-direction: column;
|
275 |
+
align-items: center;
|
276 |
+
gap: 1rem;
|
277 |
+
padding: 2rem;
|
278 |
+
}
|
279 |
+
|
280 |
+
.spinner {
|
281 |
+
width: 40px;
|
282 |
+
height: 40px;
|
283 |
+
border: 4px solid rgba(79, 70, 229, 0.2);
|
284 |
+
border-radius: 50%;
|
285 |
+
border-top-color: var(--primary);
|
286 |
+
animation: spin 1s linear infinite;
|
287 |
+
}
|
288 |
+
|
289 |
+
@keyframes spin {
|
290 |
+
0% { transform: rotate(0deg); }
|
291 |
+
100% { transform: rotate(360deg); }
|
292 |
+
}
|
293 |
+
|
294 |
+
.message {
|
295 |
+
padding: 1rem;
|
296 |
+
border-radius: var(--radius);
|
297 |
+
margin-bottom: 1.5rem;
|
298 |
+
display: flex;
|
299 |
+
align-items: center;
|
300 |
+
gap: 0.75rem;
|
301 |
+
}
|
302 |
+
|
303 |
+
.message-success {
|
304 |
+
background-color: var(--success-light);
|
305 |
+
border: 1px solid var(--success);
|
306 |
+
color: var(--success);
|
307 |
+
}
|
308 |
+
|
309 |
+
.message-error {
|
310 |
+
background-color: var(--error-light);
|
311 |
+
border: 1px solid var(--error);
|
312 |
+
color: var(--error);
|
313 |
+
white-space: pre-wrap;
|
314 |
+
}
|
315 |
+
|
316 |
+
.tab-container {
|
317 |
+
border: 1px solid var(--border);
|
318 |
+
border-radius: var(--radius);
|
319 |
+
overflow: hidden;
|
320 |
+
}
|
321 |
+
|
322 |
+
.tabs {
|
323 |
+
display: flex;
|
324 |
+
background: var(--bg-light);
|
325 |
+
}
|
326 |
+
|
327 |
+
.tab {
|
328 |
+
padding: 0.75rem 1.25rem;
|
329 |
+
cursor: pointer;
|
330 |
+
border-bottom: 2px solid transparent;
|
331 |
+
font-weight: 500;
|
332 |
+
color: var(--text-light);
|
333 |
+
transition: all 0.3s ease;
|
334 |
+
}
|
335 |
+
|
336 |
+
.tab.active {
|
337 |
+
color: var(--primary);
|
338 |
+
border-bottom-color: var(--primary);
|
339 |
+
}
|
340 |
+
|
341 |
+
.tab-content {
|
342 |
+
display: none;
|
343 |
+
padding: 1.5rem;
|
344 |
+
}
|
345 |
+
|
346 |
+
.tab-content.active {
|
347 |
+
display: block;
|
348 |
+
}
|
349 |
+
|
350 |
+
#pdf-viewer {
|
351 |
+
width: 100%;
|
352 |
+
height: 600px;
|
353 |
+
border: 1px solid var(--border);
|
354 |
+
border-radius: var(--radius);
|
355 |
+
}
|
356 |
+
|
357 |
+
.code-area {
|
358 |
+
background-color: #f8fafc;
|
359 |
+
border: 1px solid var(--border);
|
360 |
+
border-radius: 0.25rem;
|
361 |
+
padding: 1rem;
|
362 |
+
max-height: 400px;
|
363 |
+
overflow-y: auto;
|
364 |
+
}
|
365 |
+
|
366 |
+
.code-area pre {
|
367 |
+
margin: 0;
|
368 |
+
white-space: pre-wrap;
|
369 |
+
word-wrap: break-word;
|
370 |
+
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
371 |
+
font-size: 0.875rem;
|
372 |
+
line-height: 1.5;
|
373 |
+
}
|
374 |
+
|
375 |
+
.download-button {
|
376 |
+
display: inline-flex;
|
377 |
+
align-items: center;
|
378 |
+
justify-content: center;
|
379 |
+
gap: 0.5rem;
|
380 |
+
background-color: var(--primary);
|
381 |
+
color: white;
|
382 |
+
padding: 0.75rem 1.5rem;
|
383 |
+
border: none;
|
384 |
+
border-radius: var(--radius);
|
385 |
+
font-weight: 500;
|
386 |
+
font-size: 1rem;
|
387 |
+
cursor: pointer;
|
388 |
+
transition: background-color 0.3s, transform 0.2s;
|
389 |
+
margin: 1.5rem auto;
|
390 |
+
width: auto;
|
391 |
+
}
|
392 |
+
|
393 |
+
.hidden {
|
394 |
+
display: none !important;
|
395 |
+
}
|
396 |
+
|
397 |
+
/* Responsive design */
|
398 |
+
@media (max-width: 768px) {
|
399 |
+
.status-checks {
|
400 |
+
grid-template-columns: 1fr;
|
401 |
+
}
|
402 |
+
|
403 |
+
.file-upload-container {
|
404 |
+
height: 120px;
|
405 |
+
}
|
406 |
+
|
407 |
+
.tab {
|
408 |
+
padding: 0.5rem 1rem;
|
409 |
+
font-size: 0.875rem;
|
410 |
+
}
|
411 |
+
|
412 |
+
#pdf-viewer {
|
413 |
+
height: 400px;
|
414 |
+
}
|
415 |
+
}
|
416 |
+
</style>
|
417 |
+
</head>
|
418 |
+
<body>
|
419 |
+
<div class="container">
|
420 |
+
<div class="header">
|
421 |
+
<h1>Mariam AI</h1>
|
422 |
+
<p class="subheader">Correcteur Intelligent d'Exercices Mathématiques/physique/chimie.</p>
|
423 |
+
</div>
|
424 |
+
|
425 |
+
<div class="card">
|
426 |
+
<h2>État du système</h2>
|
427 |
+
<div class="status-checks">
|
428 |
+
<div id="latex-status" class="status-item">
|
429 |
+
<span class="status-icon"><i class="fas fa-spinner fa-spin"></i></span>
|
430 |
+
<span class="status-text">Vérification de LaTeX...</span>
|
431 |
+
</div>
|
432 |
+
<div id="api-status" class="status-item">
|
433 |
+
<span class="status-icon"><i class="fas fa-spinner fa-spin"></i></span>
|
434 |
+
<span class="status-text">Vérification de l'API Mariam AI...</span>
|
435 |
+
</div>
|
436 |
+
</div>
|
437 |
+
</div>
|
438 |
+
|
439 |
+
<div class="card">
|
440 |
+
<h2>Soumettre un exercice</h2>
|
441 |
+
<div class="file-upload">
|
442 |
+
<div id="file-input-container" class="file-input-container">
|
443 |
+
<span class="upload-icon"><i class="fas fa-cloud-upload-alt"></i></span>
|
444 |
+
<span class="upload-label">Déposez votre image ou cliquez pour sélectionner</span>
|
445 |
+
<span class="upload-hint">Formats acceptés: JPG, PNG, GIF</span>
|
446 |
+
<input type="file" id="image-input" class="file-input" accept="image/*">
|
447 |
+
|
448 |
+
<div class="file-preview">
|
449 |
+
<img id="image-preview" src="" alt="Aperçu">
|
450 |
+
</div>
|
451 |
+
<div class="file-name" id="file-name"></div>
|
452 |
+
<div class="clear-file" id="clear-file"><i class="fas fa-times"></i></div>
|
453 |
+
</div>
|
454 |
+
|
455 |
+
<button id="process-button" class="button" disabled>
|
456 |
+
<span class="button-icon"><i class="fas fa-magic"></i></span>
|
457 |
+
<span>Générer la solution</span>
|
458 |
+
</button>
|
459 |
+
</div>
|
460 |
+
</div>
|
461 |
+
|
462 |
+
<div id="loading" class="card loading hidden">
|
463 |
+
<div class="spinner"></div>
|
464 |
+
<p>Mariam AI analyse l'exercice et génère la solution...</p>
|
465 |
+
<p class="upload-hint">Cette opération peut prendre jusqu'à une minute.</p>
|
466 |
+
</div>
|
467 |
+
|
468 |
+
<div id="messages" class="message hidden"></div>
|
469 |
+
|
470 |
+
<div id="results" class="card hidden">
|
471 |
+
<h2>Résultat de l'analyse</h2>
|
472 |
+
|
473 |
+
<div class="tab-container">
|
474 |
+
<div class="tabs">
|
475 |
+
<div class="tab active" data-tab="pdf">Aperçu PDF</div>
|
476 |
+
<div class="tab" data-tab="latex">Code LaTeX</div>
|
477 |
+
<div class="tab" data-tab="thinking">Processus de réflexion</div>
|
478 |
+
</div>
|
479 |
+
|
480 |
+
<div id="pdf-tab" class="tab-content active">
|
481 |
+
<iframe id="pdf-viewer" title="Aperçu du PDF de la solution"></iframe>
|
482 |
+
<div class="download-container">
|
483 |
+
<button id="download-button" class="download-button">
|
484 |
+
<i class="fas fa-download"></i> Télécharger le PDF
|
485 |
+
</button>
|
486 |
+
</div>
|
487 |
+
</div>
|
488 |
+
|
489 |
+
<div id="latex-tab" class="tab-content">
|
490 |
+
<div class="code-area">
|
491 |
+
<pre id="latex-output"></pre>
|
492 |
+
</div>
|
493 |
+
</div>
|
494 |
+
|
495 |
+
<div id="thinking-tab" class="tab-content">
|
496 |
+
<div class="code-area">
|
497 |
+
<pre id="thinking-output"></pre>
|
498 |
+
</div>
|
499 |
+
</div>
|
500 |
+
</div>
|
501 |
+
</div>
|
502 |
+
</div>
|
503 |
+
|
504 |
+
<script>
|
505 |
+
document.addEventListener('DOMContentLoaded', () => {
|
506 |
+
// Éléments DOM
|
507 |
+
const latexStatusEl = document.getElementById('latex-status');
|
508 |
+
const apiStatusEl = document.getElementById('api-status');
|
509 |
+
const fileInputContainer = document.getElementById('file-input-container');
|
510 |
+
const imageInput = document.getElementById('image-input');
|
511 |
+
const imagePreview = document.getElementById('image-preview');
|
512 |
+
const fileName = document.getElementById('file-name');
|
513 |
+
const clearFile = document.getElementById('clear-file');
|
514 |
+
const processButton = document.getElementById('process-button');
|
515 |
+
const loadingEl = document.getElementById('loading');
|
516 |
+
const messagesEl = document.getElementById('messages');
|
517 |
+
const resultsEl = document.getElementById('results');
|
518 |
+
const pdfViewer = document.getElementById('pdf-viewer');
|
519 |
+
const downloadButton = document.getElementById('download-button');
|
520 |
+
const latexOutputEl = document.getElementById('latex-output');
|
521 |
+
const thinkingOutputEl = document.getElementById('thinking-output');
|
522 |
+
const tabs = document.querySelectorAll('.tab');
|
523 |
+
const tabContents = document.querySelectorAll('.tab-content');
|
524 |
+
|
525 |
+
let currentPdfBase64 = null; // Stockage des données PDF
|
526 |
+
|
527 |
+
// --- Gestion des onglets ---
|
528 |
+
tabs.forEach(tab => {
|
529 |
+
tab.addEventListener('click', () => {
|
530 |
+
// Désactiver tous les onglets
|
531 |
+
tabs.forEach(t => t.classList.remove('active'));
|
532 |
+
tabContents.forEach(c => c.classList.remove('active'));
|
533 |
+
|
534 |
+
// Activer l'onglet sélectionné
|
535 |
+
tab.classList.add('active');
|
536 |
+
document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active');
|
537 |
+
});
|
538 |
+
});
|
539 |
+
|
540 |
+
// --- Gestion du chargement des images ---
|
541 |
+
imageInput.addEventListener('change', handleFileSelect);
|
542 |
+
|
543 |
+
// Gestion du drag and drop
|
544 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
545 |
+
fileInputContainer.addEventListener(eventName, preventDefaults, false);
|
546 |
+
});
|
547 |
+
|
548 |
+
['dragenter', 'dragover'].forEach(eventName => {
|
549 |
+
fileInputContainer.addEventListener(eventName, () => {
|
550 |
+
fileInputContainer.classList.add('dragover');
|
551 |
+
}, false);
|
552 |
+
});
|
553 |
+
|
554 |
+
['dragleave', 'drop'].forEach(eventName => {
|
555 |
+
fileInputContainer.addEventListener(eventName, () => {
|
556 |
+
fileInputContainer.classList.remove('dragover');
|
557 |
+
}, false);
|
558 |
+
});
|
559 |
+
|
560 |
+
fileInputContainer.addEventListener('drop', handleDrop, false);
|
561 |
+
|
562 |
+
// Gestion du bouton de suppression de fichier
|
563 |
+
clearFile.addEventListener('click', (e) => {
|
564 |
+
e.stopPropagation();
|
565 |
+
clearFileInput();
|
566 |
+
});
|
567 |
+
|
568 |
+
function preventDefaults(e) {
|
569 |
+
e.preventDefault();
|
570 |
+
e.stopPropagation();
|
571 |
+
}
|
572 |
+
|
573 |
+
function handleDrop(e) {
|
574 |
+
const dt = e.dataTransfer;
|
575 |
+
const files = dt.files;
|
576 |
+
|
577 |
+
if (files && files[0]) {
|
578 |
+
imageInput.files = files;
|
579 |
+
handleFileSelect();
|
580 |
+
}
|
581 |
+
}
|
582 |
+
|
583 |
+
function handleFileSelect() {
|
584 |
+
const file = imageInput.files[0];
|
585 |
+
|
586 |
+
if (file) {
|
587 |
+
const reader = new FileReader();
|
588 |
+
|
589 |
+
reader.onload = function(e) {
|
590 |
+
imagePreview.src = e.target.result;
|
591 |
+
fileName.textContent = file.name;
|
592 |
+
fileInputContainer.classList.add('preview-active');
|
593 |
+
processButton.disabled = false;
|
594 |
+
};
|
595 |
+
|
596 |
+
reader.readAsDataURL(file);
|
597 |
+
} else {
|
598 |
+
clearFileInput();
|
599 |
+
}
|
600 |
+
}
|
601 |
+
|
602 |
+
function clearFileInput() {
|
603 |
+
imageInput.value = '';
|
604 |
+
imagePreview.src = '';
|
605 |
+
fileName.textContent = '';
|
606 |
+
fileInputContainer.classList.remove('preview-active');
|
607 |
+
processButton.disabled = true;
|
608 |
+
}
|
609 |
+
|
610 |
+
// --- Fonctions Utilitaires ---
|
611 |
+
function showLoading() {
|
612 |
+
loadingEl.classList.remove('hidden');
|
613 |
+
processButton.disabled = true;
|
614 |
+
messagesEl.classList.add('hidden');
|
615 |
+
resultsEl.classList.add('hidden');
|
616 |
+
pdfViewer.src = 'about:blank'; // Vider l'aperçu PDF
|
617 |
+
latexOutputEl.textContent = '';
|
618 |
+
thinkingOutputEl.textContent = '';
|
619 |
+
currentPdfBase64 = null;
|
620 |
+
}
|
621 |
+
|
622 |
+
function hideLoading() {
|
623 |
+
loadingEl.classList.add('hidden');
|
624 |
+
processButton.disabled = !imageInput.files[0];
|
625 |
+
}
|
626 |
+
|
627 |
+
function showMessage(message, isError = false) {
|
628 |
+
messagesEl.innerHTML = `
|
629 |
+
<i class="fas ${isError ? 'fa-exclamation-circle' : 'fa-check-circle'}"></i>
|
630 |
+
<div>${message}</div>
|
631 |
+
`;
|
632 |
+
messagesEl.className = `message ${isError ? 'message-error' : 'message-success'}`;
|
633 |
+
messagesEl.classList.remove('hidden');
|
634 |
+
}
|
635 |
+
|
636 |
+
function displayResults(data) {
|
637 |
+
resultsEl.classList.remove('hidden');
|
638 |
+
|
639 |
+
// Gestion du PDF
|
640 |
+
if (data.pdf_base64) {
|
641 |
+
pdfViewer.src = `data:application/pdf;base64,${data.pdf_base64}`;
|
642 |
+
currentPdfBase64 = data.pdf_base64;
|
643 |
+
downloadButton.classList.remove('hidden');
|
644 |
+
} else {
|
645 |
+
pdfViewer.src = 'about:blank';
|
646 |
+
downloadButton.classList.add('hidden');
|
647 |
+
}
|
648 |
+
|
649 |
+
// Gestion du LaTeX
|
650 |
+
latexOutputEl.textContent = data.latex || "Aucun code LaTeX disponible.";
|
651 |
+
|
652 |
+
// Gestion du processus de réflexion
|
653 |
+
thinkingOutputEl.textContent = data.thinking || "Aucun processus de réflexion disponible.";
|
654 |
+
|
655 |
+
// Activer l'onglet approprié par défaut
|
656 |
+
if (data.pdf_base64) {
|
657 |
+
activateTab('pdf');
|
658 |
+
} else if (data.latex) {
|
659 |
+
activateTab('latex');
|
660 |
+
} else if (data.thinking) {
|
661 |
+
activateTab('thinking');
|
662 |
+
}
|
663 |
+
}
|
664 |
+
|
665 |
+
function activateTab(tabName) {
|
666 |
+
tabs.forEach(t => t.classList.remove('active'));
|
667 |
+
tabContents.forEach(c => c.classList.remove('active'));
|
668 |
+
|
669 |
+
document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active');
|
670 |
+
document.getElementById(`${tabName}-tab`).classList.add('active');
|
671 |
+
}
|
672 |
+
|
673 |
+
// --- Vérifications Initiales ---
|
674 |
+
async function checkStatus() {
|
675 |
+
try {
|
676 |
+
const latexRes = await fetch('/check-latex');
|
677 |
+
const latexData = await latexRes.json();
|
678 |
+
|
679 |
+
latexStatusEl.innerHTML = `
|
680 |
+
<span class="status-icon"><i class="fas ${latexData.success ? 'fa-check-circle' : 'fa-times-circle'}"></i></span>
|
681 |
+
<span class="status-text">${latexData.message}</span>
|
682 |
+
`;
|
683 |
+
latexStatusEl.className = `status-item ${latexData.success ? 'status-success' : 'status-error'}`;
|
684 |
+
} catch (error) {
|
685 |
+
latexStatusEl.innerHTML = `
|
686 |
+
<span class="status-icon"><i class="fas fa-times-circle"></i></span>
|
687 |
+
<span class="status-text">Erreur lors de la vérification de LaTeX: ${error}</span>
|
688 |
+
`;
|
689 |
+
latexStatusEl.className = 'status-item status-error';
|
690 |
+
}
|
691 |
+
|
692 |
+
try {
|
693 |
+
const apiRes = await fetch('/check-api');
|
694 |
+
const apiData = await apiRes.json();
|
695 |
+
|
696 |
+
apiStatusEl.innerHTML = `
|
697 |
+
<span class="status-icon"><i class="fas ${apiData.success ? 'fa-check-circle' : 'fa-times-circle'}"></i></span>
|
698 |
+
<span class="status-text">${apiData.message}</span>
|
699 |
+
`;
|
700 |
+
apiStatusEl.className = `status-item ${apiData.success ? 'status-success' : 'status-error'}`;
|
701 |
+
} catch (error) {
|
702 |
+
apiStatusEl.innerHTML = `
|
703 |
+
<span class="status-icon"><i class="fas fa-times-circle"></i></span>
|
704 |
+
<span class="status-text">Erreur lors de la vérification de l'API: ${error}</span>
|
705 |
+
`;
|
706 |
+
apiStatusEl.className = 'status-item status-error';
|
707 |
+
}
|
708 |
+
}
|
709 |
+
|
710 |
+
// --- Traitement de l'Image ---
|
711 |
+
processButton.addEventListener('click', async () => {
|
712 |
+
const file = imageInput.files[0];
|
713 |
+
if (!file) {
|
714 |
+
showMessage('Veuillez sélectionner un fichier image.', true);
|
715 |
+
return;
|
716 |
+
}
|
717 |
+
|
718 |
+
showLoading();
|
719 |
+
|
720 |
+
const formData = new FormData();
|
721 |
+
formData.append('image', file);
|
722 |
+
|
723 |
+
try {
|
724 |
+
const response = await fetch('/process', {
|
725 |
+
method: 'POST',
|
726 |
+
body: formData
|
727 |
+
});
|
728 |
+
|
729 |
+
const data = await response.json();
|
730 |
+
hideLoading();
|
731 |
+
|
732 |
+
if (data.success) {
|
733 |
+
showMessage('Solution générée avec succès !');
|
734 |
+
displayResults(data);
|
735 |
+
} else {
|
736 |
+
showMessage(`Erreur : ${data.message}`, true);
|
737 |
+
// Afficher le LaTeX/Thinking même en cas d'erreur de compilation
|
738 |
+
if(data.latex || data.thinking) {
|
739 |
+
displayResults(data);
|
740 |
+
}
|
741 |
+
}
|
742 |
+
|
743 |
+
} catch (error) {
|
744 |
+
hideLoading();
|
745 |
+
showMessage(`Erreur de communication avec le serveur : ${error}`, true);
|
746 |
+
console.error("Fetch Error:", error);
|
747 |
+
}
|
748 |
+
});
|
749 |
+
|
750 |
+
// --- Téléchargement du PDF ---
|
751 |
+
downloadButton.addEventListener('click', async () => {
|
752 |
+
if (!currentPdfBase64) {
|
753 |
+
showMessage('Aucune donnée PDF à télécharger.', true);
|
754 |
+
return;
|
755 |
+
}
|
756 |
+
|
757 |
+
try {
|
758 |
+
const response = await fetch('/download-pdf', {
|
759 |
+
method: 'POST',
|
760 |
+
headers: {
|
761 |
+
'Content-Type': 'application/json',
|
762 |
+
},
|
763 |
+
body: JSON.stringify({ pdf_data: currentPdfBase64 }),
|
764 |
+
});
|
765 |
+
|
766 |
+
if (response.ok) {
|
767 |
+
const blob = await response.blob();
|
768 |
+
const url = window.URL.createObjectURL(blob);
|
769 |
+
const a = document.createElement('a');
|
770 |
+
a.style.display = 'none';
|
771 |
+
a.href = url;
|
772 |
+
a.download = response.headers.get('Content-Disposition')?.split('filename=')[1]?.replaceAll('"', '') || 'solution_mariam_ai.pdf';
|
773 |
+
document.body.appendChild(a);
|
774 |
+
a.click();
|
775 |
+
window.URL.revokeObjectURL(url);
|
776 |
+
a.remove();
|
777 |
+
showMessage('Téléchargement démarré.');
|
778 |
+
} else {
|
779 |
+
let errorMsg = `Échec du téléchargement (code ${response.status}).`;
|
780 |
+
try {
|
781 |
+
const errorData = await response.json();
|
782 |
+
if(errorData.message) errorMsg += ` Raison: ${errorData.message}`;
|
783 |
+
} catch(e) { /* Ignorer l'erreur si la réponse n'est pas JSON */ }
|
784 |
+
showMessage(errorMsg, true);
|
785 |
+
}
|
786 |
+
} catch (error) {
|
787 |
+
showMessage(`Erreur lors de la tentative de téléchargement : ${error}`, true);
|
788 |
+
console.error("Download Error:", error);
|
789 |
+
}
|
790 |
+
});
|
791 |
+
|
792 |
+
// Exécuter les vérifications au chargement
|
793 |
+
checkStatus();
|
794 |
+
});
|
795 |
+
</script>
|
796 |
+
</body>
|
797 |
+
</html>
|