| import gradio as gr |
| import PyPDF2 |
| from pptx import Presentation |
| from PIL import Image |
| import io |
| import google.generativeai as genai |
| import fitz |
| import os |
| import logging |
| import re |
| import time |
| import hashlib |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
|
|
| |
| APP_PASSWORD = os.environ.get("APP_PASSWORD") |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") |
|
|
| |
| ISP_COLORS = { |
| "primary": "#1e3a5f", |
| "secondary": "#4a7ba7", |
| "accent": "#6fa3d2", |
| "light": "#a8c9e5", |
| "success": "#2e86ab", |
| "background": "#f0f4f8", |
| "text": "#2c3e50" |
| } |
|
|
| |
| custom_css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| .gradio-container { |
| font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif !important; |
| background: linear-gradient(135deg, #f0f4f8 0%, #e1ecf4 100%) !important; |
| } |
| |
| .gr-button-primary { |
| background: linear-gradient(135deg, #1e3a5f 0%, #4a7ba7 100%) !important; |
| border: none !important; |
| color: white !important; |
| font-weight: 500 !important; |
| letter-spacing: 0.5px !important; |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; |
| box-shadow: 0 4px 14px rgba(30, 58, 95, 0.25) !important; |
| } |
| |
| .gr-button-primary:hover { |
| transform: translateY(-2px) !important; |
| box-shadow: 0 8px 24px rgba(30, 58, 95, 0.35) !important; |
| background: linear-gradient(135deg, #2e4a6f 0%, #5a8bb7 100%) !important; |
| } |
| |
| .gr-button-secondary { |
| background: rgba(255, 255, 255, 0.95) !important; |
| border: 2px solid #4a7ba7 !important; |
| color: #1e3a5f !important; |
| font-weight: 500 !important; |
| transition: all 0.3s ease !important; |
| } |
| |
| .gr-button-secondary:hover { |
| background: #f0f4f8 !important; |
| border-color: #1e3a5f !important; |
| box-shadow: 0 3px 10px rgba(30, 58, 95, 0.15) !important; |
| } |
| |
| h1 { |
| color: #1e3a5f !important; |
| font-weight: 700 !important; |
| letter-spacing: -0.5px !important; |
| text-shadow: 0 2px 4px rgba(30, 58, 95, 0.1) !important; |
| } |
| |
| h2, h3 { |
| color: #2e4a6f !important; |
| font-weight: 600 !important; |
| } |
| |
| .login-container { |
| max-width: 400px; |
| margin: 100px auto; |
| padding: 40px; |
| background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(240,244,248,0.95) 100%); |
| border-radius: 20px; |
| box-shadow: 0 20px 60px rgba(30, 58, 95, 0.15); |
| border: 1px solid rgba(168, 201, 229, 0.3); |
| } |
| |
| .header-banner { |
| background: linear-gradient(135deg, #1e3a5f 0%, #4a7ba7 50%, #6fa3d2 100%); |
| color: white; |
| padding: 30px; |
| border-radius: 16px; |
| margin-bottom: 30px; |
| text-align: center; |
| box-shadow: 0 10px 30px rgba(30, 58, 95, 0.25); |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .header-banner::before { |
| content: ''; |
| position: absolute; |
| top: -50%; |
| right: -50%; |
| width: 200%; |
| height: 200%; |
| background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); |
| animation: shimmer 15s linear infinite; |
| } |
| |
| @keyframes shimmer { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| .success-notification { |
| background: linear-gradient(135deg, #2e86ab 0%, #6fa3d2 100%); |
| color: white; |
| padding: 14px 24px; |
| border-radius: 10px; |
| margin: 10px 0; |
| animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); |
| box-shadow: 0 6px 20px rgba(46, 134, 171, 0.3); |
| } |
| |
| @keyframes slideIn { |
| from { |
| transform: translateY(-20px); |
| opacity: 0; |
| } |
| to { |
| transform: translateY(0); |
| opacity: 1; |
| } |
| } |
| |
| .isp-footer { |
| text-align: center; |
| padding: 25px; |
| color: #4a7ba7; |
| font-size: 14px; |
| border-top: 2px solid #e1ecf4; |
| margin-top: 50px; |
| background: linear-gradient(180deg, transparent 0%, rgba(240,244,248,0.5) 100%); |
| } |
| |
| .gr-file { |
| border: 2px dashed #6fa3d2 !important; |
| background: rgba(111, 163, 210, 0.05) !important; |
| transition: all 0.3s ease !important; |
| } |
| |
| .gr-file:hover { |
| border-color: #4a7ba7 !important; |
| background: rgba(74, 123, 167, 0.08) !important; |
| } |
| |
| .gr-textbox input, .gr-textbox textarea { |
| border: 2px solid #a8c9e5 !important; |
| background: white !important; |
| transition: all 0.3s ease !important; |
| } |
| |
| .gr-textbox input:focus, .gr-textbox textarea:focus { |
| border-color: #4a7ba7 !important; |
| box-shadow: 0 0 0 3px rgba(74, 123, 167, 0.1) !important; |
| } |
| |
| .password-input input { |
| font-size: 16px !important; |
| padding: 12px !important; |
| } |
| |
| .gr-markdown { |
| color: #2c3e50 !important; |
| line-height: 1.6 !important; |
| } |
| |
| .gr-box { |
| border-radius: 12px !important; |
| border: 1px solid rgba(168, 201, 229, 0.2) !important; |
| background: rgba(255, 255, 255, 0.8) !important; |
| } |
| """ |
|
|
| |
| def verify_password(password): |
| """Verify if the entered password matches the app password""" |
| if not APP_PASSWORD: |
| return True, "⚠️ No password set in environment. Access granted." |
| |
| if password == APP_PASSWORD: |
| return True, "✅ Access granted! Welcome to ISP Reverse Unit Planner." |
| else: |
| return False, "❌ Incorrect password. Please try again." |
|
|
| |
| def extract_content_from_pdf(file_path): |
| try: |
| text = "" |
| images = [] |
| doc = fitz.open(file_path) |
| |
| for page_num, page in enumerate(doc): |
| text += f"\n--- Page {page_num + 1} ---\n" |
| text += page.get_text() + "\n" |
| |
| for img_index, img in enumerate(page.get_images()): |
| try: |
| xref = img[0] |
| base_image = doc.extract_image(xref) |
| image_bytes = base_image["image"] |
| image = Image.open(io.BytesIO(image_bytes)) |
| images.append(image) |
| except Exception as img_error: |
| logging.warning(f"Could not extract image {img_index} from page {page_num + 1}: {img_error}") |
| |
| doc.close() |
| return text, images |
| except Exception as e: |
| logging.error(f"Error extracting content from PDF: {e}") |
| return "", [] |
|
|
| |
| def extract_content_from_pptx(file_path): |
| try: |
| text = "" |
| images = [] |
| prs = Presentation(file_path) |
| |
| for slide_num, slide in enumerate(prs.slides): |
| text += f"\n--- Slide {slide_num + 1} ---\n" |
| |
| for shape in slide.shapes: |
| if hasattr(shape, 'text') and shape.text: |
| text += shape.text + "\n" |
| |
| if shape.shape_type == 13: |
| try: |
| image = shape.image |
| image_bytes = image.blob |
| img = Image.open(io.BytesIO(image_bytes)) |
| images.append(img) |
| except Exception as img_error: |
| logging.warning(f"Could not extract image from slide {slide_num + 1}: {img_error}") |
| |
| return text, images |
| except Exception as e: |
| logging.error(f"Error extracting content from PPTX: {e}") |
| return "", [] |
|
|
| |
| def process_file(file_path): |
| if file_path is None: |
| return "No file uploaded", [] |
|
|
| try: |
| file_ext = file_path.lower().split('.')[-1] |
| |
| if file_ext == 'pdf': |
| return extract_content_from_pdf(file_path) |
| elif file_ext in ['pptx', 'ppt']: |
| return extract_content_from_pptx(file_path) |
| else: |
| return f"Unsupported file format: .{file_ext}", [] |
| except Exception as e: |
| logging.error(f"Error processing file: {e}") |
| return f"An error occurred while processing the file: {str(e)}", [] |
|
|
| |
| |
| |
| _inline_guard = r"[\\^_{}]" |
| def _normalize_math_delimiters(s: str) -> str: |
| """Convert $$..$$ -> \\[..\\] and $..$ -> \\(..\\) while avoiding $20 currency.""" |
| if not s: |
| return s |
|
|
| |
| s = re.sub(r"\$\$(.+?)\$\$", r"[[DISPLAY_MATH:\1]]", s, flags=re.DOTALL) |
|
|
| |
| def _inline_repl(m): |
| inner = m.group(1) |
| |
| if re.search(_inline_guard, inner): |
| return f"((INLINE_MATH:{inner}))" |
| return f"${inner}$" |
|
|
| s = re.sub(r"\$(.+?)\$", _inline_repl, s, flags=re.DOTALL) |
|
|
| |
| s = s.replace("[[DISPLAY_MATH:", r"\[").replace("]]", r"\]") |
| s = s.replace("((INLINE_MATH:", r"\(").replace("))", r"\)") |
| return s |
|
|
| |
| def clean_response(response_text): |
| |
| cleaned = re.sub(r'```python|```markdown|```', '', response_text).strip() |
| |
| cleaned = re.sub(r'\n\s*\n\s*\n+', '\n\n', cleaned) |
| |
| cleaned = _normalize_math_delimiters(cleaned) |
| return cleaned |
|
|
| |
| def understand_content(text, images, progress=gr.Progress()): |
| try: |
| if not GEMINI_API_KEY: |
| return "❌ Gemini API key not configured. Please contact the administrator." |
| |
| |
| genai.configure(api_key=GEMINI_API_KEY) |
|
|
| progress(0.3, desc="🔍 Analyzing content...") |
| |
| |
| content_parts = [text] |
| |
| |
| for i, image in enumerate(images[:5]): |
| if i == 0: |
| progress(0.4, desc="🖼️ Processing images...") |
| content_parts.append(image) |
|
|
| progress(0.5, desc="🎯 Generating unit plan...") |
| |
| |
| prompt = """ |
| You are an expert instructional designer at International School of Panama (ISP). |
| Below are materials shared by a teacher. Your role is to reverse-engineer a comprehensive unit planner for this content. |
| Please create a unit planner that follows this EXACT structure: |
| # 📚 UNIT PLANNER |
| ## 📋 Standards |
| - If the subject is English, use Common Core |
| - If the subject is Spanish, use Common Core |
| - If the subject is English or Spanish Acquisition (EAL, SAL), use ACTFL |
| - If the subject is Social Studies, use AERO |
| - If the subject is Mathematics, use AERO |
| - If the subject is Science, use NGSS |
| ## 🎯 Transfer Goal |
| (This answers: "Why are we learning this?" How will students transfer their learning in "real-life" and in the future?) |
| ## ❓ Essential Questions |
| (What are the key questions that students will be asking, exploring, and answering through this unit?) |
| ## 💡 Enduring Understandings |
| (This is the "U" in KUD and helps students answer the essential questions by connecting concepts. Students will understand that [connections between key concepts] and why [connections between key concepts]) |
| ## 📖 Students Will Know |
| (This is the "K" in KUD, the content that students will use and process to construct concepts - the building blocks of understandings. Students will know [fact, formula, definition…]) |
| ## 🛠️ Students Will Be Able To |
| (This is the "D" in KUD, the skills that students will develop and use to process knowledge, concepts, understandings, and answer the essential questions. These skills should be connected to the command terms.) |
| ## 📝 Formative Assessments |
| ## 📊 Summative Assessments |
| ## 🗓️ Scope and Sequence |
| (This is a description of the overall flow of the unit. It includes an assessment plan and brief lesson objectives along the learning scale and towards the summative) |
| ## 🌟 Unit Overview |
| (This is a brief summation of everything above. What will students be learning, and why? How will they progressively develop and demonstrate their learning?) |
| ## ⚠️ Potential Barriers |
| (This identifies potential barriers to student learning and strategies to address them) |
| ## 🔗 Connections |
| ### ISP Core Values |
| - **Commitment to Excellence**: |
| - **Strength in Diversity**: |
| - **Compassion and Integrity**: |
| - **Innovative Spirit**: |
| - **Lasting Impact**: |
| ### IB Theory of Knowledge (TOK) |
| ### IB Approaches to Learning (ATL) |
| - Collaboration skills: |
| - Communication skills: |
| - Affective skills: |
| - Reflection skills: |
| - Information and media literacy skills: |
| - Critical thinking skills: |
| - Creative thinking skills: |
| ## 🎭 Authentic Assessment (GRASPS) |
| **Goal**: Assign an authentic (real-life), exciting (challenging), and meaningful (relatable, impactful) project or problem to solve |
| **Role**: Give students an authentic, exciting, and meaningful role to play |
| **Audience**: Identify an authentic, exciting, and meaningful audience that students can serve |
| **Situation**: Create an authentic, exciting, and meaningful scenario or context |
| **Project/Product/Performance and Progress**: Clarify what students are expected to do and how |
| **Success Criteria**: Provide task-specific learning scales and benchmark sheets |
| --- |
| *Unit planner generated 🎉* |
| Content to analyze: |
| """ |
|
|
| |
| model = genai.GenerativeModel('gemini-flash-latest') |
| response = model.generate_content(prompt + "\n\n" + text) |
|
|
| progress(0.8, desc="✨ Finalizing output...") |
| |
| |
| response_text = response.text |
| cleaned_response = clean_response(response_text) |
| |
| progress(1.0, desc="✅ Complete!") |
| return cleaned_response |
|
|
| except Exception as e: |
| logging.error(f"Error in content understanding: {e}") |
| return f"❌ Error in processing: {str(e)}" |
|
|
| |
| def generate_elt_plan(file, progress=gr.Progress()): |
| try: |
| progress(0.1, desc="📂 Opening file...") |
| |
| if file is None: |
| return "Please upload a file first", None |
| |
| logging.info(f"Processing file: {file.name}") |
| content, images = process_file(file.name) |
| |
| if isinstance(content, str) and ("error occurred" in content.lower() or "unsupported" in content.lower()): |
| return content, None |
| |
| if not content or len(content.strip()) < 50: |
| return "❌ The file appears to be empty or contains very little text. Please check the file and try again.", None |
| |
| logging.info(f"Extracted content length: {len(content)}, Number of images: {len(images)}") |
| progress(0.2, desc="📄 Content extracted, generating plan...") |
| |
| elt_plan = understand_content(content, images, progress) |
|
|
| |
| timestamp = time.strftime("%Y%m%d_%H%M%S") |
| filename = f"ISP_Unit_Plan_{timestamp}.md" |
| filepath = os.path.join("/tmp", filename) if os.path.exists("/tmp") else filename |
| |
| with open(filepath, "w", encoding="utf-8") as f: |
| f.write(elt_plan) |
|
|
| return elt_plan, filepath |
| |
| except Exception as e: |
| logging.error(f"Error in generate_elt_plan: {e}") |
| return f"❌ An error occurred: {str(e)}", None |
|
|
| |
| def create_main_interface(): |
| with gr.Blocks(theme=gr.themes.Soft( |
| primary_hue="blue", |
| secondary_hue="orange", |
| neutral_hue="gray", |
| font=["Inter", "system-ui", "sans-serif"] |
| ), css=custom_css) as interface: |
| |
| |
| gr.HTML(""" |
| <script> |
| window.MathJax = { tex: { inlineMath: [['\\\$begin:math:text$','\\\\\\$end:math:text$']], displayMath: [['\\\$begin:math:display$','\\\\\\$end:math:display$']] } }; |
| </script> |
| <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script> |
| """) |
| |
| |
| gr.HTML(""" |
| <div class="header-banner"> |
| <h1 style="margin: 0; font-size: 2.5em;">🔄 ISP Reverse Unit Planner</h1> |
| <p style="margin: 10px 0 0 0; opacity: 0.9; font-size: 1.1em;"> |
| Transform your teaching materials into comprehensive unit plans |
| </p> |
| </div> |
| """) |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown(""" |
| ### 📤 Upload Your Materials |
| Upload a PDF or PowerPoint presentation containing your teaching materials. |
| The AI will analyze the content and generate a complete unit plan following ISP standards. |
| """) |
| |
| file_input = gr.File( |
| label="Upload PPTX or PDF", |
| file_types=[".pdf", ".pptx", ".ppt"], |
| elem_classes="file-upload" |
| ) |
| |
| submit_btn = gr.Button( |
| "🎯 Generate Unit Plan", |
| variant="primary", |
| size="lg" |
| ) |
|
|
| with gr.Column(scale=2): |
| output = gr.Markdown( |
| label="Generated Unit Plan", |
| value="*Your unit plan will appear here...*", |
| elem_classes="output-area" |
| ) |
| |
| with gr.Row(): |
| copy_btn = gr.Button( |
| "📋 Copy to Clipboard", |
| variant="secondary", |
| size="sm" |
| ) |
| download_btn = gr.File( |
| label="📥 Download Unit Plan", |
| visible=False |
| ) |
|
|
| |
| gr.HTML(""" |
| <div class="isp-footer"> |
| <p>🏫 International School of Panama - Reverse Unit Planning Tool v2.5</p> |
| <p style="font-size: 12px; opacity: 0.7;"> |
| Powered by Gemini |
| </p> |
| </div> |
| """) |
|
|
| |
| def process_and_show_download(file, progress=gr.Progress()): |
| output_text, file_path = generate_elt_plan(file, progress) |
| if file_path: |
| return output_text, gr.File(value=file_path, visible=True) |
| else: |
| return output_text, gr.File(visible=False) |
| |
| submit_btn.click( |
| process_and_show_download, |
| inputs=[file_input], |
| outputs=[output, download_btn] |
| ) |
| |
| |
| copy_btn.click( |
| fn=None, |
| inputs=output, |
| outputs=None, |
| js=""" |
| async (text) => { |
| // Extract just the text content, removing any markdown artifacts |
| const textContent = text; |
| |
| try { |
| await navigator.clipboard.writeText(textContent); |
| |
| // Create success notification |
| const notification = document.createElement('div'); |
| notification.className = 'success-notification'; |
| notification.style.position = 'fixed'; |
| notification.style.bottom = '20px'; |
| notification.style.left = '50%'; |
| notification.style.transform = 'translateX(-50%)'; |
| notification.style.zIndex = '9999'; |
| notification.innerHTML = '✅ Unit plan copied to clipboard!'; |
| document.body.appendChild(notification); |
| |
| setTimeout(() => { |
| notification.style.opacity = '0'; |
| notification.style.transform = 'translateX(-50%) translateY(20px)'; |
| setTimeout(() => notification.remove(), 300); |
| }, 3000); |
| } catch (err) { |
| console.error('Failed to copy text: ', err); |
| alert('Failed to copy to clipboard. Please try selecting and copying manually.'); |
| } |
| } |
| """ |
| ) |
| |
| return interface |
|
|
| |
| def create_login_interface(): |
| with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as login_interface: |
| gr.HTML(""" |
| <div class="login-container"> |
| <div style="text-align: center; margin-bottom: 30px;"> |
| <h1 style="color: #003366; margin-bottom: 10px;">🔐 ISP Reverse Unit Planner</h1> |
| <p style="color: #666;">Please enter the access password to continue</p> |
| </div> |
| </div> |
| """) |
| |
| with gr.Column(): |
| password_input = gr.Textbox( |
| label="Password", |
| type="password", |
| placeholder="Enter access password", |
| elem_classes="password-input" |
| ) |
| |
| login_btn = gr.Button( |
| "🔓 Login", |
| variant="primary", |
| size="lg" |
| ) |
| |
| login_status = gr.Markdown("") |
| |
| |
| login_success = gr.State(False) |
| |
| def handle_login(password): |
| success, message = verify_password(password) |
| if success: |
| return message, True |
| else: |
| return message, False |
| |
| login_btn.click( |
| handle_login, |
| inputs=[password_input], |
| outputs=[login_status, login_success] |
| ) |
| |
| |
| password_input.submit( |
| handle_login, |
| inputs=[password_input], |
| outputs=[login_status, login_success] |
| ) |
| |
| return login_interface, login_success |
|
|
| |
| def create_app(): |
| with gr.Blocks( |
| theme=gr.themes.Soft( |
| primary_hue="blue", |
| secondary_hue="indigo", |
| neutral_hue="slate", |
| font=["Inter", "system-ui", "sans-serif"], |
| text_size="md", |
| spacing_size="md", |
| radius_size="lg" |
| ), |
| css=custom_css, |
| title="ISP Unit Planner" |
| ) as app: |
| |
| gr.HTML(""" |
| <script> |
| window.MathJax = { tex: { inlineMath: [['\\\$begin:math:text$','\\\\\\$end:math:text$']], displayMath: [['\\\$begin:math:display$','\\\\\\$end:math:display$']] } }; |
| </script> |
| <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script> |
| """) |
|
|
| |
| is_authenticated = gr.State(False) |
| |
| |
| with gr.Column(visible=True) as login_view: |
| gr.HTML(""" |
| <div style="max-width: 450px; margin: 80px auto; padding: 50px; background: linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(240,244,248,0.95) 100%); border-radius: 24px; box-shadow: 0 25px 70px rgba(30, 58, 95, 0.2); border: 1px solid rgba(168, 201, 229, 0.3);"> |
| <div style="text-align: center;"> |
| <h1 style="color: #1e3a5f; margin-bottom: 15px; font-size: 2.2em; font-weight: 300; letter-spacing: -0.5px;"> |
| <span style="font-weight: 700;">ISP</span> Unit Planner |
| </h1> |
| <p style="color: #4a7ba7; margin-bottom: 35px; font-size: 16px; font-weight: 400;">Authentication Required</p> |
| </div> |
| </div> |
| """) |
| |
| with gr.Column(): |
| password_input = gr.Textbox( |
| label="Password", |
| type="password", |
| placeholder="Enter access password", |
| elem_classes="password-input" |
| ) |
| |
| login_btn = gr.Button( |
| "Access Application", |
| variant="primary", |
| size="lg" |
| ) |
| |
| login_status = gr.Markdown("") |
| |
| |
| with gr.Column(visible=False) as main_view: |
| |
| gr.HTML(""" |
| <div class="header-banner"> |
| <h1 style="margin: 0; font-size: 2.8em; font-weight: 300; letter-spacing: -1px; position: relative; z-index: 1;"> |
| <span style="font-weight: 700;">ISP</span> Reverse Unit Planner |
| </h1> |
| <p style="margin: 12px 0 0 0; opacity: 0.95; font-size: 1.15em; font-weight: 300; letter-spacing: 0.5px; position: relative; z-index: 1;"> |
| Transform teaching materials into comprehensive unit plans |
| </p> |
| </div> |
| """) |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown(""" |
| ### Upload Materials |
| Select a PDF or PowerPoint file to analyze. |
| """) |
| |
| file_input = gr.File( |
| label="Choose File", |
| file_types=[".pdf", ".pptx", ".ppt"], |
| elem_classes="file-upload" |
| ) |
| |
| submit_btn = gr.Button( |
| "Generate Unit Plan", |
| variant="primary", |
| size="lg" |
| ) |
|
|
| with gr.Column(scale=2): |
| output = gr.Markdown( |
| label="Generated Unit Plan", |
| value="*Your unit plan will appear here...*", |
| elem_classes="output-area" |
| ) |
| |
| with gr.Row(): |
| copy_btn = gr.Button( |
| "Copy to Clipboard", |
| variant="secondary", |
| size="sm" |
| ) |
| download_btn = gr.File( |
| label="Download", |
| visible=False |
| ) |
| |
| |
| gr.HTML(""" |
| <div class="isp-footer"> |
| <p style="font-weight: 500; font-size: 15px; margin-bottom: 8px;">International School of Panama</p> |
| <p style="font-size: 13px; opacity: 0.8;"> |
| ISP Reverse Unit Planner v2.5 • Powered by Gemini |
| </p> |
| </div> |
| """) |
| |
| |
| def authenticate(password): |
| success, message = verify_password(password) |
| if success: |
| return ( |
| gr.update(value=message, visible=True), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| True |
| ) |
| else: |
| return ( |
| gr.update(value=message, visible=True), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| False |
| ) |
| |
| |
| login_btn.click( |
| authenticate, |
| inputs=[password_input], |
| outputs=[login_status, login_view, main_view, is_authenticated] |
| ) |
| |
| password_input.submit( |
| authenticate, |
| inputs=[password_input], |
| outputs=[login_status, login_view, main_view, is_authenticated] |
| ) |
| |
| |
| def process_and_show_download(file, progress=gr.Progress()): |
| output_text, file_path = generate_elt_plan(file, progress) |
| if file_path: |
| return output_text, gr.File(value=file_path, visible=True) |
| else: |
| return output_text, gr.File(visible=False) |
| |
| submit_btn.click( |
| process_and_show_download, |
| inputs=[file_input], |
| outputs=[output, download_btn] |
| ) |
| |
| |
| copy_btn.click( |
| fn=None, |
| inputs=output, |
| outputs=None, |
| js=""" |
| async (text) => { |
| try { |
| await navigator.clipboard.writeText(text); |
| |
| // Create success notification |
| const notification = document.createElement('div'); |
| notification.className = 'success-notification'; |
| notification.style.cssText = ` |
| position: fixed; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: linear-gradient(135deg, #2e86ab 0%, #6fa3d2 100%); |
| color: white; |
| padding: 14px 28px; |
| border-radius: 12px; |
| z-index: 9999; |
| font-weight: 500; |
| box-shadow: 0 8px 24px rgba(46, 134, 171, 0.35); |
| animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); |
| `; |
| notification.innerHTML = '✅ Unit plan copied to clipboard!'; |
| document.body.appendChild(notification); |
| |
| setTimeout(() => { |
| notification.style.opacity = '0'; |
| notification.style.transform = 'translateX(-50%) translateY(20px)'; |
| setTimeout(() => notification.remove(), 300); |
| }, 3000); |
| } catch (err) { |
| console.error('Failed to copy text: ', err); |
| alert('Failed to copy to clipboard. Please try selecting and copying manually.'); |
| } |
| } |
| """ |
| ) |
| |
| return app |
|
|
| |
| if __name__ == "__main__": |
| |
| if not GEMINI_API_KEY: |
| print("⚠️ WARNING: GEMINI_API_KEY environment variable not set!") |
| print("Please set the GEMINI_API_KEY environment variable before running.") |
| print("Example: export GEMINI_API_KEY='your-api-key-here'") |
| |
| if not APP_PASSWORD: |
| print("⚠️ WARNING: APP_PASSWORD environment variable not set!") |
| print("Using default password. Please set APP_PASSWORD for production.") |
| |
| |
| app = create_app() |
| app.launch( |
| share=True, |
| server_name="0.0.0.0", |
| server_port=7860, |
| favicon_path=None, |
| show_error=True |
| ) |