From ff85dd87fefd1588a884c6e8e58e4a0704b726cb Mon Sep 17 00:00:00 2001 From: Kunal Pai Date: Mon, 12 May 2025 08:05:53 -0700 Subject: [PATCH 1/7] initial work on Auth0 --- main.py | 142 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/main.py b/main.py index f71a047..682820f 100644 --- a/main.py +++ b/main.py @@ -1,55 +1,98 @@ -import gradio as gr +# ------------------------------ main.py ------------------------------ -import base64 +import os, base64 +from dotenv import load_dotenv +from fastapi import FastAPI, Request +from fastapi.responses import RedirectResponse +from starlette.middleware.sessions import SessionMiddleware +from starlette.middleware.base import BaseHTTPMiddleware +from authlib.integrations.starlette_client import OAuth +import gradio as gr from src.manager.manager import GeminiManager -_logo_bytes = open("HASHIRU_LOGO.png", "rb").read() -_logo_b64 = base64.b64encode(_logo_bytes).decode() -_header_html = f""" -
- -

- HASHIRU AI -

-
-""" -css = """ - .logo { margin-right: 20px; } - """ +# 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() + +# 3a. *Inner* auth‑gate middleware (needs session already populated) +class RequireAuthMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + public = ("/login", "/auth", "/logout", "/static", "/assets", "/favicon") + if any(request.url.path.startswith(p) for p in public): + return await call_next(request) + if "user" not in request.session: + return RedirectResponse("/login") + return await call_next(request) + +app.add_middleware(RequireAuthMiddleware) # Add **first** (inner) +app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET_KEY) # Add **second** (outer) + +# 4. Auth routes ------------------------------------------------------- +@app.get("/login") +async def login(request: Request): + return await oauth.auth0.authorize_redirect(request, request.url_for("auth"), audience=AUTH0_AUDIENCE) + +@app.get("/auth") +async def auth(request: Request): + token = await oauth.auth0.authorize_access_token(request) + request.session["user"] = token["userinfo"] + return RedirectResponse("/") + +@app.get("/logout") +async def logout(request: Request): + request.session.clear() + return RedirectResponse( + f"https://{AUTH0_DOMAIN}/v2/logout?client_id={AUTH0_CLIENT_ID}&returnTo=http://localhost:7860/" + ) + +# 5. Gradio UI --------------------------------------------------------- +_logo_b64 = base64.b64encode(open("HASHIRU_LOGO.png", "rb").read()).decode() +HEADER_HTML = f""" +
+ +

HASHIRU AI

