Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from functools import partial | |
| import gradio as gr | |
| from config import APP_NAME, APP_SUBTITLE, get_openai_api_key, get_openai_model | |
| from openai_service import format_openai_error, stream_tutor_response | |
| from prompts import ABOUT_PAK_ANGELS, MODULES, build_system_instructions | |
| BUILD_ID = "pak-angels-zerogpu-2026-07-11-v2" | |
| try: | |
| import spaces | |
| except ImportError: | |
| class _SpacesFallback: | |
| def GPU(*args, **kwargs): | |
| if args and callable(args[0]) and len(args) == 1 and not kwargs: | |
| return args[0] | |
| def decorator(function): | |
| return function | |
| return decorator | |
| spaces = _SpacesFallback() | |
| print(f"Starting {APP_NAME} build {BUILD_ID}") | |
| PRIVACY_NOTICE = ( | |
| "Privacy notice: Do not enter confidential, proprietary, financial, medical, " | |
| "personal, or otherwise sensitive information into the AI Tutor." | |
| ) | |
| CSS = """ | |
| :root { | |
| --pak-blue: #0b5cab; | |
| --pak-blue-dark: #073f78; | |
| --pak-blue-soft: #eaf4ff; | |
| --pak-line: #d8e5f2; | |
| --pak-text: #14213d; | |
| } | |
| body, | |
| .gradio-container { | |
| background: #f7fbff !important; | |
| color: var(--pak-text); | |
| } | |
| .main-shell { | |
| max-width: 1180px; | |
| margin: 0 auto; | |
| } | |
| .hero { | |
| background: linear-gradient(135deg, #ffffff 0%, #eaf4ff 62%, #d6ebff 100%); | |
| border: 1px solid var(--pak-line); | |
| border-radius: 8px; | |
| padding: 24px; | |
| margin-bottom: 14px; | |
| } | |
| .hero h1 { | |
| color: var(--pak-blue-dark); | |
| font-size: 38px; | |
| line-height: 1.1; | |
| margin: 0 0 8px 0; | |
| } | |
| .hero p { | |
| margin: 5px 0; | |
| font-size: 16px; | |
| } | |
| .mode-label { | |
| border-left: 5px solid var(--pak-blue); | |
| background: #ffffff; | |
| border-radius: 8px; | |
| padding: 14px 16px; | |
| box-shadow: 0 1px 4px rgba(11, 92, 171, 0.08); | |
| } | |
| .privacy { | |
| background: #fff8e8; | |
| border: 1px solid #f1d28c; | |
| border-radius: 8px; | |
| padding: 12px 14px; | |
| font-size: 14px; | |
| } | |
| .side-panel { | |
| background: #ffffff; | |
| border: 1px solid var(--pak-line); | |
| border-radius: 8px; | |
| padding: 14px; | |
| } | |
| .suggestion-button { | |
| min-height: 46px; | |
| } | |
| button.primary { | |
| background: var(--pak-blue) !important; | |
| border-color: var(--pak-blue) !important; | |
| } | |
| """ | |
| def hero_html() -> str: | |
| return f""" | |
| <div class="hero"> | |
| <h1>{APP_NAME}</h1> | |
| <p><strong>{APP_SUBTITLE}</strong></p> | |
| <p>Pak Angels AI Tutor helps students, faculty, professionals, | |
| entrepreneurs, and startup founders learn Artificial Intelligence, | |
| build practical applications, design intelligent workflows, automate | |
| business processes, and develop AI-powered startups.</p> | |
| </div> | |
| """ | |
| def module_summary_html(module_name: str) -> str: | |
| module = MODULES[module_name] | |
| about = "" | |
| if module_name == "About Pak Angels": | |
| about = f"<p>{ABOUT_PAK_ANGELS}</p>" | |
| return f""" | |
| <div class="mode-label"> | |
| <strong>Selected learning mode:</strong> {module_name}<br> | |
| <span>{module["summary"]}</span> | |
| {about} | |
| </div> | |
| """ | |
| def topics_markdown(module_name: str) -> str: | |
| topics = "\n".join(f"- {topic}" for topic in MODULES[module_name]["topics"]) | |
| return f"### Topics in this mode\n{topics}" | |
| def get_suggestion(module_name: str, index: int) -> str: | |
| suggestions = MODULES[module_name]["suggestions"] | |
| return suggestions[index] if index < len(suggestions) else "" | |
| def update_module(module_name: str): | |
| suggestions = MODULES[module_name]["suggestions"] | |
| button_updates = [ | |
| gr.update(value=suggestion, visible=True) for suggestion in suggestions[:5] | |
| ] | |
| while len(button_updates) < 5: | |
| button_updates.append(gr.update(value="", visible=False)) | |
| return ( | |
| module_summary_html(module_name), | |
| topics_markdown(module_name), | |
| *button_updates, | |
| ) | |
| def add_user_message(message: str, history: list[dict[str, str]] | None): | |
| history = list(history or []) | |
| message = (message or "").strip() | |
| if not message: | |
| return "", history | |
| history.append({"role": "user", "content": message}) | |
| return "", history | |
| def generate_response(history: list[dict[str, str]] | None, module_name: str): | |
| history = list(history or []) | |
| if not history or history[-1]["role"] != "user": | |
| yield history | |
| return | |
| api_key = get_openai_api_key() | |
| if not api_key: | |
| message = ( | |
| "OPENAI_API_KEY is not configured. In Hugging Face Spaces, add it under " | |
| "Settings -> Variables and secrets -> New secret, then restart the Space." | |
| ) | |
| history.append({"role": "assistant", "content": message}) | |
| yield history | |
| return | |
| history.append({"role": "assistant", "content": ""}) | |
| try: | |
| for delta in stream_tutor_response( | |
| api_key=api_key, | |
| model=get_openai_model(), | |
| system_instructions=build_system_instructions(module_name), | |
| messages=history[:-1], | |
| ): | |
| history[-1]["content"] += delta | |
| yield history | |
| except Exception as error: | |
| history[-1]["content"] = format_openai_error(error) | |
| yield history | |
| def submit_message(message: str, history: list[dict[str, str]] | None, module_name: str): | |
| textbox, updated_history = add_user_message(message, history) | |
| yield textbox, updated_history | |
| for streamed_history in generate_response(updated_history, module_name): | |
| yield textbox, streamed_history | |
| def submit_suggestion( | |
| suggestion_index: int, | |
| history: list[dict[str, str]] | None, | |
| module_name: str, | |
| ): | |
| question = get_suggestion(module_name, suggestion_index) | |
| yield from submit_message(question, history, module_name) | |
| def clear_conversation(): | |
| return [] | |
| def build_app() -> gr.Blocks: | |
| with gr.Blocks( | |
| title=APP_NAME, | |
| css=CSS, | |
| theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"), | |
| ) as demo: | |
| with gr.Column(elem_classes=["main-shell"]): | |
| gr.HTML(hero_html()) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=1, min_width=260, elem_classes=["side-panel"]): | |
| module_selector = gr.Radio( | |
| choices=list(MODULES.keys()), | |
| value="Home", | |
| label="Learning mode", | |
| ) | |
| gr.Textbox( | |
| value=get_openai_model(), | |
| label="OpenAI model", | |
| interactive=False, | |
| ) | |
| new_button = gr.Button("New Conversation") | |
| clear_button = gr.Button("Clear Chat") | |
| with gr.Accordion("About Pak Angels", open=False): | |
| gr.Markdown(ABOUT_PAK_ANGELS) | |
| with gr.Column(scale=3, min_width=420): | |
| module_summary = gr.HTML(module_summary_html("Home")) | |
| gr.HTML(f'<div class="privacy">{PRIVACY_NOTICE}</div>') | |
| topics = gr.Markdown(topics_markdown("Home")) | |
| gr.Markdown("### Suggested questions") | |
| suggestion_buttons = [] | |
| with gr.Row(): | |
| suggestion_buttons.append( | |
| gr.Button(get_suggestion("Home", 0), elem_classes=["suggestion-button"]) | |
| ) | |
| suggestion_buttons.append( | |
| gr.Button(get_suggestion("Home", 1), elem_classes=["suggestion-button"]) | |
| ) | |
| with gr.Row(): | |
| suggestion_buttons.append( | |
| gr.Button(get_suggestion("Home", 2), elem_classes=["suggestion-button"]) | |
| ) | |
| suggestion_buttons.append( | |
| gr.Button(get_suggestion("Home", 3), elem_classes=["suggestion-button"]) | |
| ) | |
| suggestion_buttons.append( | |
| gr.Button(get_suggestion("Home", 4), elem_classes=["suggestion-button"]) | |
| ) | |
| chatbot = gr.Chatbot( | |
| label="Pak Angels AI Tutor", | |
| type="messages", | |
| height=520, | |
| show_copy_button=True, | |
| allow_tags=False, | |
| ) | |
| message_box = gr.Textbox( | |
| label="Ask Pak Angels AI Tutor", | |
| placeholder="Ask a question or choose a suggested question above.", | |
| lines=3, | |
| ) | |
| send_button = gr.Button("Send", variant="primary") | |
| module_selector.change( | |
| update_module, | |
| inputs=[module_selector], | |
| outputs=[module_summary, topics, *suggestion_buttons], | |
| ) | |
| send_button.click( | |
| submit_message, | |
| inputs=[message_box, chatbot, module_selector], | |
| outputs=[message_box, chatbot], | |
| ) | |
| message_box.submit( | |
| submit_message, | |
| inputs=[message_box, chatbot, module_selector], | |
| outputs=[message_box, chatbot], | |
| ) | |
| for index, button in enumerate(suggestion_buttons): | |
| button.click( | |
| partial(submit_suggestion, index), | |
| inputs=[chatbot, module_selector], | |
| outputs=[message_box, chatbot], | |
| ) | |
| new_button.click(clear_conversation, outputs=[chatbot]) | |
| clear_button.click(clear_conversation, outputs=[chatbot]) | |
| return demo | |
| demo = build_app() | |
| if __name__ == "__main__": | |
| demo.queue().launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) | |