import os, base64 from dotenv import load_dotenv from fastapi import FastAPI, Request, Depends from fastapi.responses import RedirectResponse, JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from authlib.integrations.starlette_client import OAuth import gradio as gr import requests from src.manager.manager import GeminiManager # 1. Load environment -------------------------------------------------- load_dotenv() AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN") AUTH0_CLIENT_ID = os.getenv("AUTH0_CLIENT_ID") AUTH0_CLIENT_SECRET = os.getenv("AUTH0_CLIENT_SECRET") AUTH0_AUDIENCE = os.getenv("AUTH0_AUDIENCE") SESSION_SECRET_KEY = os.getenv("SESSION_SECRET_KEY", "replace‑me") # 2. Auth0 client ------------------------------------------------------ oauth = OAuth() oauth.register( "auth0", client_id=AUTH0_CLIENT_ID, client_secret=AUTH0_CLIENT_SECRET, client_kwargs={"scope": "openid profile email"}, server_metadata_url=f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration", ) # 3. FastAPI app ------------------------------------------------------- app = FastAPI() # Create static directory if it doesn't exist os.makedirs("static/fonts/ui-sans-serif", exist_ok=True) os.makedirs("static/fonts/system-ui", exist_ok=True) # Mount static files directory app.mount("/static", StaticFiles(directory="static"), name="static") # Add session middleware (no auth requirement) app.add_middleware( SessionMiddleware, secret_key=SESSION_SECRET_KEY, session_cookie="session", max_age=86400, same_site="lax", https_only=False ) # 4. Auth routes ------------------------------------------------------- # Dependency to get the current user def get_user(request: Request): user = request.session.get('user') if user: return user['name'] return None @app.get('/') def public(request: Request, user = Depends(get_user)): if user: return RedirectResponse("/gradio") else: return RedirectResponse("/main") @app.get("/login") async def login(request: Request): print("Session cookie:", request.cookies.get("session")) print("Session data:", dict(request.session)) return await oauth.auth0.authorize_redirect(request, request.url_for("auth"), audience=AUTH0_AUDIENCE, prompt="login") @app.get("/auth") async def auth(request: Request): try: token = await oauth.auth0.authorize_access_token(request) request.session["user"] = token["userinfo"] return RedirectResponse("/") except Exception as e: return JSONResponse({"error": str(e)}, status_code=500) @app.get("/logout") async def logout(request: Request): auth0_logout_url = ( f"https://{AUTH0_DOMAIN}/v2/logout" f"?client_id={AUTH0_CLIENT_ID}" f"&returnTo=http://localhost:7860/post-logout" ) return RedirectResponse(auth0_logout_url) @app.get("/post-logout") async def post_logout(request: Request): request.session.clear() return RedirectResponse("/") @app.get("/manifest.json") async def manifest(): return JSONResponse({ "name": "HASHIRU AI", "short_name": "HASHIRU", "icons": [], "start_url": "/", "display": "standalone" }) @app.get("/api/login-status") async def api_login_status(request: Request): if "user" in request.session: user_info = request.session["user"] user_name = user_info.get("name", user_info.get("email", "User")) return {"status": f"Logged in: {user_name}"} else: return {"status": "Logged out"} # 5. Gradio UI --------------------------------------------------------- _logo_b64 = base64.b64encode(open("HASHIRU_LOGO.png", "rb").read()).decode() HEADER_HTML = f"""

HASHIRU AI

