Update app.py
Browse files
app.py
CHANGED
|
@@ -37,14 +37,14 @@ MODELS = {
|
|
| 37 |
"default_api": True
|
| 38 |
},
|
| 39 |
"ChatGPT 5 (OpenAI)": {
|
| 40 |
-
"provider": "OpenAI",
|
| 41 |
"class": ChatOpenAI,
|
| 42 |
"model_name": "gpt-4o",
|
| 43 |
"default_api": False
|
| 44 |
},
|
| 45 |
"Claude Sonnet 4 (Anthropic)": {
|
| 46 |
"provider": "Anthropic",
|
| 47 |
-
"class": ChatAnthropic,
|
| 48 |
"model_name": "claude-3-5-sonnet-20241022",
|
| 49 |
"default_api": False
|
| 50 |
},
|
|
@@ -63,9 +63,7 @@ def extract_text_from_file(file):
|
|
| 63 |
"""Extrait le texte d'un fichier uploadé"""
|
| 64 |
if file is None:
|
| 65 |
return ""
|
| 66 |
-
|
| 67 |
file_extension = os.path.splitext(file.name)[1].lower()
|
| 68 |
-
|
| 69 |
try:
|
| 70 |
if file_extension == '.pdf':
|
| 71 |
with open(file.name, 'rb') as f:
|
|
@@ -74,22 +72,18 @@ def extract_text_from_file(file):
|
|
| 74 |
for page in reader.pages:
|
| 75 |
text += page.extract_text() + "\n"
|
| 76 |
return text
|
| 77 |
-
|
| 78 |
elif file_extension == '.docx':
|
| 79 |
doc = docx.Document(file.name)
|
| 80 |
text = ""
|
| 81 |
for paragraph in doc.paragraphs:
|
| 82 |
text += paragraph.text + "\n"
|
| 83 |
return text
|
| 84 |
-
|
| 85 |
elif file_extension == '.txt':
|
| 86 |
with open(file.name, 'r', encoding='utf-8') as f:
|
| 87 |
return f.read()
|
| 88 |
-
|
| 89 |
elif file_extension in ['.xlsx', '.xls']:
|
| 90 |
df = pd.read_excel(file.name)
|
| 91 |
return df.to_string()
|
| 92 |
-
|
| 93 |
elif file_extension == '.pptx':
|
| 94 |
prs = Presentation(file.name)
|
| 95 |
text = ""
|
|
@@ -98,10 +92,8 @@ def extract_text_from_file(file):
|
|
| 98 |
if hasattr(shape, "text"):
|
| 99 |
text += shape.text + "\n"
|
| 100 |
return text
|
| 101 |
-
|
| 102 |
else:
|
| 103 |
return "Format de fichier non supporté"
|
| 104 |
-
|
| 105 |
except Exception as e:
|
| 106 |
return f"Erreur lors de la lecture du fichier: {str(e)}"
|
| 107 |
|
|
@@ -110,15 +102,10 @@ def extract_text_from_url(url):
|
|
| 110 |
try:
|
| 111 |
response = requests.get(url, timeout=10)
|
| 112 |
response.raise_for_status()
|
| 113 |
-
|
| 114 |
-
# Simple extraction du contenu textuel
|
| 115 |
content = response.text
|
| 116 |
-
# Suppression basique des balises HTML
|
| 117 |
content = re.sub(r'<[^>]+>', '', content)
|
| 118 |
content = re.sub(r'\s+', ' ', content).strip()
|
| 119 |
-
|
| 120 |
return content[:10000] # Limite à 10k caractères
|
| 121 |
-
|
| 122 |
except Exception as e:
|
| 123 |
return f"Erreur lors de la récupération de l'URL: {str(e)}"
|
| 124 |
|
|
@@ -136,7 +123,6 @@ def get_document_content(text_input, url_input, file_input):
|
|
| 136 |
def create_llm_instance(model_name, api_key):
|
| 137 |
"""Crée une instance du modèle LLM"""
|
| 138 |
model_config = MODELS[model_name]
|
| 139 |
-
|
| 140 |
if model_config["provider"] == "OpenAI":
|
| 141 |
return model_config["class"](
|
| 142 |
model=model_config["model_name"],
|
|
@@ -160,23 +146,21 @@ def create_llm_instance(model_name, api_key):
|
|
| 160 |
def generate_html(model_name, api_key, text_input, url_input, file_input):
|
| 161 |
"""Génère le fichier HTML éducatif"""
|
| 162 |
start_time = time.time()
|
| 163 |
-
|
| 164 |
-
# Validation des entrées
|
| 165 |
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip():
|
| 166 |
return None, "❌ Erreur: Veuillez fournir une clé API pour ce modèle.", 0
|
| 167 |
-
|
| 168 |
document_content = get_document_content(text_input, url_input, file_input)
|
| 169 |
if not document_content:
|
| 170 |
return None, "❌ Erreur: Veuillez fournir un document (texte, URL ou fichier).", 0
|
| 171 |
-
|
| 172 |
try:
|
| 173 |
# Création de l'instance LLM
|
| 174 |
llm = create_llm_instance(model_name, api_key)
|
| 175 |
-
|
| 176 |
# Lecture du prompt template
|
| 177 |
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f:
|
| 178 |
prompt_template = f.read()
|
| 179 |
-
|
| 180 |
# Remplacement des variables
|
| 181 |
model_config = MODELS[model_name]
|
| 182 |
prompt = prompt_template.format(
|
|
@@ -184,27 +168,28 @@ def generate_html(model_name, api_key, text_input, url_input, file_input):
|
|
| 184 |
provider_name=model_config["provider"],
|
| 185 |
document=document_content
|
| 186 |
)
|
| 187 |
-
|
| 188 |
# Génération du contenu
|
| 189 |
message = HumanMessage(content=prompt)
|
| 190 |
response = llm.invoke([message])
|
| 191 |
-
|
| 192 |
html_content = response.content
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
# Calcul du temps de génération
|
| 195 |
generation_time = time.time() - start_time
|
| 196 |
-
|
| 197 |
# Sauvegarde du fichier HTML
|
| 198 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 199 |
filename = f"document_educatif_{timestamp}.html"
|
| 200 |
-
|
| 201 |
with open(filename, "w", encoding="utf-8") as f:
|
| 202 |
f.write(html_content)
|
| 203 |
-
|
| 204 |
success_message = f"✅ Fichier HTML généré avec succès en {generation_time:.2f} secondes!"
|
| 205 |
-
|
| 206 |
return filename, success_message, generation_time
|
| 207 |
-
|
| 208 |
except Exception as e:
|
| 209 |
error_message = f"❌ Erreur lors de la génération: {str(e)}"
|
| 210 |
return None, error_message, 0
|
|
@@ -237,169 +222,475 @@ def update_api_info(model_name):
|
|
| 237 |
info="🔑 Clé API requise pour ce modèle"
|
| 238 |
)
|
| 239 |
|
| 240 |
-
# Interface Gradio
|
| 241 |
with gr.Blocks(
|
| 242 |
title="EduHTML Creator - Générateur de contenu éducatif HTML",
|
| 243 |
theme=gr.themes.Soft(),
|
| 244 |
css="""
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
"""
|
| 295 |
) as app:
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
gr.HTML("""
|
| 298 |
-
<div class="header">
|
|
|
|
| 299 |
<h1>🎓 EduHTML Creator</h1>
|
| 300 |
-
<p
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
à partir de vos documents. Le design s'inspire du style Apple pour une expérience utilisateur premium.
|
| 304 |
-
L'objectif éducatif est de faciliter l'apprentissage grâce à la structuration, l'interactivité et la visualisation
|
| 305 |
-
des informations clés de vos documents originaux.
|
| 306 |
</p>
|
|
|
|
| 307 |
</div>
|
| 308 |
""")
|
| 309 |
-
|
| 310 |
-
with gr.Row():
|
| 311 |
with gr.Column(scale=1):
|
| 312 |
-
gr.HTML("<div class='
|
| 313 |
-
|
| 314 |
-
# Sélection du modèle
|
| 315 |
model_dropdown = gr.Dropdown(
|
| 316 |
choices=list(MODELS.keys()),
|
| 317 |
value="Gemini 2.5 Flash (Google AI)",
|
| 318 |
-
label="
|
| 319 |
-
info="
|
| 320 |
)
|
| 321 |
-
|
| 322 |
-
# Champ API
|
| 323 |
api_input = gr.Textbox(
|
| 324 |
label="Clé API (optionnelle)",
|
| 325 |
-
placeholder="API gratuite disponible
|
| 326 |
-
info="
|
| 327 |
type="password"
|
| 328 |
)
|
| 329 |
-
|
| 330 |
gr.HTML("</div>")
|
| 331 |
-
|
| 332 |
-
gr.HTML("<div class='
|
| 333 |
-
gr.HTML("<h3
|
| 334 |
-
|
| 335 |
-
# Entrées de document
|
| 336 |
text_input = gr.Textbox(
|
| 337 |
label="Texte copié/collé",
|
| 338 |
placeholder="Collez votre texte ici...",
|
| 339 |
-
lines=
|
| 340 |
)
|
| 341 |
-
|
| 342 |
url_input = gr.Textbox(
|
| 343 |
label="Lien Web",
|
| 344 |
placeholder="https://exemple.com/article"
|
| 345 |
)
|
| 346 |
-
|
| 347 |
file_input = gr.File(
|
| 348 |
label="Fichier",
|
| 349 |
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"]
|
| 350 |
)
|
| 351 |
-
|
| 352 |
gr.HTML("</div>")
|
| 353 |
-
|
| 354 |
-
# Boutons
|
| 355 |
with gr.Row():
|
| 356 |
-
submit_btn = gr.Button(
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
elem_classes=["apple-button"]
|
| 360 |
-
)
|
| 361 |
-
reset_btn = gr.Button(
|
| 362 |
-
"🔄 Reset",
|
| 363 |
-
elem_classes=["reset-button"]
|
| 364 |
-
)
|
| 365 |
-
|
| 366 |
with gr.Column(scale=1):
|
| 367 |
-
# Statut et résultats
|
| 368 |
status_output = gr.HTML(label="Statut")
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
# Événements
|
| 383 |
model_dropdown.change(
|
| 384 |
fn=update_api_info,
|
| 385 |
inputs=[model_dropdown],
|
| 386 |
outputs=[api_input]
|
| 387 |
)
|
| 388 |
-
|
| 389 |
submit_btn.click(
|
| 390 |
fn=generate_html,
|
| 391 |
inputs=[model_dropdown, api_input, text_input, url_input, file_input],
|
| 392 |
outputs=[html_file_output, status_output, gr.State()]
|
| 393 |
).then(
|
| 394 |
-
fn=lambda file, status,
|
| 395 |
gr.update(visible=file is not None),
|
| 396 |
status,
|
| 397 |
-
gr.update(visible=file is not None, value=open(file, 'r', encoding='utf-8').read() if file else "")
|
| 398 |
),
|
| 399 |
inputs=[html_file_output, status_output, gr.State()],
|
| 400 |
outputs=[html_file_output, status_output, html_preview]
|
| 401 |
)
|
| 402 |
-
|
| 403 |
reset_btn.click(
|
| 404 |
fn=reset_form,
|
| 405 |
outputs=[model_dropdown, api_input, text_input, url_input, file_input, status_output, html_file_output, html_preview]
|
|
|
|
| 37 |
"default_api": True
|
| 38 |
},
|
| 39 |
"ChatGPT 5 (OpenAI)": {
|
| 40 |
+
"provider": "OpenAI",
|
| 41 |
"class": ChatOpenAI,
|
| 42 |
"model_name": "gpt-4o",
|
| 43 |
"default_api": False
|
| 44 |
},
|
| 45 |
"Claude Sonnet 4 (Anthropic)": {
|
| 46 |
"provider": "Anthropic",
|
| 47 |
+
"class": ChatAnthropic,
|
| 48 |
"model_name": "claude-3-5-sonnet-20241022",
|
| 49 |
"default_api": False
|
| 50 |
},
|
|
|
|
| 63 |
"""Extrait le texte d'un fichier uploadé"""
|
| 64 |
if file is None:
|
| 65 |
return ""
|
|
|
|
| 66 |
file_extension = os.path.splitext(file.name)[1].lower()
|
|
|
|
| 67 |
try:
|
| 68 |
if file_extension == '.pdf':
|
| 69 |
with open(file.name, 'rb') as f:
|
|
|
|
| 72 |
for page in reader.pages:
|
| 73 |
text += page.extract_text() + "\n"
|
| 74 |
return text
|
|
|
|
| 75 |
elif file_extension == '.docx':
|
| 76 |
doc = docx.Document(file.name)
|
| 77 |
text = ""
|
| 78 |
for paragraph in doc.paragraphs:
|
| 79 |
text += paragraph.text + "\n"
|
| 80 |
return text
|
|
|
|
| 81 |
elif file_extension == '.txt':
|
| 82 |
with open(file.name, 'r', encoding='utf-8') as f:
|
| 83 |
return f.read()
|
|
|
|
| 84 |
elif file_extension in ['.xlsx', '.xls']:
|
| 85 |
df = pd.read_excel(file.name)
|
| 86 |
return df.to_string()
|
|
|
|
| 87 |
elif file_extension == '.pptx':
|
| 88 |
prs = Presentation(file.name)
|
| 89 |
text = ""
|
|
|
|
| 92 |
if hasattr(shape, "text"):
|
| 93 |
text += shape.text + "\n"
|
| 94 |
return text
|
|
|
|
| 95 |
else:
|
| 96 |
return "Format de fichier non supporté"
|
|
|
|
| 97 |
except Exception as e:
|
| 98 |
return f"Erreur lors de la lecture du fichier: {str(e)}"
|
| 99 |
|
|
|
|
| 102 |
try:
|
| 103 |
response = requests.get(url, timeout=10)
|
| 104 |
response.raise_for_status()
|
|
|
|
|
|
|
| 105 |
content = response.text
|
|
|
|
| 106 |
content = re.sub(r'<[^>]+>', '', content)
|
| 107 |
content = re.sub(r'\s+', ' ', content).strip()
|
|
|
|
| 108 |
return content[:10000] # Limite à 10k caractères
|
|
|
|
| 109 |
except Exception as e:
|
| 110 |
return f"Erreur lors de la récupération de l'URL: {str(e)}"
|
| 111 |
|
|
|
|
| 123 |
def create_llm_instance(model_name, api_key):
|
| 124 |
"""Crée une instance du modèle LLM"""
|
| 125 |
model_config = MODELS[model_name]
|
|
|
|
| 126 |
if model_config["provider"] == "OpenAI":
|
| 127 |
return model_config["class"](
|
| 128 |
model=model_config["model_name"],
|
|
|
|
| 146 |
def generate_html(model_name, api_key, text_input, url_input, file_input):
|
| 147 |
"""Génère le fichier HTML éducatif"""
|
| 148 |
start_time = time.time()
|
|
|
|
|
|
|
| 149 |
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip():
|
| 150 |
return None, "❌ Erreur: Veuillez fournir une clé API pour ce modèle.", 0
|
| 151 |
+
|
| 152 |
document_content = get_document_content(text_input, url_input, file_input)
|
| 153 |
if not document_content:
|
| 154 |
return None, "❌ Erreur: Veuillez fournir un document (texte, URL ou fichier).", 0
|
| 155 |
+
|
| 156 |
try:
|
| 157 |
# Création de l'instance LLM
|
| 158 |
llm = create_llm_instance(model_name, api_key)
|
| 159 |
+
|
| 160 |
# Lecture du prompt template
|
| 161 |
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f:
|
| 162 |
prompt_template = f.read()
|
| 163 |
+
|
| 164 |
# Remplacement des variables
|
| 165 |
model_config = MODELS[model_name]
|
| 166 |
prompt = prompt_template.format(
|
|
|
|
| 168 |
provider_name=model_config["provider"],
|
| 169 |
document=document_content
|
| 170 |
)
|
| 171 |
+
|
| 172 |
# Génération du contenu
|
| 173 |
message = HumanMessage(content=prompt)
|
| 174 |
response = llm.invoke([message])
|
|
|
|
| 175 |
html_content = response.content
|
| 176 |
+
|
| 177 |
+
# Nettoyage des éventuelles balises de code des modèles
|
| 178 |
+
html_content = html_content.replace("```html", "")
|
| 179 |
+
html_content = html_content.replace("```", "")
|
| 180 |
+
|
| 181 |
# Calcul du temps de génération
|
| 182 |
generation_time = time.time() - start_time
|
| 183 |
+
|
| 184 |
# Sauvegarde du fichier HTML
|
| 185 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 186 |
filename = f"document_educatif_{timestamp}.html"
|
|
|
|
| 187 |
with open(filename, "w", encoding="utf-8") as f:
|
| 188 |
f.write(html_content)
|
| 189 |
+
|
| 190 |
success_message = f"✅ Fichier HTML généré avec succès en {generation_time:.2f} secondes!"
|
|
|
|
| 191 |
return filename, success_message, generation_time
|
| 192 |
+
|
| 193 |
except Exception as e:
|
| 194 |
error_message = f"❌ Erreur lors de la génération: {str(e)}"
|
| 195 |
return None, error_message, 0
|
|
|
|
| 222 |
info="🔑 Clé API requise pour ce modèle"
|
| 223 |
)
|
| 224 |
|
| 225 |
+
# Interface Gradio (Apple-like)
|
| 226 |
with gr.Blocks(
|
| 227 |
title="EduHTML Creator - Générateur de contenu éducatif HTML",
|
| 228 |
theme=gr.themes.Soft(),
|
| 229 |
css="""
|
| 230 |
+
/* =============== Apple-inspired Global Reset & Typography =============== */
|
| 231 |
+
:root {
|
| 232 |
+
--apple-black: #000000;
|
| 233 |
+
--apple-white: #ffffff;
|
| 234 |
+
--apple-grey-50: #f5f5f7;
|
| 235 |
+
--apple-grey-100: #efeff4; /* iOS group background */
|
| 236 |
+
--apple-grey-200: #e5e5ea;
|
| 237 |
+
--apple-grey-300: #d1d1d6;
|
| 238 |
+
--apple-grey-600: #8e8e93;
|
| 239 |
+
--apple-blue: #007aff;
|
| 240 |
+
--apple-blue-hover: #0056cc;
|
| 241 |
+
--apple-green: #34c759;
|
| 242 |
+
--apple-red: #ff3b30;
|
| 243 |
+
--shadow-soft: 0 8px 30px rgba(0,0,0,0.08);
|
| 244 |
+
--radius-lg: 14px;
|
| 245 |
+
--radius-md: 12px;
|
| 246 |
+
--radius-sm: 10px;
|
| 247 |
+
--transition: all 0.3s ease;
|
| 248 |
+
--text-color: #1d1d1f;
|
| 249 |
+
--link-underline-offset: 2px;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
* { box-sizing: border-box; }
|
| 253 |
+
html, body {
|
| 254 |
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
| 255 |
+
color: var(--text-color);
|
| 256 |
+
background: var(--apple-grey-50);
|
| 257 |
+
-webkit-font-smoothing: antialiased;
|
| 258 |
+
-moz-osx-font-smoothing: grayscale;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/* =============== Layout Containers =============== */
|
| 262 |
+
.main-container {
|
| 263 |
+
max-width: 1100px;
|
| 264 |
+
margin: 0 auto;
|
| 265 |
+
padding: 24px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* Fixed top nav (subtle, Apple-like) */
|
| 269 |
+
.top-nav {
|
| 270 |
+
position: sticky;
|
| 271 |
+
top: 0;
|
| 272 |
+
z-index: 1000;
|
| 273 |
+
backdrop-filter: saturate(180%) blur(12px);
|
| 274 |
+
background: rgba(250,250,252,0.72);
|
| 275 |
+
border-bottom: 1px solid rgba(0,0,0,0.06);
|
| 276 |
+
}
|
| 277 |
+
.top-nav-inner {
|
| 278 |
+
max-width: 1100px;
|
| 279 |
+
margin: 0 auto;
|
| 280 |
+
height: 56px;
|
| 281 |
+
display: flex;
|
| 282 |
+
align-items: center;
|
| 283 |
+
justify-content: space-between;
|
| 284 |
+
padding: 0 16px;
|
| 285 |
+
}
|
| 286 |
+
.brand {
|
| 287 |
+
display: flex;
|
| 288 |
+
align-items: center;
|
| 289 |
+
gap: 10px;
|
| 290 |
+
font-weight: 600;
|
| 291 |
+
letter-spacing: 0.2px;
|
| 292 |
+
}
|
| 293 |
+
.brand .dot {
|
| 294 |
+
width: 10px;
|
| 295 |
+
height: 10px;
|
| 296 |
+
border-radius: 50%;
|
| 297 |
+
background: var(--apple-blue);
|
| 298 |
+
box-shadow: 0 0 0 4px rgba(0,122,255,0.15);
|
| 299 |
+
}
|
| 300 |
+
.nav-actions .burger {
|
| 301 |
+
width: 36px; height: 36px;
|
| 302 |
+
border-radius: 9px;
|
| 303 |
+
border: 1px solid rgba(0,0,0,0.08);
|
| 304 |
+
display: grid; place-items: center;
|
| 305 |
+
background: var(--apple-white);
|
| 306 |
+
box-shadow: var(--shadow-soft);
|
| 307 |
+
cursor: pointer;
|
| 308 |
+
transition: var(--transition);
|
| 309 |
+
}
|
| 310 |
+
.nav-actions .burger:hover { transform: translateY(-1px); }
|
| 311 |
+
|
| 312 |
+
/* Inline burger menu panel (local help/actions) */
|
| 313 |
+
.inline-menu {
|
| 314 |
+
position: fixed;
|
| 315 |
+
top: 64px;
|
| 316 |
+
right: 16px;
|
| 317 |
+
width: 260px;
|
| 318 |
+
background: var(--apple-white);
|
| 319 |
+
border: 1px solid rgba(0,0,0,0.06);
|
| 320 |
+
border-radius: var(--radius-lg);
|
| 321 |
+
box-shadow: var(--shadow-soft);
|
| 322 |
+
padding: 12px;
|
| 323 |
+
opacity: 0;
|
| 324 |
+
pointer-events: none;
|
| 325 |
+
transform: translateY(-8px);
|
| 326 |
+
transition: var(--transition);
|
| 327 |
+
}
|
| 328 |
+
.inline-menu.open {
|
| 329 |
+
opacity: 1;
|
| 330 |
+
pointer-events: auto;
|
| 331 |
+
transform: translateY(0);
|
| 332 |
+
}
|
| 333 |
+
.inline-menu h4 {
|
| 334 |
+
margin: 6px 0 10px 0;
|
| 335 |
+
font-size: 14px;
|
| 336 |
+
font-weight: 700;
|
| 337 |
+
}
|
| 338 |
+
.inline-menu a {
|
| 339 |
+
display: block;
|
| 340 |
+
padding: 8px 10px;
|
| 341 |
+
border-radius: 10px;
|
| 342 |
+
text-decoration: underline;
|
| 343 |
+
text-underline-offset: var(--link-underline-offset);
|
| 344 |
+
color: var(--text-color);
|
| 345 |
+
}
|
| 346 |
+
.inline-menu a:hover { background: var(--apple-grey-100); }
|
| 347 |
+
|
| 348 |
+
/* =============== Header (full-width black) =============== */
|
| 349 |
+
.header {
|
| 350 |
+
margin: 18px 0 28px 0;
|
| 351 |
+
border-radius: var(--radius-lg);
|
| 352 |
+
overflow: hidden;
|
| 353 |
+
background: var(--apple-black);
|
| 354 |
+
color: var(--apple-white);
|
| 355 |
+
box-shadow: var(--shadow-soft);
|
| 356 |
+
}
|
| 357 |
+
.header-inner {
|
| 358 |
+
padding: 48px 32px;
|
| 359 |
+
text-align: center;
|
| 360 |
+
}
|
| 361 |
+
.header h1 {
|
| 362 |
+
margin: 0;
|
| 363 |
+
font-weight: 700;
|
| 364 |
+
letter-spacing: -0.02em;
|
| 365 |
+
font-size: clamp(28px, 3.5vw, 40px);
|
| 366 |
+
}
|
| 367 |
+
.header p {
|
| 368 |
+
opacity: 0.92;
|
| 369 |
+
font-size: 17px;
|
| 370 |
+
line-height: 1.5;
|
| 371 |
+
margin: 12px auto 0;
|
| 372 |
+
max-width: 760px;
|
| 373 |
+
}
|
| 374 |
+
.header a {
|
| 375 |
+
color: #fff;
|
| 376 |
+
text-decoration: underline;
|
| 377 |
+
text-underline-offset: var(--link-underline-offset);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
/* =============== Section Cards (alternating backgrounds) =============== */
|
| 381 |
+
.section {
|
| 382 |
+
background: var(--apple-white);
|
| 383 |
+
border: 1px solid rgba(0,0,0,0.06);
|
| 384 |
+
border-radius: var(--radius-lg);
|
| 385 |
+
padding: 20px;
|
| 386 |
+
box-shadow: var(--shadow-soft);
|
| 387 |
+
}
|
| 388 |
+
.section + .section { margin-top: 16px; }
|
| 389 |
+
|
| 390 |
+
/* Alternate section – subtle grey Apple background */
|
| 391 |
+
.section.alt {
|
| 392 |
+
background: var(--apple-grey-100);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* Labels/titles inside sections */
|
| 396 |
+
.section h3, .section h2, .section h4 {
|
| 397 |
+
margin-top: 0;
|
| 398 |
+
letter-spacing: -0.01em;
|
| 399 |
+
}
|
| 400 |
+
.section h3 {
|
| 401 |
+
font-size: 18px;
|
| 402 |
+
font-weight: 700;
|
| 403 |
+
margin-bottom: 12px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/* =============== Inputs & Controls =============== */
|
| 407 |
+
input[type="text"], input[type="password"], textarea {
|
| 408 |
+
border-radius: var(--radius-md) !important;
|
| 409 |
+
border: 1px solid rgba(0,0,0,0.08) !important;
|
| 410 |
+
background: var(--apple-white) !important;
|
| 411 |
+
transition: var(--transition) !important;
|
| 412 |
+
box-shadow: 0 1px 0 rgba(0,0,0,0.02) inset;
|
| 413 |
+
}
|
| 414 |
+
input:focus, textarea:focus {
|
| 415 |
+
outline: none !important;
|
| 416 |
+
border-color: var(--apple-blue) !important;
|
| 417 |
+
box-shadow: 0 0 0 3px rgba(0,122,255,0.15) !important;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
/* Buttons */
|
| 421 |
+
.apple-button, .reset-button, .ghost-button {
|
| 422 |
+
display: inline-flex;
|
| 423 |
+
align-items: center;
|
| 424 |
+
gap: 8px;
|
| 425 |
+
height: 44px;
|
| 426 |
+
padding: 0 18px;
|
| 427 |
+
border-radius: 12px;
|
| 428 |
+
border: 1px solid transparent;
|
| 429 |
+
cursor: pointer;
|
| 430 |
+
font-weight: 600;
|
| 431 |
+
letter-spacing: 0.2px;
|
| 432 |
+
transition: var(--transition);
|
| 433 |
+
user-select: none;
|
| 434 |
+
}
|
| 435 |
+
.apple-button {
|
| 436 |
+
background: var(--apple-blue);
|
| 437 |
+
color: #fff;
|
| 438 |
+
box-shadow: 0 6px 18px rgba(0,122,255,0.25);
|
| 439 |
+
}
|
| 440 |
+
.apple-button:hover { background: var(--apple-blue-hover); transform: translateY(-1px); }
|
| 441 |
+
.apple-button:active { transform: translateY(0); }
|
| 442 |
+
|
| 443 |
+
.reset-button {
|
| 444 |
+
background: var(--apple-red);
|
| 445 |
+
color: #fff;
|
| 446 |
+
box-shadow: 0 6px 18px rgba(255,59,48,0.22);
|
| 447 |
+
}
|
| 448 |
+
.reset-button:hover { filter: brightness(0.96); transform: translateY(-1px); }
|
| 449 |
+
.reset-button:active { transform: translateY(0); }
|
| 450 |
+
|
| 451 |
+
/* Secondary ghost button style if needed */
|
| 452 |
+
.ghost-button {
|
| 453 |
+
background: var(--apple-white);
|
| 454 |
+
color: var(--text-color);
|
| 455 |
+
border: 1px solid rgba(0,0,0,0.08);
|
| 456 |
+
}
|
| 457 |
+
.ghost-button:hover { background: #fafafa; }
|
| 458 |
+
|
| 459 |
+
/* Status messages */
|
| 460 |
+
.status-success { color: var(--apple-green); font-weight: 600; }
|
| 461 |
+
.status-error { color: var(--apple-red); font-weight: 600; }
|
| 462 |
+
|
| 463 |
+
/* =============== Download & Preview =============== */
|
| 464 |
+
.preview-card {
|
| 465 |
+
background: var(--apple-white);
|
| 466 |
+
border: 1px solid rgba(0,0,0,0.06);
|
| 467 |
+
border-radius: var(--radius-lg);
|
| 468 |
+
box-shadow: var(--shadow-soft);
|
| 469 |
+
overflow: hidden;
|
| 470 |
+
}
|
| 471 |
+
.preview-header {
|
| 472 |
+
background: linear-gradient(180deg, #fafafa, #f2f2f7);
|
| 473 |
+
border-bottom: 1px solid rgba(0,0,0,0.06);
|
| 474 |
+
padding: 12px 16px;
|
| 475 |
+
display: flex; align-items: center; gap: 8px;
|
| 476 |
+
}
|
| 477 |
+
.preview-dot {
|
| 478 |
+
width: 10px; height: 10px; border-radius: 50%;
|
| 479 |
+
background: #ff5f57; box-shadow: 16px 0 0 #ffbd2e, 32px 0 0 #28c840;
|
| 480 |
+
}
|
| 481 |
+
.preview-body {
|
| 482 |
+
padding: 16px;
|
| 483 |
+
max-height: 520px;
|
| 484 |
+
overflow: auto;
|
| 485 |
+
background: var(--apple-grey-50);
|
| 486 |
+
}
|
| 487 |
+
.preview-body pre, .preview-body code {
|
| 488 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 489 |
+
font-size: 12px;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
/* =============== Footer (full-width black) =============== */
|
| 493 |
+
.footer {
|
| 494 |
+
margin-top: 22px;
|
| 495 |
+
border-radius: var(--radius-lg);
|
| 496 |
+
overflow: hidden;
|
| 497 |
+
background: var(--apple-black);
|
| 498 |
+
color: var(--apple-white);
|
| 499 |
+
box-shadow: var(--shadow-soft);
|
| 500 |
+
}
|
| 501 |
+
.footer-inner {
|
| 502 |
+
padding: 18px 20px;
|
| 503 |
+
font-size: 14px;
|
| 504 |
+
line-height: 1.5;
|
| 505 |
+
}
|
| 506 |
+
.footer a {
|
| 507 |
+
color: #fff; text-decoration: underline; text-underline-offset: var(--link-underline-offset);
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* Links globally */
|
| 511 |
+
a { text-decoration: underline; text-underline-offset: var(--link-underline-offset); }
|
| 512 |
+
|
| 513 |
+
/* Hover animations */
|
| 514 |
+
.section, .preview-card, .header, .footer {
|
| 515 |
+
transition: var(--transition);
|
| 516 |
+
}
|
| 517 |
+
.section:hover, .preview-card:hover {
|
| 518 |
+
transform: translateY(-2px);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* Responsive */
|
| 522 |
+
@media (max-width: 768px) {
|
| 523 |
+
.top-nav-inner { padding: 0 12px; }
|
| 524 |
+
.header-inner { padding: 36px 20px; }
|
| 525 |
+
.preview-body { max-height: 420px; }
|
| 526 |
+
}
|
| 527 |
"""
|
| 528 |
) as app:
|
| 529 |
+
# Top navigation (sticky) + burger menu
|
| 530 |
+
gr.HTML("""
|
| 531 |
+
<div class='top-nav' role='navigation' aria-label='Navigation principale'>
|
| 532 |
+
<div class='top-nav-inner'>
|
| 533 |
+
<div class='brand'>
|
| 534 |
+
<span class='dot' aria-hidden='true'></span>
|
| 535 |
+
<span>EduHTML Creator</span>
|
| 536 |
+
</div>
|
| 537 |
+
<div class='nav-actions'>
|
| 538 |
+
<button class='burger' aria-label='Menu' id='inlineMenuBtn'>
|
| 539 |
+
<svg width='18' height='18' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'>
|
| 540 |
+
<path d='M3 6h18M3 12h18M3 18h18' stroke='#1d1d1f' stroke-width='2' stroke-linecap='round'/>
|
| 541 |
+
</svg>
|
| 542 |
+
</button>
|
| 543 |
+
</div>
|
| 544 |
+
</div>
|
| 545 |
+
<div class='inline-menu' id='inlineMenu' aria-hidden='true'>
|
| 546 |
+
<h4>Actions rapides</h4>
|
| 547 |
+
<a href='#' id='scrollTopLink'>Revenir en haut</a>
|
| 548 |
+
<a href='https://www.apple.com' target='_blank' rel='noopener'>Inspiration Apple</a>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
""")
|
| 552 |
+
|
| 553 |
+
# Header hero (black, full-width look within container)
|
| 554 |
gr.HTML("""
|
| 555 |
+
<div class="header" role="banner">
|
| 556 |
+
<div class="header-inner">
|
| 557 |
<h1>🎓 EduHTML Creator</h1>
|
| 558 |
+
<p>
|
| 559 |
+
Transformez n'importe quel document en contenu éducatif HTML interactif, avec un design premium inspiré d'Apple.
|
| 560 |
+
Fidélité au document, structuration claire, interactivité, et mise en valeur des informations clés.
|
|
|
|
|
|
|
|
|
|
| 561 |
</p>
|
| 562 |
+
</div>
|
| 563 |
</div>
|
| 564 |
""")
|
| 565 |
+
|
| 566 |
+
with gr.Row(elem_classes=["main-container"]):
|
| 567 |
with gr.Column(scale=1):
|
| 568 |
+
gr.HTML("<div class='section'>")
|
|
|
|
|
|
|
| 569 |
model_dropdown = gr.Dropdown(
|
| 570 |
choices=list(MODELS.keys()),
|
| 571 |
value="Gemini 2.5 Flash (Google AI)",
|
| 572 |
+
label="Modèle LLM",
|
| 573 |
+
info="Sélectionnez le modèle à utiliser pour la génération"
|
| 574 |
)
|
| 575 |
+
|
|
|
|
| 576 |
api_input = gr.Textbox(
|
| 577 |
label="Clé API (optionnelle)",
|
| 578 |
+
placeholder="API gratuite (Gemini Flash) disponible. Vous pouvez entrer votre propre clé.",
|
| 579 |
+
info="Pour OpenAI/Anthropic, une clé est obligatoire.",
|
| 580 |
type="password"
|
| 581 |
)
|
|
|
|
| 582 |
gr.HTML("</div>")
|
| 583 |
+
|
| 584 |
+
gr.HTML("<div class='section alt'>")
|
| 585 |
+
gr.HTML("<h3>Source du document</h3>")
|
|
|
|
|
|
|
| 586 |
text_input = gr.Textbox(
|
| 587 |
label="Texte copié/collé",
|
| 588 |
placeholder="Collez votre texte ici...",
|
| 589 |
+
lines=6
|
| 590 |
)
|
|
|
|
| 591 |
url_input = gr.Textbox(
|
| 592 |
label="Lien Web",
|
| 593 |
placeholder="https://exemple.com/article"
|
| 594 |
)
|
|
|
|
| 595 |
file_input = gr.File(
|
| 596 |
label="Fichier",
|
| 597 |
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"]
|
| 598 |
)
|
|
|
|
| 599 |
gr.HTML("</div>")
|
| 600 |
+
|
|
|
|
| 601 |
with gr.Row():
|
| 602 |
+
submit_btn = gr.Button("Générer le HTML", variant="primary", elem_classes=["apple-button"])
|
| 603 |
+
reset_btn = gr.Button("Reset", elem_classes=["reset-button"])
|
| 604 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
with gr.Column(scale=1):
|
|
|
|
| 606 |
status_output = gr.HTML(label="Statut")
|
| 607 |
+
gr.HTML("<div class='section preview-card'>")
|
| 608 |
+
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>Prévisualisation</div></div>")
|
| 609 |
+
html_preview = gr.HTML(label="Prévisualisation", visible=False, elem_id="html-preview", elem_classes=["preview-body"])
|
| 610 |
+
html_file_output = gr.File(label="Fichier HTML téléchargeable", visible=False)
|
| 611 |
+
gr.HTML("</div>")
|
| 612 |
+
|
| 613 |
+
# Footer (black)
|
| 614 |
+
gr.HTML("""
|
| 615 |
+
<div class="footer" role="contentinfo">
|
| 616 |
+
<div class="footer-inner">
|
| 617 |
+
<span>Design inspiré d'Apple • Contrastes élevés • Interactions fluides</span>
|
| 618 |
+
</div>
|
| 619 |
+
</div>
|
| 620 |
+
""")
|
| 621 |
+
|
| 622 |
+
# Light JS: smooth scroll to top, inline burger, focus handling
|
| 623 |
+
gr.HTML("""
|
| 624 |
+
<script>
|
| 625 |
+
(function() {
|
| 626 |
+
const menuBtn = document.getElementById('inlineMenuBtn');
|
| 627 |
+
const menu = document.getElementById('inlineMenu');
|
| 628 |
+
const topLink = document.getElementById('scrollTopLink');
|
| 629 |
+
|
| 630 |
+
function closeMenu() {
|
| 631 |
+
if (!menu) return;
|
| 632 |
+
menu.classList.remove('open');
|
| 633 |
+
menu.setAttribute('aria-hidden', 'true');
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
if (menuBtn && menu) {
|
| 637 |
+
menuBtn.addEventListener('click', function(e) {
|
| 638 |
+
e.preventDefault();
|
| 639 |
+
const isOpen = menu.classList.contains('open');
|
| 640 |
+
if (isOpen) {
|
| 641 |
+
closeMenu();
|
| 642 |
+
} else {
|
| 643 |
+
menu.classList.add('open');
|
| 644 |
+
menu.setAttribute('aria-hidden', 'false');
|
| 645 |
+
}
|
| 646 |
+
});
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
if (topLink) {
|
| 650 |
+
topLink.addEventListener('click', function(e) {
|
| 651 |
+
e.preventDefault();
|
| 652 |
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 653 |
+
closeMenu();
|
| 654 |
+
});
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
// Close when clicking outside
|
| 658 |
+
document.addEventListener('click', function(e) {
|
| 659 |
+
if (!menu || !menuBtn) return;
|
| 660 |
+
if (!menu.contains(e.target) && !menuBtn.contains(e.target)) {
|
| 661 |
+
closeMenu();
|
| 662 |
+
}
|
| 663 |
+
});
|
| 664 |
+
|
| 665 |
+
// Accessibility: close on Escape
|
| 666 |
+
document.addEventListener('keydown', function(e) {
|
| 667 |
+
if (e.key === 'Escape') closeMenu();
|
| 668 |
+
});
|
| 669 |
+
})();
|
| 670 |
+
</script>
|
| 671 |
+
""")
|
| 672 |
+
|
| 673 |
# Événements
|
| 674 |
model_dropdown.change(
|
| 675 |
fn=update_api_info,
|
| 676 |
inputs=[model_dropdown],
|
| 677 |
outputs=[api_input]
|
| 678 |
)
|
| 679 |
+
|
| 680 |
submit_btn.click(
|
| 681 |
fn=generate_html,
|
| 682 |
inputs=[model_dropdown, api_input, text_input, url_input, file_input],
|
| 683 |
outputs=[html_file_output, status_output, gr.State()]
|
| 684 |
).then(
|
| 685 |
+
fn=lambda file, status, _: (
|
| 686 |
gr.update(visible=file is not None),
|
| 687 |
status,
|
| 688 |
+
gr.update(visible=file is not None, value=(open(file, 'r', encoding='utf-8').read() if file else ""))
|
| 689 |
),
|
| 690 |
inputs=[html_file_output, status_output, gr.State()],
|
| 691 |
outputs=[html_file_output, status_output, html_preview]
|
| 692 |
)
|
| 693 |
+
|
| 694 |
reset_btn.click(
|
| 695 |
fn=reset_form,
|
| 696 |
outputs=[model_dropdown, api_input, text_input, url_input, file_input, status_output, html_file_output, html_preview]
|