+
""" +CSS = ".logo{margin-right:20px;}" def run_model(message, history): - history.append({ - "role": "user", - "content": message, - }) + history.append({"role": "user", "content": message}) yield "", history for messages in model_manager.run(history): - for message in messages: - if message.get("role") == "summary": - print(f"Summary: {message.get('content', '')}") + for m in messages: + if m.get("role") == "summary": + print("Summary:", m["content"]) yield "", messages -def update_model(model_name): - print(f"Model changed to: {model_name}") - pass +def update_model(name): + print("Model changed to:", name) -with gr.Blocks(css=css, fill_width=True, fill_height=True) as demo: +with gr.Blocks(css=CSS, fill_width=True, fill_height=True) as demo: model_manager = GeminiManager(gemini_model="gemini-2.0-flash") - - with gr.Column(scale=1): - with gr.Row(scale=0): - gr.Markdown(_header_html) + with gr.Column(): + with gr.Row(): + gr.Markdown(HEADER_HTML) model_dropdown = gr.Dropdown( - choices=[ + [ "HASHIRU", "Static-HASHIRU", "Cloud-Only HASHIRU", @@ -59,20 +102,19 @@ def update_model(model_name): value="HASHIRU", interactive=True, ) - - model_dropdown.change( - fn=update_model, inputs=model_dropdown, outputs=[]) - with gr.Row(scale=1): + model_dropdown.change(update_model, model_dropdown) + with gr.Row(): chatbot = gr.Chatbot( avatar_images=("HASHIRU_2.png", "HASHIRU.png"), - type="messages", - show_copy_button=True, - editable="user", - scale=1, - render_markdown=True, - placeholder="Type your message here...", + type="messages", show_copy_button=True, editable="user", + placeholder="Type your message here…", ) - gr.ChatInterface(fn=run_model, type="messages", chatbot=chatbot, - additional_outputs=[chatbot], save_history=True) + gr.ChatInterface(run_model, type="messages", chatbot=chatbot, additional_outputs=[chatbot], save_history=True) + +# Mount at root +gr.mount_gradio_app(app, demo, path="/") + +# 6. Entrypoint -------------------------------------------------------- if __name__ == "__main__": - demo.launch() + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=7860) From 714ea2582d02a87450e7485aab76102166db21ae Mon Sep 17 00:00:00 2001 From: Harshil Patel Date: Sat, 17 May 2025 18:20:01 -0700 Subject: [PATCH 2/7] Make auth0 login and logout work - needs work on UI --- main.py | 236 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 188 insertions(+), 48 deletions(-) diff --git a/main.py b/main.py index 682820f..3c09256 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,12 @@ -# ------------------------------ main.py ------------------------------ - import os, base64 from dotenv import load_dotenv from fastapi import FastAPI, Request -from fastapi.responses import RedirectResponse +from fastapi.responses import RedirectResponse, JSONResponse, FileResponse +from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware -from starlette.middleware.base import BaseHTTPMiddleware from authlib.integrations.starlette_client import OAuth import gradio as gr +import requests from src.manager.manager import GeminiManager # 1. Load environment -------------------------------------------------- @@ -31,36 +30,72 @@ # 3. FastAPI app ------------------------------------------------------- app = FastAPI() -# 3a. *Inner* auth‑gate middleware (needs session already populated) -class RequireAuthMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next): - public = ("/login", "/auth", "/logout", "/static", "/assets", "/favicon") - if any(request.url.path.startswith(p) for p in public): - return await call_next(request) - if "user" not in request.session: - return RedirectResponse("/login") - return await call_next(request) +# 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") -app.add_middleware(RequireAuthMiddleware) # Add **first** (inner) -app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET_KEY) # Add **second** (outer) +# 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 ------------------------------------------------------- @app.get("/login") async def login(request: Request): - return await oauth.auth0.authorize_redirect(request, request.url_for("auth"), audience=AUTH0_AUDIENCE) + 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): - token = await oauth.auth0.authorize_access_token(request) - request.session["user"] = token["userinfo"] - return RedirectResponse("/") + 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): - request.session.clear() - return RedirectResponse( - f"https://{AUTH0_DOMAIN}/v2/logout?client_id={AUTH0_CLIENT_ID}&returnTo=http://localhost:7860/" + auth0_logout_url = ( + f"https://{AUTH0_DOMAIN}/v2/logout" + f"?client_id={AUTH0_CLIENT_ID}" + f"&returnTo=http://localhost:7860/post-logout" + f"&federated" ) + 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() @@ -69,8 +104,74 @@ async def logout(request: Request):

HASHIRU AI

""" -CSS = ".logo{margin-right:20px;}" +CSS = """ +.logo { + margin-right: 20px; +} +.login-status { + font-weight: bold; + margin-right: 20px; + padding: 8px; + border-radius: 4px; + background-color: #f0f0f0; +} +#profile-name { + background-color: #f97316; /* Orange */ + color: white; + font-weight: bold; + padding: 10px 14px; + border-radius: 6px; + cursor: pointer; + user-select: none; + display: inline-block; + position: relative; + z-index: 10; +} +/* Fix profile menu */ +#profile-menu { + position: absolute; + background: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 10px; + z-index: 1000; + box-shadow: 0px 4px 8px rgba(0,0,0,0.1); + margin-top: 5px; +} +#profile-menu.hidden { + display: none; +} +#profile-menu button { + background-color: #f97316; /* Orange (Tailwind orange-500) */ + border: none; + color: white; + font-size: 16px; + text-align: left; + width: 100%; + padding: 10px; + margin-bottom: 5px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} +#profile-menu button:hover { + background-color: #ea580c; /* Darker orange on hover (Tailwind orange-600) */ +} + + +/* 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; +} + +""" def run_model(message, history): history.append({"role": "user", "content": message}) @@ -81,37 +182,76 @@ def run_model(message, history): print("Summary:", m["content"]) yield "", messages - def update_model(name): print("Model changed to:", name) - 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=""" +
+
Guest
+ +
+ """) + with gr.Column(): - with gr.Row(): - gr.Markdown(HEADER_HTML) - 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) - with gr.Row(): - 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) - -# Mount at root + 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"); + + // Toggle menu + profileBtn.onclick = () => { + profileMenu.classList.toggle("hidden"); + } + + try { + const res = await fetch('/api/login-status', { credentials: 'include' }); + const data = await res.json(); + const name = data.status.replace("Logged in: ", ""); + if (!data.status.includes("Logged out")) { + profileBtn.innerText = name; + document.querySelector("#profile-menu button[onclick*='/login']").style.display = "none"; + document.querySelector("#profile-menu button[onclick*='/logout']").style.display = "block"; + } else { + profileBtn.innerText = "Guest"; + document.querySelector("#profile-menu button[onclick*='/login']").style.display = "block"; + document.querySelector("#profile-menu button[onclick*='/logout']").style.display = "none"; + } + } catch { + profileBtn.innerText = "Login status unknown"; + } + } + """) + gr.mount_gradio_app(app, demo, path="/") # 6. Entrypoint -------------------------------------------------------- From 015066562a881d6520a3ad725a62d57eb612a347 Mon Sep 17 00:00:00 2001 From: Harshil Patel Date: Sat, 17 May 2025 21:47:26 -0700 Subject: [PATCH 3/7] Update UI drop down --- main.py | 168 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 36 deletions(-) diff --git a/main.py b/main.py index 3c09256..2722cfb 100644 --- a/main.py +++ b/main.py @@ -69,7 +69,6 @@ async def logout(request: Request): f"https://{AUTH0_DOMAIN}/v2/logout" f"?client_id={AUTH0_CLIENT_ID}" f"&returnTo=http://localhost:7860/post-logout" - f"&federated" ) return RedirectResponse(auth0_logout_url) @@ -116,49 +115,71 @@ async def api_login_status(request: Request): 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: #f97316; /* Orange */ - color: white; + 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-block; - position: relative; - z-index: 10; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 40px; + min-height: 40px; + border: 2px solid #f97316; /* Add border */ } -/* Fix profile menu */ + #profile-menu { - position: absolute; - background: transparent; + 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; - padding: 10px; - z-index: 1000; - box-shadow: 0px 4px 8px rgba(0,0,0,0.1); - margin-top: 5px; + 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 (Tailwind orange-500) */ + background-color: #f97316; /* Orange background */ border: none; - color: white; + color: white; /* White text */ font-size: 16px; + border-radius: 8px; text-align: left; width: 100%; - padding: 10px; - margin-bottom: 5px; - border-radius: 6px; + padding: 12px 16px; cursor: pointer; transition: background-color 0.2s ease; + display: block; } + #profile-menu button:hover { - background-color: #ea580c; /* Darker orange on hover (Tailwind orange-600) */ + 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 { @@ -171,6 +192,37 @@ async def api_login_status(request: Request): 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): @@ -194,10 +246,10 @@ def update_model(name): with gr.Column(scale=1, min_width=250): profile_html = gr.HTML(value="""
-
Guest
- """) @@ -227,27 +279,71 @@ def update_model(name): 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 = () => { + 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(); - const name = data.status.replace("Logged in: ", ""); + if (!data.status.includes("Logged out")) { - profileBtn.innerText = name; - document.querySelector("#profile-menu button[onclick*='/login']").style.display = "none"; - document.querySelector("#profile-menu button[onclick*='/logout']").style.display = "block"; + const name = data.status.replace("Logged in: ", ""); + profileBtn.innerHTML = `
${getInitial(name)}
`; + profileBtn.title = name; + loginBtn.style.display = "none"; + logoutBtn.style.display = "block"; } else { - profileBtn.innerText = "Guest"; - document.querySelector("#profile-menu button[onclick*='/login']").style.display = "block"; - document.querySelector("#profile-menu button[onclick*='/logout']").style.display = "none"; + profileBtn.innerHTML = `
G
`; + profileBtn.title = "Guest"; + loginBtn.style.display = "block"; + logoutBtn.style.display = "none"; } - } catch { - profileBtn.innerText = "Login status unknown"; + } catch (error) { + console.error("Error fetching login status:", error); + profileBtn.innerHTML = `
?
`; + profileBtn.title = "Login status unknown"; } } """) @@ -257,4 +353,4 @@ def update_model(name): # 6. Entrypoint -------------------------------------------------------- if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7860) + uvicorn.run(app, host="0.0.0.0", port=7860) \ No newline at end of file From 8ff1fedb0233a99a6482f0124bfb0db799d0b0a6 Mon Sep 17 00:00:00 2001 From: Harshil Patel Date: Sat, 17 May 2025 22:28:41 -0700 Subject: [PATCH 4/7] Make login mandatory --- main.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 2722cfb..ee1fbfe 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ import os, base64 from dotenv import load_dotenv -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Depends from fastapi.responses import RedirectResponse, JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware @@ -48,6 +48,20 @@ ) # 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")) @@ -236,6 +250,18 @@ def run_model(message, history): 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") @@ -348,7 +374,7 @@ def update_model(name): } """) -gr.mount_gradio_app(app, demo, path="/") +app = gr.mount_gradio_app(app, demo, path="/gradio",auth_dependency=get_user) # 6. Entrypoint -------------------------------------------------------- if __name__ == "__main__": From c74a1ceb4ef6d517900edc5ad070b04aa2e583c0 Mon Sep 17 00:00:00 2001 From: Harshil Patel Date: Sun, 18 May 2025 16:25:56 -0700 Subject: [PATCH 5/7] Move authentication to a new file --- main.py => main_auth.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename main.py => main_auth.py (100%) diff --git a/main.py b/main_auth.py similarity index 100% rename from main.py rename to main_auth.py From d06ea375af104c74985513889ca7119c2cf5e186 Mon Sep 17 00:00:00 2001 From: Harshil Patel Date: Sun, 18 May 2025 16:27:10 -0700 Subject: [PATCH 6/7] re add original main.py --- main.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..42e94e7 --- /dev/null +++ b/main.py @@ -0,0 +1,87 @@ +from typing import List +import gradio as gr + +import base64 +from src.manager.manager import GeminiManager, Mode +from enum import Enum + +_logo_bytes = open("HASHIRU_LOGO.png", "rb").read() +_logo_b64 = base64.b64encode(_logo_bytes).decode() +_header_html = f""" +
+ +

