pierreguillou commited on
Commit
8c7d566
·
verified ·
1 Parent(s): 04bd018

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +430 -139
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
- .main-container {
246
- max-width: 1200px;
247
- margin: 0 auto;
248
- padding: 20px;
249
- }
250
- .header {
251
- text-align: center;
252
- margin-bottom: 30px;
253
- padding: 30px;
254
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
255
- border-radius: 15px;
256
- color: white;
257
- }
258
- .form-section {
259
- background: white;
260
- padding: 25px;
261
- border-radius: 15px;
262
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
263
- margin-bottom: 20px;
264
- }
265
- .apple-button {
266
- background: #007AFF;
267
- color: white;
268
- border: none;
269
- border-radius: 8px;
270
- padding: 12px 24px;
271
- font-weight: 500;
272
- transition: all 0.3s ease;
273
- }
274
- .apple-button:hover {
275
- background: #0056CC;
276
- transform: translateY(-1px);
277
- }
278
- .reset-button {
279
- background: #FF3B30;
280
- color: white;
281
- border: none;
282
- border-radius: 8px;
283
- padding: 12px 24px;
284
- font-weight: 500;
285
- }
286
- .status-success {
287
- color: #34C759;
288
- font-weight: 500;
289
- }
290
- .status-error {
291
- color: #FF3B30;
292
- font-weight: 500;
293
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  """
295
  ) as app:
296
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  gr.HTML("""
298
- <div class="header">
 
299
  <h1>🎓 EduHTML Creator</h1>
300
- <p style="font-size: 18px; margin: 10px 0;">Transformez n'importe quel document en contenu éducatif HTML interactif</p>
301
- <p style="font-size: 14px; opacity: 0.9;">
302
- Cette application utilise l'intelligence artificielle pour créer des pages HTML éducatives élégantes et interactives
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='form-section'>")
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="🤖 Modèle LLM",
319
- info="Choisissez le modèle d'IA à utiliser"
320
  )
321
-
322
- # Champ API
323
  api_input = gr.Textbox(
324
  label="Clé API (optionnelle)",
325
- placeholder="API gratuite disponible jusqu'à épuisement, ou utilisez votre propre clé",
326
- info="💡 Une API gratuite est déjà configurée pour ce modèle. Vous pouvez utiliser votre propre clé si vous le souhaitez.",
327
  type="password"
328
  )
329
-
330
  gr.HTML("</div>")
331
-
332
- gr.HTML("<div class='form-section'>")
333
- gr.HTML("<h3>📄 Source du document</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=5
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
- "🚀 Générer le HTML",
358
- variant="primary",
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
- # Fichier téléchargeable
371
- html_file_output = gr.File(
372
- label="📥 Fichier HTML téléchargeable",
373
- visible=False
374
- )
375
-
376
- # Prévisualisation
377
- html_preview = gr.HTML(
378
- label="👀 Prévisualisation",
379
- visible=False
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, time_taken: (
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]