|
|
import gradio as gr |
|
|
import os |
|
|
import time |
|
|
import requests |
|
|
from datetime import datetime |
|
|
from langchain_openai import ChatOpenAI |
|
|
from langchain_anthropic import ChatAnthropic |
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
from langchain_core.messages import HumanMessage |
|
|
from langchain_core.caches import BaseCache |
|
|
from langchain_core.callbacks import Callbacks |
|
|
ChatGoogleGenerativeAI.model_rebuild() |
|
|
import PyPDF2 |
|
|
import docx |
|
|
import pandas as pd |
|
|
from pptx import Presentation |
|
|
import io |
|
|
import tempfile |
|
|
from urllib.parse import urlparse |
|
|
import re |
|
|
|
|
|
|
|
|
try: |
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
from langchain_core.caches import BaseCache |
|
|
ChatGoogleGenerativeAI.model_rebuild() |
|
|
except Exception as e: |
|
|
print(f"Avertissement lors du rebuild: {e}") |
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
|
|
|
|
|
|
MODELS = { |
|
|
"Gemini 2.5 Flash (Google AI)": { |
|
|
"provider": "Google AI", |
|
|
"class": ChatGoogleGenerativeAI, |
|
|
"model_name": "gemini-2.0-flash-exp", |
|
|
"default_api": True |
|
|
}, |
|
|
"ChatGPT 5 (OpenAI)": { |
|
|
"provider": "OpenAI", |
|
|
"class": ChatOpenAI, |
|
|
"model_name": "gpt-4o", |
|
|
"default_api": False |
|
|
}, |
|
|
"Claude Sonnet 4 (Anthropic)": { |
|
|
"provider": "Anthropic", |
|
|
"class": ChatAnthropic, |
|
|
"model_name": "claude-3-5-sonnet-20241022", |
|
|
"default_api": False |
|
|
}, |
|
|
"Gemini 2.5 Pro (Google AI)": { |
|
|
"provider": "Google AI", |
|
|
"class": ChatGoogleGenerativeAI, |
|
|
"model_name": "gemini-2.0-flash-exp", |
|
|
"default_api": False |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
DEFAULT_GEMINI_API = os.getenv("FLASH_GOOGLE_API_KEY") |
|
|
|
|
|
def extract_text_from_file(file): |
|
|
"""Extrait le texte d'un fichier uploadé""" |
|
|
if file is None: |
|
|
return "" |
|
|
file_extension = os.path.splitext(file.name)[1].lower() |
|
|
try: |
|
|
if file_extension == '.pdf': |
|
|
with open(file.name, 'rb') as f: |
|
|
reader = PyPDF2.PdfReader(f) |
|
|
text = "" |
|
|
for page in reader.pages: |
|
|
text += page.extract_text() + "\n" |
|
|
return text |
|
|
elif file_extension == '.docx': |
|
|
doc = docx.Document(file.name) |
|
|
text = "" |
|
|
for paragraph in doc.paragraphs: |
|
|
text += paragraph.text + "\n" |
|
|
return text |
|
|
elif file_extension == '.txt': |
|
|
with open(file.name, 'r', encoding='utf-8') as f: |
|
|
return f.read() |
|
|
elif file_extension in ['.xlsx', '.xls']: |
|
|
df = pd.read_excel(file.name) |
|
|
return df.to_string() |
|
|
elif file_extension == '.pptx': |
|
|
prs = Presentation(file.name) |
|
|
text = "" |
|
|
for slide in prs.slides: |
|
|
for shape in slide.shapes: |
|
|
if hasattr(shape, "text"): |
|
|
text += shape.text + "\n" |
|
|
return text |
|
|
else: |
|
|
return "Format de fichier non supporté" |
|
|
except Exception as e: |
|
|
return f"Erreur lors de la lecture du fichier: {str(e)}" |
|
|
|
|
|
def extract_text_from_url(url): |
|
|
"""Extrait le texte d'une URL""" |
|
|
try: |
|
|
response = requests.get(url, timeout=10) |
|
|
response.raise_for_status() |
|
|
content = response.text |
|
|
content = re.sub(r'<[^>]+>', '', content) |
|
|
content = re.sub(r'\s+', ' ', content).strip() |
|
|
return content[:10000] |
|
|
except Exception as e: |
|
|
return f"Erreur lors de la récupération de l'URL: {str(e)}" |
|
|
|
|
|
def get_document_content(text_input, url_input, file_input): |
|
|
"""Récupère le contenu du document selon la source""" |
|
|
if text_input.strip(): |
|
|
return text_input.strip() |
|
|
elif url_input.strip(): |
|
|
return extract_text_from_url(url_input.strip()) |
|
|
elif file_input is not None: |
|
|
return extract_text_from_file(file_input) |
|
|
else: |
|
|
return "" |
|
|
|
|
|
def create_llm_instance(model_name, api_key): |
|
|
"""Crée une instance du modèle LLM""" |
|
|
model_config = MODELS[model_name] |
|
|
if model_config["provider"] == "OpenAI": |
|
|
return model_config["class"]( |
|
|
model=model_config["model_name"], |
|
|
api_key=api_key, |
|
|
temperature=0.7 |
|
|
) |
|
|
elif model_config["provider"] == "Anthropic": |
|
|
return model_config["class"]( |
|
|
model=model_config["model_name"], |
|
|
api_key=api_key, |
|
|
temperature=0.7 |
|
|
) |
|
|
elif model_config["provider"] == "Google AI": |
|
|
api_to_use = api_key if api_key else DEFAULT_GEMINI_API |
|
|
return model_config["class"]( |
|
|
model=model_config["model_name"], |
|
|
google_api_key=api_to_use, |
|
|
temperature=0.7 |
|
|
) |
|
|
|
|
|
def generate_html(model_name, api_key, text_input, url_input, file_input): |
|
|
"""Génère le fichier HTML éducatif""" |
|
|
start_time = time.time() |
|
|
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip(): |
|
|
return None, "❌ Erreur: Veuillez fournir une clé API pour ce modèle.", 0 |
|
|
|
|
|
document_content = get_document_content(text_input, url_input, file_input) |
|
|
if not document_content: |
|
|
return None, "❌ Erreur: Veuillez fournir un document (texte, URL ou fichier).", 0 |
|
|
|
|
|
try: |
|
|
|
|
|
llm = create_llm_instance(model_name, api_key) |
|
|
|
|
|
|
|
|
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f: |
|
|
prompt_template = f.read() |
|
|
|
|
|
|
|
|
model_config = MODELS[model_name] |
|
|
prompt = prompt_template.format( |
|
|
model_name=model_config["model_name"], |
|
|
provider_name=model_config["provider"], |
|
|
document=document_content |
|
|
) |
|
|
|
|
|
|
|
|
message = HumanMessage(content=prompt) |
|
|
response = llm.invoke([message]) |
|
|
html_content = response.content |
|
|
|
|
|
|
|
|
html_content = html_content.replace("```html", "") |
|
|
html_content = html_content.replace("```", "") |
|
|
|
|
|
|
|
|
generation_time = time.time() - start_time |
|
|
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
|
filename = f"document_educatif_{timestamp}.html" |
|
|
with open(filename, "w", encoding="utf-8") as f: |
|
|
f.write(html_content) |
|
|
|
|
|
success_message = f"✅ Fichier HTML généré avec succès en {generation_time:.2f} secondes!" |
|
|
return filename, success_message, generation_time |
|
|
|
|
|
except Exception as e: |
|
|
error_message = f"❌ Erreur lors de la génération: {str(e)}" |
|
|
return None, error_message, 0 |
|
|
|
|
|
def reset_form(): |
|
|
"""Remet à zéro le formulaire""" |
|
|
return ( |
|
|
"Gemini 2.5 Flash (Google AI)", |
|
|
"", |
|
|
"", |
|
|
"", |
|
|
None, |
|
|
"", |
|
|
None, |
|
|
"" |
|
|
) |
|
|
|
|
|
def update_api_info(model_name): |
|
|
"""Met à jour les informations sur l'API selon le modèle sélectionné""" |
|
|
if model_name == "Gemini 2.5 Flash (Google AI)": |
|
|
return gr.update( |
|
|
label="Clé API (optionnelle)", |
|
|
placeholder="API gratuite disponible jusqu'à épuisement, ou utilisez votre propre clé", |
|
|
info="💡 Une API gratuite est déjà configurée pour ce modèle. Vous pouvez utiliser votre propre clé si vous le souhaitez." |
|
|
) |
|
|
else: |
|
|
return gr.update( |
|
|
label="Clé API (obligatoire)", |
|
|
placeholder="Entrez votre clé API", |
|
|
info="🔑 Clé API requise pour ce modèle" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="EduHTML Creator - Générateur de contenu éducatif HTML", |
|
|
theme=gr.themes.Soft(), |
|
|
css=""" |
|
|
/* =============== Apple-inspired Global Reset & Typography =============== */ |
|
|
:root { |
|
|
--apple-black: #000000; |
|
|
--apple-white: #ffffff; |
|
|
--apple-grey-50: #f5f5f7; |
|
|
--apple-grey-100: #efeff4; /* iOS group background */ |
|
|
--apple-grey-200: #e5e5ea; |
|
|
--apple-grey-300: #d1d1d6; |
|
|
--apple-grey-600: #8e8e93; |
|
|
--apple-blue: #007aff; |
|
|
--apple-blue-hover: #0056cc; |
|
|
--apple-green: #34c759; |
|
|
--apple-red: #ff3b30; |
|
|
--shadow-soft: 0 8px 30px rgba(0,0,0,0.08); |
|
|
--radius-lg: 14px; |
|
|
--radius-md: 12px; |
|
|
--radius-sm: 10px; |
|
|
--transition: all 0.3s ease; |
|
|
--text-color: #1d1d1f; |
|
|
--link-underline-offset: 2px; |
|
|
} |
|
|
|
|
|
* { box-sizing: border-box; } |
|
|
html, body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif; |
|
|
color: var(--text-color); |
|
|
background: var(--apple-grey-50); |
|
|
-webkit-font-smoothing: antialiased; |
|
|
-moz-osx-font-smoothing: grayscale; |
|
|
} |
|
|
|
|
|
/* =============== Layout Containers =============== */ |
|
|
.main-container { |
|
|
max-width: 1100px; |
|
|
margin: 0 auto; |
|
|
padding: 24px; |
|
|
} |
|
|
|
|
|
/* Fixed top nav (subtle, Apple-like) */ |
|
|
.top-nav { |
|
|
position: sticky; |
|
|
top: 0; |
|
|
z-index: 1000; |
|
|
backdrop-filter: saturate(180%) blur(12px); |
|
|
background: rgba(250,250,252,0.72); |
|
|
border-bottom: 1px solid rgba(0,0,0,0.06); |
|
|
} |
|
|
.top-nav-inner { |
|
|
max-width: 1100px; |
|
|
margin: 0 auto; |
|
|
height: 56px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
padding: 0 16px; |
|
|
} |
|
|
.brand { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
font-weight: 600; |
|
|
letter-spacing: 0.2px; |
|
|
} |
|
|
.brand .dot { |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
border-radius: 50%; |
|
|
background: var(--apple-blue); |
|
|
box-shadow: 0 0 0 4px rgba(0,122,255,0.15); |
|
|
} |
|
|
.nav-actions .burger { |
|
|
width: 36px; height: 36px; |
|
|
border-radius: 9px; |
|
|
border: 1px solid rgba(0,0,0,0.08); |
|
|
display: grid; place-items: center; |
|
|
background: var(--apple-white); |
|
|
box-shadow: var(--shadow-soft); |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
} |
|
|
.nav-actions .burger:hover { transform: translateY(-1px); } |
|
|
|
|
|
/* Inline burger menu panel (local help/actions) */ |
|
|
.inline-menu { |
|
|
position: fixed; |
|
|
top: 64px; |
|
|
right: 16px; |
|
|
width: 260px; |
|
|
background: var(--apple-white); |
|
|
border: 1px solid rgba(0,0,0,0.06); |
|
|
border-radius: var(--radius-lg); |
|
|
box-shadow: var(--shadow-soft); |
|
|
padding: 12px; |
|
|
opacity: 0; |
|
|
pointer-events: none; |
|
|
transform: translateY(-8px); |
|
|
transition: var(--transition); |
|
|
} |
|
|
.inline-menu.open { |
|
|
opacity: 1; |
|
|
pointer-events: auto; |
|
|
transform: translateY(0); |
|
|
} |
|
|
.inline-menu h4 { |
|
|
margin: 6px 0 10px 0; |
|
|
font-size: 14px; |
|
|
font-weight: 700; |
|
|
} |
|
|
.inline-menu a { |
|
|
display: block; |
|
|
padding: 8px 10px; |
|
|
border-radius: 10px; |
|
|
text-decoration: underline; |
|
|
text-underline-offset: var(--link-underline-offset); |
|
|
color: var(--text-color); |
|
|
} |
|
|
.inline-menu a:hover { background: var(--apple-grey-100); } |
|
|
|
|
|
/* =============== Header (full-width black) =============== */ |
|
|
.header { |
|
|
margin: 18px 0 28px 0; |
|
|
border-radius: var(--radius-lg); |
|
|
overflow: hidden; |
|
|
background: var(--apple-black); |
|
|
color: #ffffff !important; /* Force texte blanc */ |
|
|
box-shadow: var(--shadow-soft); |
|
|
} |
|
|
.header, .header * { |
|
|
color: #ffffff !important; /* Tout le contenu en blanc */ |
|
|
} |
|
|
.header a, .header a:visited, .header a:active { |
|
|
color: #ffffff !important; /* Liens blancs */ |
|
|
text-decoration: underline; |
|
|
text-underline-offset: var(--link-underline-offset); |
|
|
} |
|
|
.header-inner { |
|
|
padding: 48px 32px; |
|
|
text-align: center; |
|
|
} |
|
|
.header h1 { |
|
|
margin: 0; |
|
|
font-weight: 700; |
|
|
letter-spacing: -0.02em; |
|
|
font-size: clamp(28px, 3.5vw, 40px); |
|
|
} |
|
|
.header p { |
|
|
opacity: 0.92; |
|
|
font-size: 17px; |
|
|
line-height: 1.5; |
|
|
margin: 12px auto 0; |
|
|
max-width: 760px; |
|
|
} |
|
|
|
|
|
/* =============== Section Cards (alternating backgrounds) =============== */ |
|
|
.section { |
|
|
background: var(--apple-white); |
|
|
border: 1px solid rgba(0,0,0,0.06); |
|
|
border-radius: var(--radius-lg); |
|
|
padding: 20px; |
|
|
box-shadow: var(--shadow-soft); |
|
|
} |
|
|
.section + .section { margin-top: 16px; } |
|
|
|
|
|
/* Alternate section – subtle grey Apple background */ |
|
|
.section.alt { |
|
|
background: var(--apple-grey-100); |
|
|
} |
|
|
|
|
|
/* Labels/titles inside sections */ |
|
|
.section h3, .section h2, .section h4 { |
|
|
margin-top: 0; |
|
|
letter-spacing: -0.01em; |
|
|
} |
|
|
.section h3 { |
|
|
font-size: 18px; |
|
|
font-weight: 700; |
|
|
margin-bottom: 12px; |
|
|
} |
|
|
|
|
|
/* =============== Inputs & Controls =============== */ |
|
|
input[type="text"], input[type="password"], textarea { |
|
|
border-radius: var(--radius-md) !important; |
|
|
border: 1px solid rgba(0,0,0,0.08) !important; |
|
|
background: var(--apple-white) !important; |
|
|
transition: var(--transition) !important; |
|
|
box-shadow: 0 1px 0 rgba(0,0,0,0.02) inset; |
|
|
} |
|
|
input:focus, textarea:focus { |
|
|
outline: none !important; |
|
|
border-color: var(--apple-blue) !important; |
|
|
box-shadow: 0 0 0 3px rgba(0,122,255,0.15) !important; |
|
|
} |
|
|
|
|
|
/* Buttons */ |
|
|
.apple-button, .reset-button, .ghost-button { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
height: 44px; |
|
|
padding: 0 18px; |
|
|
border-radius: 12px; |
|
|
border: 1px solid transparent; |
|
|
cursor: pointer; |
|
|
font-weight: 600; |
|
|
letter-spacing: 0.2px; |
|
|
transition: var(--transition); |
|
|
user-select: none; |
|
|
} |
|
|
.apple-button { |
|
|
background: var(--apple-blue); |
|
|
color: #fff; |
|
|
box-shadow: 0 6px 18px rgba(0,122,255,0.25); |
|
|
} |
|
|
.apple-button:hover { background: var(--apple-blue-hover); transform: translateY(-1px); } |
|
|
.apple-button:active { transform: translateY(0); } |
|
|
|
|
|
.reset-button { |
|
|
background: var(--apple-red); |
|
|
color: #fff; |
|
|
box-shadow: 0 6px 18px rgba(255,59,48,0.22); |
|
|
} |
|
|
.reset-button:hover { filter: brightness(0.96); transform: translateY(-1px); } |
|
|
.reset-button:active { transform: translateY(0); } |
|
|
|
|
|
/* Secondary ghost button style if needed */ |
|
|
.ghost-button { |
|
|
background: var(--apple-white); |
|
|
color: var(--text-color); |
|
|
border: 1px solid rgba(0,0,0,0.08); |
|
|
} |
|
|
.ghost-button:hover { background: #fafafa; } |
|
|
|
|
|
/* Status messages */ |
|
|
.status-success { color: var(--apple-green); font-weight: 600; } |
|
|
.status-error { color: var(--apple-red); font-weight: 600; } |
|
|
|
|
|
/* =============== Download & Preview =============== */ |
|
|
.preview-card { |
|
|
background: var(--apple-white); |
|
|
border: 1px solid rgba(0,0,0,0.06); |
|
|
border-radius: var(--radius-lg); |
|
|
box-shadow: var(--shadow-soft); |
|
|
overflow: hidden; |
|
|
} |
|
|
.preview-header { |
|
|
background: linear-gradient(180deg, #fafafa, #f2f2f7); |
|
|
border-bottom: 1px solid rgba(0,0,0,0.06); |
|
|
padding: 12px 16px; |
|
|
display: flex; align-items: center; gap: 8px; |
|
|
} |
|
|
.preview-dot { |
|
|
width: 10px; height: 10px; border-radius: 50%; |
|
|
background: #ff5f57; box-shadow: 16px 0 0 #ffbd2e, 32px 0 0 #28c840; |
|
|
} |
|
|
.preview-body { |
|
|
padding: 16px; |
|
|
max-height: 520px; |
|
|
overflow: auto; |
|
|
background: var(--apple-grey-50); |
|
|
} |
|
|
.preview-body pre, .preview-body code { |
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
/* =============== Footer (full-width black) =============== */ |
|
|
.footer { |
|
|
margin-top: 22px; |
|
|
border-radius: var(--radius-lg); |
|
|
overflow: hidden; |
|
|
background: var(--apple-black); |
|
|
color: #ffffff !important; /* Force texte blanc */ |
|
|
box-shadow: var(--shadow-soft); |
|
|
} |
|
|
.footer, .footer * { |
|
|
color: #ffffff !important; /* Tout le contenu en blanc */ |
|
|
} |
|
|
.footer a, .footer a:visited, .footer a:active { |
|
|
color: #ffffff !important; /* Liens blancs */ |
|
|
text-decoration: underline; |
|
|
text-underline-offset: var(--link-underline-offset); |
|
|
} |
|
|
.footer-inner { |
|
|
padding: 18px 20px; |
|
|
font-size: 14px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
/* Links globally */ |
|
|
a { text-decoration: underline; text-underline-offset: var(--link-underline-offset); } |
|
|
|
|
|
/* Hover animations */ |
|
|
.section, .preview-card, .header, .footer { |
|
|
transition: var(--transition); |
|
|
} |
|
|
.section:hover, .preview-card:hover { |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
/* Responsive */ |
|
|
@media (max-width: 768px) { |
|
|
.top-nav-inner { padding: 0 12px; } |
|
|
.header-inner { padding: 36px 20px; } |
|
|
.preview-body { max-height: 420px; } |
|
|
} |
|
|
""" |
|
|
) as app: |
|
|
|
|
|
gr.HTML(""" |
|
|
<div class='top-nav' role='navigation' aria-label='Navigation principale'> |
|
|
<div class='top-nav-inner'> |
|
|
<div class='brand'> |
|
|
<span class='dot' aria-hidden='true'></span> |
|
|
<span>EduHTML Creator</span> |
|
|
</div> |
|
|
<div class='nav-actions'> |
|
|
<button class='burger' aria-label='Menu' id='inlineMenuBtn'> |
|
|
<svg width='18' height='18' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'> |
|
|
<path d='M3 6h18M3 12h18M3 18h18' stroke='#1d1d1f' stroke-width='2' stroke-linecap='round'/> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class='inline-menu' id='inlineMenu' aria-hidden='true'> |
|
|
<h4>Actions rapides</h4> |
|
|
<a href='#' id='scrollTopLink'>Revenir en haut</a> |
|
|
<a href='https://www.apple.com' target='_blank' rel='noopener'>Inspiration Apple</a> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="header" role="banner"> |
|
|
<div class="header-inner"> |
|
|
<h1>🎓 EduHTML Creator</h1> |
|
|
<p> |
|
|
Transformez n'importe quel document en contenu éducatif HTML interactif, avec un design premium inspiré d'Apple. |
|
|
Fidélité au document, structuration claire, interactivité, et mise en valeur des informations clés. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(elem_classes=["main-container"]): |
|
|
with gr.Column(scale=1): |
|
|
gr.HTML("<div class='section'>") |
|
|
model_dropdown = gr.Dropdown( |
|
|
choices=list(MODELS.keys()), |
|
|
value="Gemini 2.5 Flash (Google AI)", |
|
|
label="Modèle LLM", |
|
|
info="Sélectionnez le modèle à utiliser pour la génération" |
|
|
) |
|
|
|
|
|
api_input = gr.Textbox( |
|
|
label="Clé API (optionnelle)", |
|
|
placeholder="API gratuite (Gemini Flash) disponible. Vous pouvez entrer votre propre clé.", |
|
|
info="Pour OpenAI/Anthropic, une clé est obligatoire.", |
|
|
type="password" |
|
|
) |
|
|
gr.HTML("</div>") |
|
|
|
|
|
gr.HTML("<div class='section alt'>") |
|
|
gr.HTML("<h3>Source du document</h3>") |
|
|
text_input = gr.Textbox( |
|
|
label="Texte copié/collé", |
|
|
placeholder="Collez votre texte ici...", |
|
|
lines=6 |
|
|
) |
|
|
url_input = gr.Textbox( |
|
|
label="Lien Web", |
|
|
placeholder="https://exemple.com/article" |
|
|
) |
|
|
file_input = gr.File( |
|
|
label="Fichier", |
|
|
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"] |
|
|
) |
|
|
gr.HTML("</div>") |
|
|
|
|
|
with gr.Row(): |
|
|
submit_btn = gr.Button("Générer le HTML", variant="primary", elem_classes=["apple-button"]) |
|
|
reset_btn = gr.Button("Reset", elem_classes=["reset-button"]) |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
status_output = gr.HTML(label="Statut") |
|
|
gr.HTML("<div class='section preview-card'>") |
|
|
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>Prévisualisation</div></div>") |
|
|
html_preview = gr.HTML(label="Prévisualisation", visible=False, elem_id="html-preview", elem_classes=["preview-body"]) |
|
|
html_file_output = gr.File(label="Fichier HTML téléchargeable", visible=False) |
|
|
gr.HTML("</div>") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="footer" role="contentinfo"> |
|
|
<div class="footer-inner"> |
|
|
<span>Design inspiré d'Apple • Contrastes élevés • Interactions fluides</span> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<script> |
|
|
(function() { |
|
|
const menuBtn = document.getElementById('inlineMenuBtn'); |
|
|
const menu = document.getElementById('inlineMenu'); |
|
|
const topLink = document.getElementById('scrollTopLink'); |
|
|
|
|
|
function closeMenu() { |
|
|
if (!menu) return; |
|
|
menu.classList.remove('open'); |
|
|
menu.setAttribute('aria-hidden', 'true'); |
|
|
} |
|
|
|
|
|
if (menuBtn && menu) { |
|
|
menuBtn.addEventListener('click', function(e) { |
|
|
e.preventDefault(); |
|
|
const isOpen = menu.classList.contains('open'); |
|
|
if (isOpen) { |
|
|
closeMenu(); |
|
|
} else { |
|
|
menu.classList.add('open'); |
|
|
menu.setAttribute('aria-hidden', 'false'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
if (topLink) { |
|
|
topLink.addEventListener('click', function(e) { |
|
|
e.preventDefault(); |
|
|
window.scrollTo({ top: 0, behavior: 'smooth' }); |
|
|
closeMenu(); |
|
|
}); |
|
|
} |
|
|
|
|
|
// Close when clicking outside |
|
|
document.addEventListener('click', function(e) { |
|
|
if (!menu || !menuBtn) return; |
|
|
if (!menu.contains(e.target) && !menuBtn.contains(e.target)) { |
|
|
closeMenu(); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Accessibility: close on Escape |
|
|
document.addEventListener('keydown', function(e) { |
|
|
if (e.key === 'Escape') closeMenu(); |
|
|
}); |
|
|
})(); |
|
|
</script> |
|
|
""") |
|
|
|
|
|
|
|
|
model_dropdown.change( |
|
|
fn=update_api_info, |
|
|
inputs=[model_dropdown], |
|
|
outputs=[api_input] |
|
|
) |
|
|
|
|
|
submit_btn.click( |
|
|
fn=generate_html, |
|
|
inputs=[model_dropdown, api_input, text_input, url_input, file_input], |
|
|
outputs=[html_file_output, status_output, gr.State()] |
|
|
).then( |
|
|
fn=lambda file, status, _: ( |
|
|
gr.update(visible=file is not None), |
|
|
status, |
|
|
gr.update(visible=file is not None, value=(open(file, 'r', encoding='utf-8').read() if file else "")) |
|
|
), |
|
|
inputs=[html_file_output, status_output, gr.State()], |
|
|
outputs=[html_file_output, status_output, html_preview] |
|
|
) |
|
|
|
|
|
reset_btn.click( |
|
|
fn=reset_form, |
|
|
outputs=[model_dropdown, api_input, text_input, url_input, file_input, status_output, html_file_output, html_preview] |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
app.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=True |
|
|
) |