+ HASHIRU AI +

+
+""" +css = """ + .logo { margin-right: 20px; } + """ + + +def run_model(message, history): + if 'text' in message: + history.append({ + "role": "user", + "content": message['text'] + }) + if 'files' in message: + for file in message['files']: + history.append({ + "role": "user", + "content": (file,) + }) + yield "", history + for messages in model_manager.run(history): + yield "", messages + + +with gr.Blocks(css=css, fill_width=True, fill_height=True) as demo: + model_manager = GeminiManager( + gemini_model="gemini-2.0-flash", modes=[mode for mode in Mode]) + + def update_model(modeIndexes: List[int]): + modes = [Mode(i+1) for i in modeIndexes] + print(f"Selected modes: {modes}") + model_manager.set_modes(modes) + + with gr.Column(scale=1): + with gr.Row(scale=0): + gr.Markdown(_header_html) + model_dropdown = gr.Dropdown( + choices=[mode.name for mode in Mode], + value=model_manager.get_current_modes, + interactive=True, + type="index", + multiselect=True, + label="Select Modes", + ) + + model_dropdown.change( + fn=update_model, inputs=model_dropdown, outputs=[]) + with gr.Row(scale=1): + chatbot = gr.Chatbot( + avatar_images=("HASHIRU_2.png", "HASHIRU.png"), + type="messages", + show_copy_button=True, + editable="user", + scale=1, + render_markdown=True, + placeholder="Type your message here...", + ) + gr.ChatInterface(fn=run_model, + type="messages", + chatbot=chatbot, + additional_outputs=[chatbot], + save_history=True, + editable=True, + multimodal=True,) +if __name__ == "__main__": + demo.launch() \ No newline at end of file From 2eb7079f2db7775eaaa3db7fafe1161f5af8da8b Mon Sep 17 00:00:00 2001 From: Harshil Patel Date: Sun, 18 May 2025 16:54:00 -0700 Subject: [PATCH 7/7] Make authentication optional via argparse --- main.py | 345 +++++++++++++++++++++++++++++++++++++++++++++- main_auth.py | 382 --------------------------------------------------- 2 files changed, 342 insertions(+), 385 deletions(-) delete mode 100644 main_auth.py diff --git a/main.py b/main.py index 42e94e7..95bbc52 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,117 @@ import base64 from src.manager.manager import GeminiManager, Mode from enum import Enum +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 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"} + _logo_bytes = open("HASHIRU_LOGO.png", "rb").read() _logo_b64 = base64.b64encode(_logo_bytes).decode() @@ -22,8 +133,125 @@
""" css = """ - .logo { margin-right: 20px; } - """ +.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): @@ -42,6 +270,17 @@ def run_model(message, history): for messages in model_manager.run(history): yield "", messages +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( @@ -55,6 +294,18 @@ def update_model(modeIndexes: List[int]): with gr.Column(scale=1): with gr.Row(scale=0): 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( choices=[mode.name for mode in Mode], value=model_manager.get_current_modes, @@ -83,5 +334,93 @@ def update_model(modeIndexes: List[int]): save_history=True, editable=True, multimodal=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) + if __name__ == "__main__": - demo.launch() \ No newline at end of file + import uvicorn + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('--no-auth', action='store_true') + args = parser.parse_args() + no_auth = args.no_auth + + if no_auth: + demo.launch() + else: + uvicorn.run(app, host="0.0.0.0", port=7860) + \ No newline at end of file diff --git a/main_auth.py b/main_auth.py deleted file mode 100644 index ee1fbfe..0000000 --- a/main_auth.py +++ /dev/null @@ -1,382 +0,0 @@ -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) \ No newline at end of file