|
|
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 pandas as pd |
|
|
import io |
|
|
import tempfile |
|
|
from urllib.parse import urlparse |
|
|
import re |
|
|
|
|
|
|
|
|
from docling.document_converter import DocumentConverter, PdfFormatOption |
|
|
from docling.datamodel.pipeline_options import PdfPipelineOptions |
|
|
from docling.datamodel.base_models import InputFormat |
|
|
|
|
|
|
|
|
try: |
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
from langchain_core.caches import BaseCache |
|
|
ChatGoogleGenerativeAI.model_rebuild() |
|
|
except Exception as e: |
|
|
print(f"Warning during rebuild: {e}") |
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pdf_options = PdfPipelineOptions( |
|
|
do_ocr=True, |
|
|
ocr_model="tesseract", |
|
|
|
|
|
|
|
|
ocr_languages=[ |
|
|
"eng","fra","deu","spa","ita","por","nld","pol","tur","ces","rus","ukr","ell","ron","hun", |
|
|
"bul","hrv","srp","slk","slv","lit","lav","est","cat","eus","glg","isl","dan","nor","swe", |
|
|
"fin","alb","mlt","afr","zul","swa","amh","uzb","aze","kaz","kir","mon","tgl","ind","msa", |
|
|
"tha","vie","khm","lao","mya","ben","hin","mar","guj","pan","mal","tam","tel","kan","nep", |
|
|
"sin","urd","fas","pus","kur","aze_cyrl","tat","uig","heb","ara","yid","grc","chr","epo", |
|
|
"hye","kat","kat_old","aze_latn","mkd","bel","srp_latn","srp_cyrillic", |
|
|
|
|
|
"chi_sim","chi_tra","jpn","kor" |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
format_options = { |
|
|
InputFormat.PDF: PdfFormatOption(pipeline_options=pdf_options) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
docling_converter = DocumentConverter(format_options=format_options) |
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
""" |
|
|
Extract text from an uploaded file or path (str). |
|
|
- Accepts an object with .name attribute (e.g. Gradio upload) OR a file path (str). |
|
|
- DocLing for: .pdf (Tesseract OCR enabled if configured), .docx, .xlsx, .pptx |
|
|
- Converts .csv /.xls -> temporary .xlsx then DocLing |
|
|
- .txt read directly |
|
|
""" |
|
|
if file is None: |
|
|
return "" |
|
|
|
|
|
|
|
|
path = file.name if hasattr(file, "name") else str(file) |
|
|
ext = os.path.splitext(path)[1].lower() |
|
|
|
|
|
docling_direct = {".pdf", ".docx", ".xlsx", ".pptx"} |
|
|
to_xlsx_first = {".csv", ".xls"} |
|
|
|
|
|
try: |
|
|
if ext in docling_direct: |
|
|
result = docling_converter.convert(path) |
|
|
return result.document.export_to_markdown() |
|
|
|
|
|
elif ext in to_xlsx_first: |
|
|
|
|
|
if ext == ".csv": |
|
|
df = pd.read_csv(path) |
|
|
else: |
|
|
df = pd.read_excel(path) |
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=True, suffix=".xlsx") as tmp: |
|
|
df.to_excel(tmp.name, index=False) |
|
|
result = docling_converter.convert(tmp.name) |
|
|
return result.document.export_to_markdown() |
|
|
|
|
|
elif ext == ".txt": |
|
|
with open(path, "r", encoding="utf-8") as f: |
|
|
return f.read() |
|
|
|
|
|
else: |
|
|
return "Unsupported file format" |
|
|
except Exception as e: |
|
|
return f"Error reading file: {str(e)}" |
|
|
|
|
|
def extract_text_from_url(url): |
|
|
"""Extract text from a 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"Error retrieving URL: {str(e)}" |
|
|
|
|
|
def get_document_content(text_input, url_input, file_input): |
|
|
"""Retrieve document content based on 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): |
|
|
"""Create an LLM model instance""" |
|
|
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): |
|
|
"""Generate educational HTML file""" |
|
|
start_time = time.time() |
|
|
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip(): |
|
|
return None, "❌ Error: Please provide an API key for this model.", 0 |
|
|
|
|
|
document_content = get_document_content(text_input, url_input, file_input) |
|
|
if not document_content: |
|
|
return None, "❌ Error: Please provide a document (text, URL or file).", 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"educational_document_{timestamp}.html" |
|
|
with open(filename, "w", encoding="utf-8") as f: |
|
|
f.write(html_content) |
|
|
|
|
|
success_message = f"✅ HTML file generated successfully in {generation_time:.2f} seconds!" |
|
|
return filename, success_message, generation_time |
|
|
|
|
|
except Exception as e: |
|
|
error_message = f"❌ Error during generation: {str(e)}" |
|
|
return None, error_message, 0 |
|
|
|
|
|
def reset_form(): |
|
|
"""Reset the form to zero""" |
|
|
return ( |
|
|
"Gemini 2.5 Flash (Google AI)", |
|
|
"", |
|
|
"", |
|
|
"", |
|
|
None, |
|
|
"", |
|
|
None, |
|
|
"" |
|
|
) |
|
|
|
|
|
def update_api_info(model_name): |
|
|
"""Update API information based on selected model""" |
|
|
if model_name == "Gemini 2.5 Flash (Google AI)": |
|
|
return gr.update( |
|
|
label="API Key (optional)", |
|
|
placeholder="Free API available until exhausted, or use your own key", |
|
|
info="💡 A free API is already configured for this model. You can use your own key if you wish." |
|
|
) |
|
|
else: |
|
|
return gr.update( |
|
|
label="API Key (required)", |
|
|
placeholder="Enter your API key", |
|
|
info="🔑 API key required for this model" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="EduHTML Creator - Educational HTML Content Generator", |
|
|
theme=gr.themes.Soft(), |
|
|
css=""" |
|
|
/* ==== Apple-inspired Global Reset & Typography ==== */ |
|
|
:root { |
|
|
--apple-black: #0000; |
|
|
--apple-white: #ffff; |
|
|
--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: #ffff !important; /* Force white text */ |
|
|
box-shadow: var(--shadow-soft); |
|
|
} |
|
|
.header, .header * { |
|
|
color: #ffff !important; /* All content in white */ |
|
|
} |
|
|
.header a, .header a:visited, .header a:active { |
|
|
color: #ffff !important; /* White links */ |
|
|
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; } |
|
|
|
|
|
/* ==== Tabs Styling (Apple-like) ==== */ |
|
|
.tab-nav { |
|
|
background: var(--apple-grey-100); |
|
|
border-radius: var(--radius-md); |
|
|
padding: 4px; |
|
|
display: flex; |
|
|
gap: 2px; |
|
|
margin-bottom: 16px; |
|
|
} |
|
|
|
|
|
.tab-nav button { |
|
|
flex: 1; |
|
|
padding: 8px 16px; |
|
|
border: none; |
|
|
border-radius: var(--radius-sm); |
|
|
background: transparent; |
|
|
color: var(--apple-grey-600); |
|
|
font-weight: 500; |
|
|
font-size: 14px; |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 6px; |
|
|
} |
|
|
|
|
|
.tab-nav button.selected { |
|
|
background: var(--apple-white); |
|
|
color: var(--text-color); |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.tab-nav button:hover:not(.selected) { |
|
|
color: var(--text-color); |
|
|
background: rgba(255,255,255,0.5); |
|
|
} |
|
|
|
|
|
/* Tab content */ |
|
|
.tab-content { |
|
|
min-height: 120px; |
|
|
} |
|
|
|
|
|
/* ==== 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: #ffff !important; /* Force white text */ |
|
|
box-shadow: var(--shadow-soft); |
|
|
} |
|
|
.footer, .footer * { |
|
|
color: #ffff !important; /* All content in white */ |
|
|
} |
|
|
.footer a, .footer a:visited, .footer a:active { |
|
|
color: #ffff !important; /* White links */ |
|
|
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="header" role="banner"> |
|
|
<div class="header-inner"> |
|
|
<h1>🎓 EduHTML Creator</h1> |
|
|
<p> |
|
|
Transform any document into interactive educational HTML content, with a premium Apple-inspired design. |
|
|
Document fidelity, clear structure, interactivity, and highlighting of key information. |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Column(elem_classes=["main-container"]): |
|
|
|
|
|
gr.HTML("<div class='section'>") |
|
|
model_dropdown = gr.Dropdown( |
|
|
choices=list(MODELS.keys()), |
|
|
value="Gemini 2.5 Flash (Google AI)", |
|
|
label="LLM Model", |
|
|
info="Select the model to use for generation" |
|
|
) |
|
|
|
|
|
api_input = gr.Textbox( |
|
|
label="API Key (optional)", |
|
|
placeholder="Free API (Gemini Flash) available. You can enter your own key.", |
|
|
info="For OpenAI/Anthropic, a key is required.", |
|
|
type="password" |
|
|
) |
|
|
gr.HTML("</div>") |
|
|
|
|
|
|
|
|
gr.HTML("<div class='section alt'>") |
|
|
gr.HTML("<h3>Document Source</h3>") |
|
|
|
|
|
with gr.Tabs(): |
|
|
with gr.TabItem("📝 Text"): |
|
|
text_input = gr.Textbox( |
|
|
label="Copied/pasted text", |
|
|
placeholder="Paste your text here...", |
|
|
lines=4 |
|
|
) |
|
|
|
|
|
with gr.TabItem("🌐 URL"): |
|
|
url_input = gr.Textbox( |
|
|
label="Web Link", |
|
|
placeholder="https://example.com/article" |
|
|
) |
|
|
|
|
|
with gr.TabItem("📁 File"): |
|
|
file_input = gr.File( |
|
|
label="File", |
|
|
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"] |
|
|
) |
|
|
|
|
|
gr.HTML("</div>") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
submit_btn = gr.Button("Generate HTML", variant="primary", elem_classes=["apple-button"]) |
|
|
reset_btn = gr.Button("Reset", elem_classes=["reset-button"]) |
|
|
|
|
|
|
|
|
status_output = gr.HTML(label="Status") |
|
|
gr.HTML("<div class='section preview-card'>") |
|
|
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>Preview</div></div>") |
|
|
html_preview = gr.HTML(label="Preview", visible=False, elem_id="html-preview", elem_classes=["preview-body"]) |
|
|
html_file_output = gr.File(label="Downloadable HTML file", visible=False) |
|
|
gr.HTML("</div>") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="footer" role="contentinfo"> |
|
|
<div class="footer-inner"> |
|
|
<span>Apple-inspired design • High contrasts • Smooth interactions</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 |
|
|
) |