Update app.py
Browse files
app.py
CHANGED
@@ -10,25 +10,61 @@ from langchain_core.messages import HumanMessage
|
|
10 |
from langchain_core.caches import BaseCache
|
11 |
from langchain_core.callbacks import Callbacks
|
12 |
ChatGoogleGenerativeAI.model_rebuild()
|
13 |
-
import PyPDF2
|
14 |
-
import docx
|
15 |
import pandas as pd
|
16 |
-
from pptx import Presentation
|
17 |
import io
|
18 |
import tempfile
|
19 |
from urllib.parse import urlparse
|
20 |
import re
|
21 |
|
22 |
-
# Import
|
|
|
|
|
|
|
|
|
|
|
23 |
try:
|
24 |
from langchain_google_genai import ChatGoogleGenerativeAI
|
25 |
from langchain_core.caches import BaseCache
|
26 |
ChatGoogleGenerativeAI.model_rebuild()
|
27 |
except Exception as e:
|
28 |
-
print(f"
|
29 |
from langchain_google_genai import ChatGoogleGenerativeAI
|
30 |
|
31 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
MODELS = {
|
33 |
"Gemini 2.5 Flash (Google AI)": {
|
34 |
"provider": "Google AI",
|
@@ -56,61 +92,67 @@ MODELS = {
|
|
56 |
}
|
57 |
}
|
58 |
|
59 |
-
# API
|
60 |
DEFAULT_GEMINI_API = os.getenv("FLASH_GOOGLE_API_KEY")
|
61 |
|
62 |
def extract_text_from_file(file):
|
63 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
if file is None:
|
65 |
return ""
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
try:
|
68 |
-
if
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
|
|
|
|
|
|
83 |
return f.read()
|
84 |
-
|
85 |
-
df = pd.read_excel(file.name)
|
86 |
-
return df.to_string()
|
87 |
-
elif file_extension == '.pptx':
|
88 |
-
prs = Presentation(file.name)
|
89 |
-
text = ""
|
90 |
-
for slide in prs.slides:
|
91 |
-
for shape in slide.shapes:
|
92 |
-
if hasattr(shape, "text"):
|
93 |
-
text += shape.text + "\n"
|
94 |
-
return text
|
95 |
else:
|
96 |
-
return "
|
97 |
except Exception as e:
|
98 |
-
return f"
|
99 |
|
100 |
def extract_text_from_url(url):
|
101 |
-
"""
|
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] #
|
109 |
except Exception as e:
|
110 |
-
return f"
|
111 |
|
112 |
def get_document_content(text_input, url_input, file_input):
|
113 |
-
"""
|
114 |
if text_input.strip():
|
115 |
return text_input.strip()
|
116 |
elif url_input.strip():
|
@@ -121,7 +163,7 @@ def get_document_content(text_input, url_input, file_input):
|
|
121 |
return ""
|
122 |
|
123 |
def create_llm_instance(model_name, api_key):
|
124 |
-
"""
|
125 |
model_config = MODELS[model_name]
|
126 |
if model_config["provider"] == "OpenAI":
|
127 |
return model_config["class"](
|
@@ -144,24 +186,24 @@ def create_llm_instance(model_name, api_key):
|
|
144 |
)
|
145 |
|
146 |
def generate_html(model_name, api_key, text_input, url_input, file_input):
|
147 |
-
"""
|
148 |
start_time = time.time()
|
149 |
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip():
|
150 |
-
return None, "❌
|
151 |
|
152 |
document_content = get_document_content(text_input, url_input, file_input)
|
153 |
if not document_content:
|
154 |
-
return None, "❌
|
155 |
|
156 |
try:
|
157 |
-
#
|
158 |
llm = create_llm_instance(model_name, api_key)
|
159 |
|
160 |
-
#
|
161 |
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f:
|
162 |
prompt_template = f.read()
|
163 |
|
164 |
-
#
|
165 |
model_config = MODELS[model_name]
|
166 |
prompt = prompt_template.format(
|
167 |
model_name=model_config["model_name"],
|
@@ -169,33 +211,33 @@ def generate_html(model_name, api_key, text_input, url_input, file_input):
|
|
169 |
document=document_content
|
170 |
)
|
171 |
|
172 |
-
#
|
173 |
message = HumanMessage(content=prompt)
|
174 |
response = llm.invoke([message])
|
175 |
html_content = response.content
|
176 |
|
177 |
-
#
|
178 |
html_content = html_content.replace("```html", "")
|
179 |
html_content = html_content.replace("```", "")
|
180 |
|
181 |
-
#
|
182 |
generation_time = time.time() - start_time
|
183 |
|
184 |
-
#
|
185 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
186 |
-
filename = f"
|
187 |
with open(filename, "w", encoding="utf-8") as f:
|
188 |
f.write(html_content)
|
189 |
|
190 |
-
success_message = f"✅
|
191 |
return filename, success_message, generation_time
|
192 |
|
193 |
except Exception as e:
|
194 |
-
error_message = f"❌
|
195 |
return None, error_message, 0
|
196 |
|
197 |
def reset_form():
|
198 |
-
"""
|
199 |
return (
|
200 |
"Gemini 2.5 Flash (Google AI)", # model_name
|
201 |
"", # api_key
|
@@ -208,23 +250,23 @@ def reset_form():
|
|
208 |
)
|
209 |
|
210 |
def update_api_info(model_name):
|
211 |
-
"""
|
212 |
if model_name == "Gemini 2.5 Flash (Google AI)":
|
213 |
return gr.update(
|
214 |
-
label="
|
215 |
-
placeholder="API
|
216 |
-
info="💡
|
217 |
)
|
218 |
else:
|
219 |
return gr.update(
|
220 |
-
label="
|
221 |
-
placeholder="
|
222 |
-
info="🔑
|
223 |
)
|
224 |
|
225 |
-
# Interface
|
226 |
with gr.Blocks(
|
227 |
-
title="EduHTML Creator -
|
228 |
theme=gr.themes.Soft(),
|
229 |
css="""
|
230 |
/* ==== Apple-inspired Global Reset & Typography ==== */
|
@@ -351,14 +393,14 @@ html, body {
|
|
351 |
border-radius: var(--radius-lg);
|
352 |
overflow: hidden;
|
353 |
background: var(--apple-black);
|
354 |
-
color: #ffff !important; /* Force
|
355 |
box-shadow: var(--shadow-soft);
|
356 |
}
|
357 |
.header, .header * {
|
358 |
-
color: #ffff !important; /*
|
359 |
}
|
360 |
.header a, .header a:visited, .header a:active {
|
361 |
-
color: #ffff !important; /*
|
362 |
text-decoration: underline;
|
363 |
text-underline-offset: var(--link-underline-offset);
|
364 |
}
|
@@ -542,14 +584,14 @@ input:focus, textarea:focus {
|
|
542 |
border-radius: var(--radius-lg);
|
543 |
overflow: hidden;
|
544 |
background: var(--apple-black);
|
545 |
-
color: #ffff !important; /* Force
|
546 |
box-shadow: var(--shadow-soft);
|
547 |
}
|
548 |
.footer, .footer * {
|
549 |
-
color: #ffff !important; /*
|
550 |
}
|
551 |
.footer a, .footer a:visited, .footer a:active {
|
552 |
-
color: #ffff !important; /*
|
553 |
text-decoration: underline;
|
554 |
text-underline-offset: var(--link-underline-offset);
|
555 |
}
|
@@ -585,155 +627,155 @@ a { text-decoration: underline; text-underline-offset: var(--link-underline-offs
|
|
585 |
<div class="header-inner">
|
586 |
<h1>🎓 EduHTML Creator</h1>
|
587 |
<p>
|
588 |
-
|
589 |
-
|
590 |
</p>
|
591 |
</div>
|
592 |
</div>
|
593 |
""")
|
594 |
|
595 |
with gr.Column(elem_classes=["main-container"]):
|
596 |
-
#
|
597 |
gr.HTML("<div class='section'>")
|
598 |
model_dropdown = gr.Dropdown(
|
599 |
choices=list(MODELS.keys()),
|
600 |
value="Gemini 2.5 Flash (Google AI)",
|
601 |
-
label="
|
602 |
-
info="
|
603 |
)
|
604 |
|
605 |
api_input = gr.Textbox(
|
606 |
-
label="
|
607 |
-
placeholder="API
|
608 |
-
info="
|
609 |
type="password"
|
610 |
)
|
611 |
gr.HTML("</div>")
|
612 |
|
613 |
-
#
|
614 |
gr.HTML("<div class='section alt'>")
|
615 |
-
gr.HTML("<h3>Source
|
616 |
|
617 |
with gr.Tabs():
|
618 |
-
with gr.TabItem("📝
|
619 |
text_input = gr.Textbox(
|
620 |
-
label="
|
621 |
-
placeholder="
|
622 |
lines=4
|
623 |
)
|
624 |
|
625 |
with gr.TabItem("🌐 URL"):
|
626 |
url_input = gr.Textbox(
|
627 |
-
label="
|
628 |
-
placeholder="https://
|
629 |
)
|
630 |
|
631 |
-
with gr.TabItem("📁
|
632 |
file_input = gr.File(
|
633 |
-
label="
|
634 |
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"]
|
635 |
)
|
636 |
|
637 |
gr.HTML("</div>")
|
638 |
|
639 |
-
#
|
640 |
with gr.Row():
|
641 |
-
submit_btn = gr.Button("
|
642 |
reset_btn = gr.Button("Reset", elem_classes=["reset-button"])
|
643 |
|
644 |
-
# Section
|
645 |
-
status_output = gr.HTML(label="
|
646 |
gr.HTML("<div class='section preview-card'>")
|
647 |
-
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>
|
648 |
-
html_preview = gr.HTML(label="
|
649 |
-
html_file_output = gr.File(label="
|
650 |
gr.HTML("</div>")
|
651 |
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
|
|
657 |
</div>
|
658 |
-
|
659 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
660 |
|
661 |
-
|
662 |
-
|
663 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
680 |
closeMenu();
|
681 |
-
} else {
|
682 |
-
menu.classList.add('open');
|
683 |
-
menu.setAttribute('aria-hidden', 'false');
|
684 |
}
|
685 |
});
|
686 |
-
}
|
687 |
|
688 |
-
|
689 |
-
|
690 |
-
e.
|
691 |
-
window.scrollTo({ top: 0, behavior: 'smooth' });
|
692 |
-
closeMenu();
|
693 |
});
|
694 |
-
}
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
// Accessibility: close on Escape
|
705 |
-
document.addEventListener('keydown', function(e) {
|
706 |
-
if (e.key === 'Escape') closeMenu();
|
707 |
-
});
|
708 |
-
})();
|
709 |
-
</script>
|
710 |
-
""")
|
711 |
-
|
712 |
-
# Événements
|
713 |
-
model_dropdown.change(
|
714 |
-
fn=update_api_info,
|
715 |
-
inputs=[model_dropdown],
|
716 |
-
outputs=[api_input]
|
717 |
-
)
|
718 |
|
719 |
-
|
720 |
-
|
721 |
-
|
722 |
-
|
723 |
-
|
724 |
-
|
725 |
-
|
726 |
-
|
727 |
-
|
728 |
-
|
729 |
-
|
730 |
-
|
731 |
-
|
732 |
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
|
737 |
|
738 |
if __name__ == "__main__":
|
739 |
app.launch(
|
|
|
10 |
from langchain_core.caches import BaseCache
|
11 |
from langchain_core.callbacks import Callbacks
|
12 |
ChatGoogleGenerativeAI.model_rebuild()
|
|
|
|
|
13 |
import pandas as pd
|
|
|
14 |
import io
|
15 |
import tempfile
|
16 |
from urllib.parse import urlparse
|
17 |
import re
|
18 |
|
19 |
+
# Import DocLing and necessary configuration classes
|
20 |
+
from docling.document_converter import DocumentConverter, PdfFormatOption
|
21 |
+
from docling.datamodel.pipeline_options import PdfPipelineOptions
|
22 |
+
from docling.datamodel.base_models import InputFormat
|
23 |
+
|
24 |
+
# Import and rebuild ChatGoogleGenerativeAI deferred
|
25 |
try:
|
26 |
from langchain_google_genai import ChatGoogleGenerativeAI
|
27 |
from langchain_core.caches import BaseCache
|
28 |
ChatGoogleGenerativeAI.model_rebuild()
|
29 |
except Exception as e:
|
30 |
+
print(f"Warning during rebuild: {e}")
|
31 |
from langchain_google_genai import ChatGoogleGenerativeAI
|
32 |
|
33 |
+
# --- START OF OCR CONFIGURATION ---
|
34 |
+
# Create a single, pre-configured DocumentConverter instance to be reused.
|
35 |
+
# This is more efficient than creating it on every function call.
|
36 |
+
|
37 |
+
# 1. Define the pipeline options to enable OCR for PDFs.
|
38 |
+
# Configure a single global DocLing converter with Tesseract OCR enabled and all languages
|
39 |
+
# Note: With tesseract-ocr-all installed, all language data files are available.
|
40 |
+
pdf_options = PdfPipelineOptions(
|
41 |
+
do_ocr=True,
|
42 |
+
ocr_model="tesseract",
|
43 |
+
# Provide a broad default set. With tesseract-ocr-all, many language packs exist.
|
44 |
+
# You can keep this small for speed or expand it. Here we include a practical wide set.
|
45 |
+
ocr_languages=[
|
46 |
+
"eng","fra","deu","spa","ita","por","nld","pol","tur","ces","rus","ukr","ell","ron","hun",
|
47 |
+
"bul","hrv","srp","slk","slv","lit","lav","est","cat","eus","glg","isl","dan","nor","swe",
|
48 |
+
"fin","alb","mlt","afr","zul","swa","amh","uzb","aze","kaz","kir","mon","tgl","ind","msa",
|
49 |
+
"tha","vie","khm","lao","mya","ben","hin","mar","guj","pan","mal","tam","tel","kan","nep",
|
50 |
+
"sin","urd","fas","pus","kur","aze_cyrl","tat","uig","heb","ara","yid","grc","chr","epo",
|
51 |
+
"hye","kat","kat_old","aze_latn","mkd","bel","srp_latn","srp_cyrillic",
|
52 |
+
# CJK — these are heavier and slower; include only if needed:
|
53 |
+
"chi_sim","chi_tra","jpn","kor"
|
54 |
+
]
|
55 |
+
)
|
56 |
+
|
57 |
+
# 2. Create the format-specific configuration.
|
58 |
+
format_options = {
|
59 |
+
InputFormat.PDF: PdfFormatOption(pipeline_options=pdf_options)
|
60 |
+
}
|
61 |
+
|
62 |
+
# 3. Initialize the converter with the OCR configuration.
|
63 |
+
# This converter will now automatically perform OCR on any PDF file.
|
64 |
+
docling_converter = DocumentConverter(format_options=format_options)
|
65 |
+
# --- END OF OCR CONFIGURATION ---
|
66 |
+
|
67 |
+
# Model configuration
|
68 |
MODELS = {
|
69 |
"Gemini 2.5 Flash (Google AI)": {
|
70 |
"provider": "Google AI",
|
|
|
92 |
}
|
93 |
}
|
94 |
|
95 |
+
# Default API for Gemini 2.5 Flash via HF Spaces Secrets
|
96 |
DEFAULT_GEMINI_API = os.getenv("FLASH_GOOGLE_API_KEY")
|
97 |
|
98 |
def extract_text_from_file(file):
|
99 |
+
"""
|
100 |
+
Extract text from an uploaded file or path (str).
|
101 |
+
- Accepts an object with .name attribute (e.g. Gradio upload) OR a file path (str).
|
102 |
+
- DocLing for: .pdf (Tesseract OCR enabled if configured), .docx, .xlsx, .pptx
|
103 |
+
- Converts .csv /.xls -> temporary .xlsx then DocLing
|
104 |
+
- .txt read directly
|
105 |
+
"""
|
106 |
if file is None:
|
107 |
return ""
|
108 |
+
|
109 |
+
# Normalize to a filesystem path string
|
110 |
+
path = file.name if hasattr(file, "name") else str(file)
|
111 |
+
ext = os.path.splitext(path)[1].lower()
|
112 |
+
|
113 |
+
docling_direct = {".pdf", ".docx", ".xlsx", ".pptx"}
|
114 |
+
to_xlsx_first = {".csv", ".xls"}
|
115 |
+
|
116 |
try:
|
117 |
+
if ext in docling_direct:
|
118 |
+
result = docling_converter.convert(path)
|
119 |
+
return result.document.export_to_markdown()
|
120 |
+
|
121 |
+
elif ext in to_xlsx_first:
|
122 |
+
# Convert CSV/XLS -> XLSX
|
123 |
+
if ext == ".csv":
|
124 |
+
df = pd.read_csv(path)
|
125 |
+
else: # .xls
|
126 |
+
df = pd.read_excel(path)
|
127 |
+
|
128 |
+
with tempfile.NamedTemporaryFile(delete=True, suffix=".xlsx") as tmp:
|
129 |
+
df.to_excel(tmp.name, index=False)
|
130 |
+
result = docling_converter.convert(tmp.name)
|
131 |
+
return result.document.export_to_markdown()
|
132 |
+
|
133 |
+
elif ext == ".txt":
|
134 |
+
with open(path, "r", encoding="utf-8") as f:
|
135 |
return f.read()
|
136 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
else:
|
138 |
+
return "Unsupported file format"
|
139 |
except Exception as e:
|
140 |
+
return f"Error reading file: {str(e)}"
|
141 |
|
142 |
def extract_text_from_url(url):
|
143 |
+
"""Extract text from a URL"""
|
144 |
try:
|
145 |
response = requests.get(url, timeout=10)
|
146 |
response.raise_for_status()
|
147 |
content = response.text
|
148 |
content = re.sub(r'<[^>]+>', '', content)
|
149 |
content = re.sub(r'\s+', ' ', content).strip()
|
150 |
+
return content[:10000] # Limit to 10k characters
|
151 |
except Exception as e:
|
152 |
+
return f"Error retrieving URL: {str(e)}"
|
153 |
|
154 |
def get_document_content(text_input, url_input, file_input):
|
155 |
+
"""Retrieve document content based on source"""
|
156 |
if text_input.strip():
|
157 |
return text_input.strip()
|
158 |
elif url_input.strip():
|
|
|
163 |
return ""
|
164 |
|
165 |
def create_llm_instance(model_name, api_key):
|
166 |
+
"""Create an LLM model instance"""
|
167 |
model_config = MODELS[model_name]
|
168 |
if model_config["provider"] == "OpenAI":
|
169 |
return model_config["class"](
|
|
|
186 |
)
|
187 |
|
188 |
def generate_html(model_name, api_key, text_input, url_input, file_input):
|
189 |
+
"""Generate educational HTML file"""
|
190 |
start_time = time.time()
|
191 |
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip():
|
192 |
+
return None, "❌ Error: Please provide an API key for this model.", 0
|
193 |
|
194 |
document_content = get_document_content(text_input, url_input, file_input)
|
195 |
if not document_content:
|
196 |
+
return None, "❌ Error: Please provide a document (text, URL or file).", 0
|
197 |
|
198 |
try:
|
199 |
+
# Create LLM instance
|
200 |
llm = create_llm_instance(model_name, api_key)
|
201 |
|
202 |
+
# Read prompt template
|
203 |
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f:
|
204 |
prompt_template = f.read()
|
205 |
|
206 |
+
# Replace variables
|
207 |
model_config = MODELS[model_name]
|
208 |
prompt = prompt_template.format(
|
209 |
model_name=model_config["model_name"],
|
|
|
211 |
document=document_content
|
212 |
)
|
213 |
|
214 |
+
# Generate content
|
215 |
message = HumanMessage(content=prompt)
|
216 |
response = llm.invoke([message])
|
217 |
html_content = response.content
|
218 |
|
219 |
+
# Clean any code tags from models
|
220 |
html_content = html_content.replace("```html", "")
|
221 |
html_content = html_content.replace("```", "")
|
222 |
|
223 |
+
# Calculate generation time
|
224 |
generation_time = time.time() - start_time
|
225 |
|
226 |
+
# Save HTML file
|
227 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
228 |
+
filename = f"educational_document_{timestamp}.html"
|
229 |
with open(filename, "w", encoding="utf-8") as f:
|
230 |
f.write(html_content)
|
231 |
|
232 |
+
success_message = f"✅ HTML file generated successfully in {generation_time:.2f} seconds!"
|
233 |
return filename, success_message, generation_time
|
234 |
|
235 |
except Exception as e:
|
236 |
+
error_message = f"❌ Error during generation: {str(e)}"
|
237 |
return None, error_message, 0
|
238 |
|
239 |
def reset_form():
|
240 |
+
"""Reset the form to zero"""
|
241 |
return (
|
242 |
"Gemini 2.5 Flash (Google AI)", # model_name
|
243 |
"", # api_key
|
|
|
250 |
)
|
251 |
|
252 |
def update_api_info(model_name):
|
253 |
+
"""Update API information based on selected model"""
|
254 |
if model_name == "Gemini 2.5 Flash (Google AI)":
|
255 |
return gr.update(
|
256 |
+
label="API Key (optional)",
|
257 |
+
placeholder="Free API available until exhausted, or use your own key",
|
258 |
+
info="💡 A free API is already configured for this model. You can use your own key if you wish."
|
259 |
)
|
260 |
else:
|
261 |
return gr.update(
|
262 |
+
label="API Key (required)",
|
263 |
+
placeholder="Enter your API key",
|
264 |
+
info="🔑 API key required for this model"
|
265 |
)
|
266 |
|
267 |
+
# Gradio Interface (Apple-like)
|
268 |
with gr.Blocks(
|
269 |
+
title="EduHTML Creator - Educational HTML Content Generator",
|
270 |
theme=gr.themes.Soft(),
|
271 |
css="""
|
272 |
/* ==== Apple-inspired Global Reset & Typography ==== */
|
|
|
393 |
border-radius: var(--radius-lg);
|
394 |
overflow: hidden;
|
395 |
background: var(--apple-black);
|
396 |
+
color: #ffff !important; /* Force white text */
|
397 |
box-shadow: var(--shadow-soft);
|
398 |
}
|
399 |
.header, .header * {
|
400 |
+
color: #ffff !important; /* All content in white */
|
401 |
}
|
402 |
.header a, .header a:visited, .header a:active {
|
403 |
+
color: #ffff !important; /* White links */
|
404 |
text-decoration: underline;
|
405 |
text-underline-offset: var(--link-underline-offset);
|
406 |
}
|
|
|
584 |
border-radius: var(--radius-lg);
|
585 |
overflow: hidden;
|
586 |
background: var(--apple-black);
|
587 |
+
color: #ffff !important; /* Force white text */
|
588 |
box-shadow: var(--shadow-soft);
|
589 |
}
|
590 |
.footer, .footer * {
|
591 |
+
color: #ffff !important; /* All content in white */
|
592 |
}
|
593 |
.footer a, .footer a:visited, .footer a:active {
|
594 |
+
color: #ffff !important; /* White links */
|
595 |
text-decoration: underline;
|
596 |
text-underline-offset: var(--link-underline-offset);
|
597 |
}
|
|
|
627 |
<div class="header-inner">
|
628 |
<h1>🎓 EduHTML Creator</h1>
|
629 |
<p>
|
630 |
+
Transform any document into interactive educational HTML content, with a premium Apple-inspired design.
|
631 |
+
Document fidelity, clear structure, interactivity, and highlighting of key information.
|
632 |
</p>
|
633 |
</div>
|
634 |
</div>
|
635 |
""")
|
636 |
|
637 |
with gr.Column(elem_classes=["main-container"]):
|
638 |
+
# Model Configuration Section
|
639 |
gr.HTML("<div class='section'>")
|
640 |
model_dropdown = gr.Dropdown(
|
641 |
choices=list(MODELS.keys()),
|
642 |
value="Gemini 2.5 Flash (Google AI)",
|
643 |
+
label="LLM Model",
|
644 |
+
info="Select the model to use for generation"
|
645 |
)
|
646 |
|
647 |
api_input = gr.Textbox(
|
648 |
+
label="API Key (optional)",
|
649 |
+
placeholder="Free API (Gemini Flash) available. You can enter your own key.",
|
650 |
+
info="For OpenAI/Anthropic, a key is required.",
|
651 |
type="password"
|
652 |
)
|
653 |
gr.HTML("</div>")
|
654 |
|
655 |
+
# Document Source Section with tabs
|
656 |
gr.HTML("<div class='section alt'>")
|
657 |
+
gr.HTML("<h3>Document Source</h3>")
|
658 |
|
659 |
with gr.Tabs():
|
660 |
+
with gr.TabItem("📝 Text"):
|
661 |
text_input = gr.Textbox(
|
662 |
+
label="Copied/pasted text",
|
663 |
+
placeholder="Paste your text here...",
|
664 |
lines=4
|
665 |
)
|
666 |
|
667 |
with gr.TabItem("🌐 URL"):
|
668 |
url_input = gr.Textbox(
|
669 |
+
label="Web Link",
|
670 |
+
placeholder="https://example.com/article"
|
671 |
)
|
672 |
|
673 |
+
with gr.TabItem("📁 File"):
|
674 |
file_input = gr.File(
|
675 |
+
label="File",
|
676 |
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"]
|
677 |
)
|
678 |
|
679 |
gr.HTML("</div>")
|
680 |
|
681 |
+
# Action buttons
|
682 |
with gr.Row():
|
683 |
+
submit_btn = gr.Button("Generate HTML", variant="primary", elem_classes=["apple-button"])
|
684 |
reset_btn = gr.Button("Reset", elem_classes=["reset-button"])
|
685 |
|
686 |
+
# Results Section
|
687 |
+
status_output = gr.HTML(label="Status")
|
688 |
gr.HTML("<div class='section preview-card'>")
|
689 |
+
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>Preview</div></div>")
|
690 |
+
html_preview = gr.HTML(label="Preview", visible=False, elem_id="html-preview", elem_classes=["preview-body"])
|
691 |
+
html_file_output = gr.File(label="Downloadable HTML file", visible=False)
|
692 |
gr.HTML("</div>")
|
693 |
|
694 |
+
# Footer (black)
|
695 |
+
gr.HTML("""
|
696 |
+
<div class="footer" role="contentinfo">
|
697 |
+
<div class="footer-inner">
|
698 |
+
<span>Apple-inspired design • High contrasts • Smooth interactions</span>
|
699 |
+
</div>
|
700 |
</div>
|
701 |
+
""")
|
702 |
+
|
703 |
+
# Light JS: smooth scroll to top, inline burger, focus handling
|
704 |
+
gr.HTML("""
|
705 |
+
<script>
|
706 |
+
(function() {
|
707 |
+
const menuBtn = document.getElementById('inlineMenuBtn');
|
708 |
+
const menu = document.getElementById('inlineMenu');
|
709 |
+
const topLink = document.getElementById('scrollTopLink');
|
710 |
+
|
711 |
+
function closeMenu() {
|
712 |
+
if (!menu) return;
|
713 |
+
menu.classList.remove('open');
|
714 |
+
menu.setAttribute('aria-hidden', 'true');
|
715 |
+
}
|
716 |
|
717 |
+
if (menuBtn && menu) {
|
718 |
+
menuBtn.addEventListener('click', function(e) {
|
719 |
+
e.preventDefault();
|
720 |
+
const isOpen = menu.classList.contains('open');
|
721 |
+
if (isOpen) {
|
722 |
+
closeMenu();
|
723 |
+
} else {
|
724 |
+
menu.classList.add('open');
|
725 |
+
menu.setAttribute('aria-hidden', 'false');
|
726 |
+
}
|
727 |
+
});
|
728 |
+
}
|
729 |
+
|
730 |
+
if (topLink) {
|
731 |
+
topLink.addEventListener('click', function(e) {
|
732 |
+
e.preventDefault();
|
733 |
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
734 |
+
closeMenu();
|
735 |
+
});
|
736 |
+
}
|
737 |
+
|
738 |
+
// Close when clicking outside
|
739 |
+
document.addEventListener('click', function(e) {
|
740 |
+
if (!menu || !menuBtn) return;
|
741 |
+
if (!menu.contains(e.target) && !menuBtn.contains(e.target)) {
|
742 |
closeMenu();
|
|
|
|
|
|
|
743 |
}
|
744 |
});
|
|
|
745 |
|
746 |
+
// Accessibility: close on Escape
|
747 |
+
document.addEventListener('keydown', function(e) {
|
748 |
+
if (e.key === 'Escape') closeMenu();
|
|
|
|
|
749 |
});
|
750 |
+
})();
|
751 |
+
</script>
|
752 |
+
""")
|
753 |
+
|
754 |
+
# Events
|
755 |
+
model_dropdown.change(
|
756 |
+
fn=update_api_info,
|
757 |
+
inputs=[model_dropdown],
|
758 |
+
outputs=[api_input]
|
759 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
760 |
|
761 |
+
submit_btn.click(
|
762 |
+
fn=generate_html,
|
763 |
+
inputs=[model_dropdown, api_input, text_input, url_input, file_input],
|
764 |
+
outputs=[html_file_output, status_output, gr.State()]
|
765 |
+
).then(
|
766 |
+
fn=lambda file, status, _: (
|
767 |
+
gr.update(visible=file is not None),
|
768 |
+
status,
|
769 |
+
gr.update(visible=file is not None, value=(open(file, 'r', encoding='utf-8').read() if file else ""))
|
770 |
+
),
|
771 |
+
inputs=[html_file_output, status_output, gr.State()],
|
772 |
+
outputs=[html_file_output, status_output, html_preview]
|
773 |
+
)
|
774 |
|
775 |
+
reset_btn.click(
|
776 |
+
fn=reset_form,
|
777 |
+
outputs=[model_dropdown, api_input, text_input, url_input, file_input, status_output, html_file_output, html_preview]
|
778 |
+
)
|
779 |
|
780 |
if __name__ == "__main__":
|
781 |
app.launch(
|