""" CSS = """ .logo { margin-right: 20px; } .login-status { font-weight: bold; margin-right: 20px; padding: 8px; border-radius: 4px; background-color: #f0f0f0; } /* Profile style improvements */ .profile-container { position: relative; display: inline-block; float: right; margin-right: 20px; z-index: 9999; /* Ensure this is higher than any other elements */ } #profile-name { background-color: transparent; /* Transparent background */ color: #f97316; /* Orange text */ font-weight: bold; padding: 10px 14px; border-radius: 6px; cursor: pointer; user-select: none; display: inline-flex; align-items: center; justify-content: center; min-width: 40px; min-height: 40px; border: 2px solid #f97316; /* Add border */ } #profile-menu { position: fixed; /* Changed from absolute to fixed for better overlay */ right: auto; /* Let JS position it precisely */ top: auto; /* Let JS position it precisely */ background-color: transparent; border: 1px solid transparent; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000; /* Very high z-index to ensure it's on top */ overflow: visible; width: 160px; } #profile-menu.hidden { display: none; } #profile-menu button { background-color: #f97316; /* Orange background */ border: none; color: white; /* White text */ font-size: 16px; border-radius: 8px; text-align: left; width: 100%; padding: 12px 16px; cursor: pointer; transition: background-color 0.2s ease; display: block; } #profile-menu button:hover { background-color: #ea580c; /* Darker orange on hover */ } #profile-menu button .icon { margin-right: 8px; color: white; /* White icon color */ } /* Fix dropdown issues */ input[type="text"], select { color: black !important; } /* Optional: limit dropdown scroll if options are long */ .gr-dropdown .gr-dropdown-options { max-height: 200px; overflow-y: auto; } /* User avatar styles */ .user-avatar { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-weight: bold; text-transform: uppercase; font-size: 20px; /* Larger font size */ color: #f97316; /* Orange color */ } /* Fix for gradio interface */ .gradio-container { overflow: visible !important; } /* Fix other container issues that might cause scrolling */ body, html { overflow-x: hidden; /* Prevent horizontal scrolling */ } #gradio-app, .gradio-container .overflow-hidden { overflow: visible !important; /* Override any overflow hidden that might interfere */ } /* Ensure dropdown appears above everything */ .profile-container * { z-index: 9999; } """ def run_model(message, history): history.append({"role": "user", "content": message}) yield "", history for messages in model_manager.run(history): for m in messages: if m.get("role") == "summary": print("Summary:", m["content"]) yield "", messages def update_model(name): print("Model changed to:", name) with gr.Blocks() as login: btn = gr.Button("Login") _js_redirect = """ () => { url = '/login' + window.location.search; window.open(url, '_blank'); } """ btn.click(None, js=_js_redirect) app = gr.mount_gradio_app(app, login, path="/main") with gr.Blocks(css=CSS, fill_width=True, fill_height=True) as demo: model_manager = GeminiManager(gemini_model="gemini-2.0-flash") with gr.Row(): gr.Markdown(HEADER_HTML) with gr.Column(scale=1, min_width=250): profile_html = gr.HTML(value="""
G
""") with gr.Column(): model_dropdown = gr.Dropdown( [ "HASHIRU", "Static-HASHIRU", "Cloud-Only HASHIRU", "Local-Only HASHIRU", "No-Economy HASHIRU", ], value="HASHIRU", interactive=True, ) model_dropdown.change(update_model, model_dropdown) chatbot = gr.Chatbot( avatar_images=("HASHIRU_2.png", "HASHIRU.png"), type="messages", show_copy_button=True, editable="user", placeholder="Type your message here…", ) gr.ChatInterface(run_model, type="messages", chatbot=chatbot, additional_outputs=[chatbot], save_history=True) demo.load(None, None, None, js=""" async () => { const profileBtn = document.getElementById("profile-name"); const profileMenu = document.getElementById("profile-menu"); const loginBtn = document.getElementById("login-btn"); const logoutBtn = document.getElementById("logout-btn"); // Position menu and handle positioning function positionMenu() { const btnRect = profileBtn.getBoundingClientRect(); profileMenu.style.position = "fixed"; profileMenu.style.top = (btnRect.bottom + 5) + "px"; profileMenu.style.left = (btnRect.right - profileMenu.offsetWidth) + "px"; // Align with right edge } // Close menu when clicking outside document.addEventListener('click', (event) => { if (!profileBtn.contains(event.target) && !profileMenu.contains(event.target)) { profileMenu.classList.add("hidden"); } }); // Toggle menu profileBtn.onclick = (e) => { e.stopPropagation(); positionMenu(); // Position before showing profileMenu.classList.toggle("hidden"); // If showing menu, make sure it's positioned correctly if (!profileMenu.classList.contains("hidden")) { setTimeout(positionMenu, 0); // Reposition after render } } // Handle window resize window.addEventListener('resize', () => { if (!profileMenu.classList.contains("hidden")) { positionMenu(); } }); // Get initial letter for avatar function getInitial(name) { if (name && name.length > 0) { return name.charAt(0); } return "?"; } try { const res = await fetch('/api/login-status', { credentials: 'include' }); const data = await res.json(); if (!data.status.includes("Logged out")) { const name = data.status.replace("Logged in: ", ""); profileBtn.innerHTML = `
${getInitial(name)}
`; profileBtn.title = name; loginBtn.style.display = "none"; logoutBtn.style.display = "block"; } else { profileBtn.innerHTML = `
G
`; profileBtn.title = "Guest"; loginBtn.style.display = "block"; logoutBtn.style.display = "none"; } } catch (error) { console.error("Error fetching login status:", error); profileBtn.innerHTML = `
?
`; profileBtn.title = "Login status unknown"; } } """) app = gr.mount_gradio_app(app, demo, path="/gradio",auth_dependency=get_user) # 6. Entrypoint -------------------------------------------------------- if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)