Harshil Patel commited on
Commit
4a20d19
·
1 Parent(s): ea2e255

Make authentication optional via argparse

Browse files
Files changed (2) hide show
  1. main.py +342 -3
  2. main_auth.py +0 -382
main.py CHANGED
@@ -4,6 +4,117 @@ import gradio as gr
4
  import base64
5
  from src.manager.manager import GeminiManager, Mode
6
  from enum import Enum
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  _logo_bytes = open("HASHIRU_LOGO.png", "rb").read()
9
  _logo_b64 = base64.b64encode(_logo_bytes).decode()
@@ -22,8 +133,125 @@ _header_html = f"""
22
  </div>
23
  """
24
  css = """
25
- .logo { margin-right: 20px; }
26
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
 
29
  def run_model(message, history):
@@ -42,6 +270,17 @@ def run_model(message, history):
42
  for messages in model_manager.run(history):
43
  yield "", messages
44
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  with gr.Blocks(css=css, fill_width=True, fill_height=True) as demo:
47
  model_manager = GeminiManager(
@@ -55,6 +294,18 @@ with gr.Blocks(css=css, fill_width=True, fill_height=True) as demo:
55
  with gr.Column(scale=1):
56
  with gr.Row(scale=0):
57
  gr.Markdown(_header_html)
 
 
 
 
 
 
 
 
 
 
 
 
58
  model_dropdown = gr.Dropdown(
59
  choices=[mode.name for mode in Mode],
60
  value=model_manager.get_current_modes,
@@ -83,5 +334,93 @@ with gr.Blocks(css=css, fill_width=True, fill_height=True) as demo:
83
  save_history=True,
84
  editable=True,
85
  multimodal=True,)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  if __name__ == "__main__":
87
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import base64
5
  from src.manager.manager import GeminiManager, Mode
6
  from enum import Enum
7
+ import os, base64
8
+ from dotenv import load_dotenv
9
+ from fastapi import FastAPI, Request, Depends
10
+ from fastapi.responses import RedirectResponse, JSONResponse, FileResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from starlette.middleware.sessions import SessionMiddleware
13
+ from authlib.integrations.starlette_client import OAuth
14
+ import requests
15
+ from src.manager.manager import GeminiManager
16
+
17
+ # 1. Load environment --------------------------------------------------
18
+ load_dotenv()
19
+ AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN")
20
+ AUTH0_CLIENT_ID = os.getenv("AUTH0_CLIENT_ID")
21
+ AUTH0_CLIENT_SECRET = os.getenv("AUTH0_CLIENT_SECRET")
22
+ AUTH0_AUDIENCE = os.getenv("AUTH0_AUDIENCE")
23
+ SESSION_SECRET_KEY = os.getenv("SESSION_SECRET_KEY", "replace‑me")
24
+
25
+ # 2. Auth0 client ------------------------------------------------------
26
+ oauth = OAuth()
27
+ oauth.register(
28
+ "auth0",
29
+ client_id=AUTH0_CLIENT_ID,
30
+ client_secret=AUTH0_CLIENT_SECRET,
31
+ client_kwargs={"scope": "openid profile email"},
32
+ server_metadata_url=f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration",
33
+ )
34
+
35
+ # 3. FastAPI app -------------------------------------------------------
36
+ app = FastAPI()
37
+
38
+ # Create static directory if it doesn't exist
39
+ os.makedirs("static/fonts/ui-sans-serif", exist_ok=True)
40
+ os.makedirs("static/fonts/system-ui", exist_ok=True)
41
+
42
+ # Mount static files directory
43
+ app.mount("/static", StaticFiles(directory="static"), name="static")
44
+
45
+ # Add session middleware (no auth requirement)
46
+ app.add_middleware(
47
+ SessionMiddleware,
48
+ secret_key=SESSION_SECRET_KEY,
49
+ session_cookie="session",
50
+ max_age=86400,
51
+ same_site="lax",
52
+ https_only=False
53
+ )
54
+
55
+ # 4. Auth routes -------------------------------------------------------
56
+ # Dependency to get the current user
57
+ def get_user(request: Request):
58
+ user = request.session.get('user')
59
+ if user:
60
+ return user['name']
61
+ return None
62
+
63
+ @app.get('/')
64
+ def public(request: Request, user = Depends(get_user)):
65
+ if user:
66
+ return RedirectResponse("/gradio")
67
+ else:
68
+ return RedirectResponse("/main")
69
+
70
+ @app.get("/login")
71
+ async def login(request: Request):
72
+ print("Session cookie:", request.cookies.get("session"))
73
+ print("Session data:", dict(request.session))
74
+ return await oauth.auth0.authorize_redirect(request, request.url_for("auth"), audience=AUTH0_AUDIENCE, prompt="login")
75
+
76
+ @app.get("/auth")
77
+ async def auth(request: Request):
78
+ try:
79
+ token = await oauth.auth0.authorize_access_token(request)
80
+ request.session["user"] = token["userinfo"]
81
+ return RedirectResponse("/")
82
+ except Exception as e:
83
+ return JSONResponse({"error": str(e)}, status_code=500)
84
+
85
+ @app.get("/logout")
86
+ async def logout(request: Request):
87
+ auth0_logout_url = (
88
+ f"https://{AUTH0_DOMAIN}/v2/logout"
89
+ f"?client_id={AUTH0_CLIENT_ID}"
90
+ f"&returnTo=http://localhost:7860/post-logout"
91
+ )
92
+ return RedirectResponse(auth0_logout_url)
93
+
94
+ @app.get("/post-logout")
95
+ async def post_logout(request: Request):
96
+ request.session.clear()
97
+ return RedirectResponse("/")
98
+
99
+ @app.get("/manifest.json")
100
+ async def manifest():
101
+ return JSONResponse({
102
+ "name": "HASHIRU AI",
103
+ "short_name": "HASHIRU",
104
+ "icons": [],
105
+ "start_url": "/",
106
+ "display": "standalone"
107
+ })
108
+
109
+ @app.get("/api/login-status")
110
+ async def api_login_status(request: Request):
111
+ if "user" in request.session:
112
+ user_info = request.session["user"]
113
+ user_name = user_info.get("name", user_info.get("email", "User"))
114
+ return {"status": f"Logged in: {user_name}"}
115
+ else:
116
+ return {"status": "Logged out"}
117
+
118
 
119
  _logo_bytes = open("HASHIRU_LOGO.png", "rb").read()
120
  _logo_b64 = base64.b64encode(_logo_bytes).decode()
 
133
  </div>
134
  """
135
  css = """
136
+ .logo {
137
+ margin-right: 20px;
138
+ }
139
+ .login-status {
140
+ font-weight: bold;
141
+ margin-right: 20px;
142
+ padding: 8px;
143
+ border-radius: 4px;
144
+ background-color: #f0f0f0;
145
+ }
146
+
147
+ /* Profile style improvements */
148
+ .profile-container {
149
+ position: relative;
150
+ display: inline-block;
151
+ float: right;
152
+ margin-right: 20px;
153
+ z-index: 9999; /* Ensure this is higher than any other elements */
154
+ }
155
+
156
+ #profile-name {
157
+ background-color: transparent; /* Transparent background */
158
+ color: #f97316; /* Orange text */
159
+ font-weight: bold;
160
+ padding: 10px 14px;
161
+ border-radius: 6px;
162
+ cursor: pointer;
163
+ user-select: none;
164
+ display: inline-flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ min-width: 40px;
168
+ min-height: 40px;
169
+ border: 2px solid #f97316; /* Add border */
170
+ }
171
+
172
+ #profile-menu {
173
+ position: fixed; /* Changed from absolute to fixed for better overlay */
174
+ right: auto; /* Let JS position it precisely */
175
+ top: auto; /* Let JS position it precisely */
176
+ background-color: transparent;
177
+ border: 1px solid transparent;
178
+ border-radius: 8px;
179
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
180
+ z-index: 10000; /* Very high z-index to ensure it's on top */
181
+ overflow: visible;
182
+ width: 160px;
183
+ }
184
+
185
+ #profile-menu.hidden {
186
+ display: none;
187
+ }
188
+
189
+ #profile-menu button {
190
+ background-color: #f97316; /* Orange background */
191
+ border: none;
192
+ color: white; /* White text */
193
+ font-size: 16px;
194
+ border-radius: 8px;
195
+ text-align: left;
196
+ width: 100%;
197
+ padding: 12px 16px;
198
+ cursor: pointer;
199
+ transition: background-color 0.2s ease;
200
+ display: block;
201
+ }
202
+
203
+ #profile-menu button:hover {
204
+ background-color: #ea580c; /* Darker orange on hover */
205
+ }
206
+
207
+ #profile-menu button .icon {
208
+ margin-right: 8px;
209
+ color: white; /* White icon color */
210
+ }
211
+
212
+ /* Fix dropdown issues */
213
+ input[type="text"], select {
214
+ color: black !important;
215
+ }
216
+
217
+ /* Optional: limit dropdown scroll if options are long */
218
+ .gr-dropdown .gr-dropdown-options {
219
+ max-height: 200px;
220
+ overflow-y: auto;
221
+ }
222
+
223
+ /* User avatar styles */
224
+ .user-avatar {
225
+ width: 100%;
226
+ height: 100%;
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ font-weight: bold;
231
+ text-transform: uppercase;
232
+ font-size: 20px; /* Larger font size */
233
+ color: #f97316; /* Orange color */
234
+ }
235
+
236
+ /* Fix for gradio interface */
237
+ .gradio-container {
238
+ overflow: visible !important;
239
+ }
240
+
241
+ /* Fix other container issues that might cause scrolling */
242
+ body, html {
243
+ overflow-x: hidden; /* Prevent horizontal scrolling */
244
+ }
245
+
246
+ #gradio-app, .gradio-container .overflow-hidden {
247
+ overflow: visible !important; /* Override any overflow hidden that might interfere */
248
+ }
249
+
250
+ /* Ensure dropdown appears above everything */
251
+ .profile-container * {
252
+ z-index: 9999;
253
+ }
254
+ """
255
 
256
 
257
  def run_model(message, history):
 
270
  for messages in model_manager.run(history):
271
  yield "", messages
272
 
273
+ with gr.Blocks() as login:
274
+ btn = gr.Button("Login")
275
+ _js_redirect = """
276
+ () => {
277
+ url = '/login' + window.location.search;
278
+ window.open(url, '_blank');
279
+ }
280
+ """
281
+ btn.click(None, js=_js_redirect)
282
+
283
+ app = gr.mount_gradio_app(app, login, path="/main")
284
 
285
  with gr.Blocks(css=css, fill_width=True, fill_height=True) as demo:
286
  model_manager = GeminiManager(
 
294
  with gr.Column(scale=1):
295
  with gr.Row(scale=0):
296
  gr.Markdown(_header_html)
297
+
298
+ with gr.Column(scale=1, min_width=250):
299
+ profile_html = gr.HTML(value="""
300
+ <div class="profile-container">
301
+ <div id="profile-name" class="user-avatar">G</div>
302
+ <div id="profile-menu" class="hidden">
303
+ <button id="login-btn" onclick="window.location.href='/login'"><span class="icon">🔐</span> Login</button>
304
+ <button id="logout-btn" onclick="window.location.href='/logout'"><span class="icon">🚪</span> Logout</button>
305
+ </div>
306
+ </div>
307
+ """)
308
+ with gr.Column():
309
  model_dropdown = gr.Dropdown(
310
  choices=[mode.name for mode in Mode],
311
  value=model_manager.get_current_modes,
 
334
  save_history=True,
335
  editable=True,
336
  multimodal=True,)
337
+
338
+ demo.load(None, None, None, js="""
339
+ async () => {
340
+ const profileBtn = document.getElementById("profile-name");
341
+ const profileMenu = document.getElementById("profile-menu");
342
+ const loginBtn = document.getElementById("login-btn");
343
+ const logoutBtn = document.getElementById("logout-btn");
344
+
345
+ // Position menu and handle positioning
346
+ function positionMenu() {
347
+ const btnRect = profileBtn.getBoundingClientRect();
348
+ profileMenu.style.position = "fixed";
349
+ profileMenu.style.top = (btnRect.bottom + 5) + "px";
350
+ profileMenu.style.left = (btnRect.right - profileMenu.offsetWidth) + "px"; // Align with right edge
351
+ }
352
+
353
+ // Close menu when clicking outside
354
+ document.addEventListener('click', (event) => {
355
+ if (!profileBtn.contains(event.target) && !profileMenu.contains(event.target)) {
356
+ profileMenu.classList.add("hidden");
357
+ }
358
+ });
359
+
360
+ // Toggle menu
361
+ profileBtn.onclick = (e) => {
362
+ e.stopPropagation();
363
+ positionMenu(); // Position before showing
364
+ profileMenu.classList.toggle("hidden");
365
+
366
+ // If showing menu, make sure it's positioned correctly
367
+ if (!profileMenu.classList.contains("hidden")) {
368
+ setTimeout(positionMenu, 0); // Reposition after render
369
+ }
370
+ }
371
+
372
+ // Handle window resize
373
+ window.addEventListener('resize', () => {
374
+ if (!profileMenu.classList.contains("hidden")) {
375
+ positionMenu();
376
+ }
377
+ });
378
+
379
+ // Get initial letter for avatar
380
+ function getInitial(name) {
381
+ if (name && name.length > 0) {
382
+ return name.charAt(0);
383
+ }
384
+ return "?";
385
+ }
386
+
387
+ try {
388
+ const res = await fetch('/api/login-status', { credentials: 'include' });
389
+ const data = await res.json();
390
+
391
+ if (!data.status.includes("Logged out")) {
392
+ const name = data.status.replace("Logged in: ", "");
393
+ profileBtn.innerHTML = `<div class="user-avatar">${getInitial(name)}</div>`;
394
+ profileBtn.title = name;
395
+ loginBtn.style.display = "none";
396
+ logoutBtn.style.display = "block";
397
+ } else {
398
+ profileBtn.innerHTML = `<div class="user-avatar">G</div>`;
399
+ profileBtn.title = "Guest";
400
+ loginBtn.style.display = "block";
401
+ logoutBtn.style.display = "none";
402
+ }
403
+ } catch (error) {
404
+ console.error("Error fetching login status:", error);
405
+ profileBtn.innerHTML = `<div class="user-avatar">?</div>`;
406
+ profileBtn.title = "Login status unknown";
407
+ }
408
+ }
409
+ """)
410
+
411
+ app = gr.mount_gradio_app(app, demo, path="/gradio",auth_dependency=get_user)
412
+
413
  if __name__ == "__main__":
414
+ import uvicorn
415
+ import argparse
416
+
417
+ parser = argparse.ArgumentParser()
418
+ parser.add_argument('--no-auth', action='store_true')
419
+ args = parser.parse_args()
420
+ no_auth = args.no_auth
421
+
422
+ if no_auth:
423
+ demo.launch()
424
+ else:
425
+ uvicorn.run(app, host="0.0.0.0", port=7860)
426
+
main_auth.py DELETED
@@ -1,382 +0,0 @@
1
- import os, base64
2
- from dotenv import load_dotenv
3
- from fastapi import FastAPI, Request, Depends
4
- from fastapi.responses import RedirectResponse, JSONResponse, FileResponse
5
- from fastapi.staticfiles import StaticFiles
6
- from starlette.middleware.sessions import SessionMiddleware
7
- from authlib.integrations.starlette_client import OAuth
8
- import gradio as gr
9
- import requests
10
- from src.manager.manager import GeminiManager
11
-
12
- # 1. Load environment --------------------------------------------------
13
- load_dotenv()
14
- AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN")
15
- AUTH0_CLIENT_ID = os.getenv("AUTH0_CLIENT_ID")
16
- AUTH0_CLIENT_SECRET = os.getenv("AUTH0_CLIENT_SECRET")
17
- AUTH0_AUDIENCE = os.getenv("AUTH0_AUDIENCE")
18
- SESSION_SECRET_KEY = os.getenv("SESSION_SECRET_KEY", "replace‑me")
19
-
20
- # 2. Auth0 client ------------------------------------------------------
21
- oauth = OAuth()
22
- oauth.register(
23
- "auth0",
24
- client_id=AUTH0_CLIENT_ID,
25
- client_secret=AUTH0_CLIENT_SECRET,
26
- client_kwargs={"scope": "openid profile email"},
27
- server_metadata_url=f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration",
28
- )
29
-
30
- # 3. FastAPI app -------------------------------------------------------
31
- app = FastAPI()
32
-
33
- # Create static directory if it doesn't exist
34
- os.makedirs("static/fonts/ui-sans-serif", exist_ok=True)
35
- os.makedirs("static/fonts/system-ui", exist_ok=True)
36
-
37
- # Mount static files directory
38
- app.mount("/static", StaticFiles(directory="static"), name="static")
39
-
40
- # Add session middleware (no auth requirement)
41
- app.add_middleware(
42
- SessionMiddleware,
43
- secret_key=SESSION_SECRET_KEY,
44
- session_cookie="session",
45
- max_age=86400,
46
- same_site="lax",
47
- https_only=False
48
- )
49
-
50
- # 4. Auth routes -------------------------------------------------------
51
- # Dependency to get the current user
52
- def get_user(request: Request):
53
- user = request.session.get('user')
54
- if user:
55
- return user['name']
56
- return None
57
-
58
- @app.get('/')
59
- def public(request: Request, user = Depends(get_user)):
60
- if user:
61
- return RedirectResponse("/gradio")
62
- else:
63
- return RedirectResponse("/main")
64
-
65
- @app.get("/login")
66
- async def login(request: Request):
67
- print("Session cookie:", request.cookies.get("session"))
68
- print("Session data:", dict(request.session))
69
- return await oauth.auth0.authorize_redirect(request, request.url_for("auth"), audience=AUTH0_AUDIENCE, prompt="login")
70
-
71
- @app.get("/auth")
72
- async def auth(request: Request):
73
- try:
74
- token = await oauth.auth0.authorize_access_token(request)
75
- request.session["user"] = token["userinfo"]
76
- return RedirectResponse("/")
77
- except Exception as e:
78
- return JSONResponse({"error": str(e)}, status_code=500)
79
-
80
- @app.get("/logout")
81
- async def logout(request: Request):
82
- auth0_logout_url = (
83
- f"https://{AUTH0_DOMAIN}/v2/logout"
84
- f"?client_id={AUTH0_CLIENT_ID}"
85
- f"&returnTo=http://localhost:7860/post-logout"
86
- )
87
- return RedirectResponse(auth0_logout_url)
88
-
89
- @app.get("/post-logout")
90
- async def post_logout(request: Request):
91
- request.session.clear()
92
- return RedirectResponse("/")
93
-
94
- @app.get("/manifest.json")
95
- async def manifest():
96
- return JSONResponse({
97
- "name": "HASHIRU AI",
98
- "short_name": "HASHIRU",
99
- "icons": [],
100
- "start_url": "/",
101
- "display": "standalone"
102
- })
103
-
104
- @app.get("/api/login-status")
105
- async def api_login_status(request: Request):
106
- if "user" in request.session:
107
- user_info = request.session["user"]
108
- user_name = user_info.get("name", user_info.get("email", "User"))
109
- return {"status": f"Logged in: {user_name}"}
110
- else:
111
- return {"status": "Logged out"}
112
-
113
- # 5. Gradio UI ---------------------------------------------------------
114
- _logo_b64 = base64.b64encode(open("HASHIRU_LOGO.png", "rb").read()).decode()
115
- HEADER_HTML = f"""
116
- <div style='display:flex;align-items:center;width:30%;'>
117
- <img src='data:image/png;base64,{_logo_b64}' width='40' class='logo'/>
118
- <h1>HASHIRU AI</h1>
119
- </div>"""
120
-
121
- CSS = """
122
- .logo {
123
- margin-right: 20px;
124
- }
125
- .login-status {
126
- font-weight: bold;
127
- margin-right: 20px;
128
- padding: 8px;
129
- border-radius: 4px;
130
- background-color: #f0f0f0;
131
- }
132
-
133
- /* Profile style improvements */
134
- .profile-container {
135
- position: relative;
136
- display: inline-block;
137
- float: right;
138
- margin-right: 20px;
139
- z-index: 9999; /* Ensure this is higher than any other elements */
140
- }
141
-
142
- #profile-name {
143
- background-color: transparent; /* Transparent background */
144
- color: #f97316; /* Orange text */
145
- font-weight: bold;
146
- padding: 10px 14px;
147
- border-radius: 6px;
148
- cursor: pointer;
149
- user-select: none;
150
- display: inline-flex;
151
- align-items: center;
152
- justify-content: center;
153
- min-width: 40px;
154
- min-height: 40px;
155
- border: 2px solid #f97316; /* Add border */
156
- }
157
-
158
- #profile-menu {
159
- position: fixed; /* Changed from absolute to fixed for better overlay */
160
- right: auto; /* Let JS position it precisely */
161
- top: auto; /* Let JS position it precisely */
162
- background-color: transparent;
163
- border: 1px solid transparent;
164
- border-radius: 8px;
165
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
166
- z-index: 10000; /* Very high z-index to ensure it's on top */
167
- overflow: visible;
168
- width: 160px;
169
- }
170
-
171
- #profile-menu.hidden {
172
- display: none;
173
- }
174
-
175
- #profile-menu button {
176
- background-color: #f97316; /* Orange background */
177
- border: none;
178
- color: white; /* White text */
179
- font-size: 16px;
180
- border-radius: 8px;
181
- text-align: left;
182
- width: 100%;
183
- padding: 12px 16px;
184
- cursor: pointer;
185
- transition: background-color 0.2s ease;
186
- display: block;
187
- }
188
-
189
- #profile-menu button:hover {
190
- background-color: #ea580c; /* Darker orange on hover */
191
- }
192
-
193
- #profile-menu button .icon {
194
- margin-right: 8px;
195
- color: white; /* White icon color */
196
- }
197
-
198
- /* Fix dropdown issues */
199
- input[type="text"], select {
200
- color: black !important;
201
- }
202
-
203
- /* Optional: limit dropdown scroll if options are long */
204
- .gr-dropdown .gr-dropdown-options {
205
- max-height: 200px;
206
- overflow-y: auto;
207
- }
208
-
209
- /* User avatar styles */
210
- .user-avatar {
211
- width: 100%;
212
- height: 100%;
213
- display: flex;
214
- align-items: center;
215
- justify-content: center;
216
- font-weight: bold;
217
- text-transform: uppercase;
218
- font-size: 20px; /* Larger font size */
219
- color: #f97316; /* Orange color */
220
- }
221
-
222
- /* Fix for gradio interface */
223
- .gradio-container {
224
- overflow: visible !important;
225
- }
226
-
227
- /* Fix other container issues that might cause scrolling */
228
- body, html {
229
- overflow-x: hidden; /* Prevent horizontal scrolling */
230
- }
231
-
232
- #gradio-app, .gradio-container .overflow-hidden {
233
- overflow: visible !important; /* Override any overflow hidden that might interfere */
234
- }
235
-
236
- /* Ensure dropdown appears above everything */
237
- .profile-container * {
238
- z-index: 9999;
239
- }
240
- """
241
-
242
- def run_model(message, history):
243
- history.append({"role": "user", "content": message})
244
- yield "", history
245
- for messages in model_manager.run(history):
246
- for m in messages:
247
- if m.get("role") == "summary":
248
- print("Summary:", m["content"])
249
- yield "", messages
250
-
251
- def update_model(name):
252
- print("Model changed to:", name)
253
-
254
- with gr.Blocks() as login:
255
- btn = gr.Button("Login")
256
- _js_redirect = """
257
- () => {
258
- url = '/login' + window.location.search;
259
- window.open(url, '_blank');
260
- }
261
- """
262
- btn.click(None, js=_js_redirect)
263
-
264
- app = gr.mount_gradio_app(app, login, path="/main")
265
-
266
- with gr.Blocks(css=CSS, fill_width=True, fill_height=True) as demo:
267
- model_manager = GeminiManager(gemini_model="gemini-2.0-flash")
268
-
269
- with gr.Row():
270
- gr.Markdown(HEADER_HTML)
271
-
272
- with gr.Column(scale=1, min_width=250):
273
- profile_html = gr.HTML(value="""
274
- <div class="profile-container">
275
- <div id="profile-name" class="user-avatar">G</div>
276
- <div id="profile-menu" class="hidden">
277
- <button id="login-btn" onclick="window.location.href='/login'"><span class="icon">🔐</span> Login</button>
278
- <button id="logout-btn" onclick="window.location.href='/logout'"><span class="icon">🚪</span> Logout</button>
279
- </div>
280
- </div>
281
- """)
282
-
283
- with gr.Column():
284
- model_dropdown = gr.Dropdown(
285
- [
286
- "HASHIRU",
287
- "Static-HASHIRU",
288
- "Cloud-Only HASHIRU",
289
- "Local-Only HASHIRU",
290
- "No-Economy HASHIRU",
291
- ],
292
- value="HASHIRU",
293
- interactive=True,
294
- )
295
- model_dropdown.change(update_model, model_dropdown)
296
-
297
- chatbot = gr.Chatbot(
298
- avatar_images=("HASHIRU_2.png", "HASHIRU.png"),
299
- type="messages", show_copy_button=True, editable="user",
300
- placeholder="Type your message here…",
301
- )
302
- gr.ChatInterface(run_model, type="messages", chatbot=chatbot, additional_outputs=[chatbot], save_history=True)
303
-
304
- demo.load(None, None, None, js="""
305
- async () => {
306
- const profileBtn = document.getElementById("profile-name");
307
- const profileMenu = document.getElementById("profile-menu");
308
- const loginBtn = document.getElementById("login-btn");
309
- const logoutBtn = document.getElementById("logout-btn");
310
-
311
- // Position menu and handle positioning
312
- function positionMenu() {
313
- const btnRect = profileBtn.getBoundingClientRect();
314
- profileMenu.style.position = "fixed";
315
- profileMenu.style.top = (btnRect.bottom + 5) + "px";
316
- profileMenu.style.left = (btnRect.right - profileMenu.offsetWidth) + "px"; // Align with right edge
317
- }
318
-
319
- // Close menu when clicking outside
320
- document.addEventListener('click', (event) => {
321
- if (!profileBtn.contains(event.target) && !profileMenu.contains(event.target)) {
322
- profileMenu.classList.add("hidden");
323
- }
324
- });
325
-
326
- // Toggle menu
327
- profileBtn.onclick = (e) => {
328
- e.stopPropagation();
329
- positionMenu(); // Position before showing
330
- profileMenu.classList.toggle("hidden");
331
-
332
- // If showing menu, make sure it's positioned correctly
333
- if (!profileMenu.classList.contains("hidden")) {
334
- setTimeout(positionMenu, 0); // Reposition after render
335
- }
336
- }
337
-
338
- // Handle window resize
339
- window.addEventListener('resize', () => {
340
- if (!profileMenu.classList.contains("hidden")) {
341
- positionMenu();
342
- }
343
- });
344
-
345
- // Get initial letter for avatar
346
- function getInitial(name) {
347
- if (name && name.length > 0) {
348
- return name.charAt(0);
349
- }
350
- return "?";
351
- }
352
-
353
- try {
354
- const res = await fetch('/api/login-status', { credentials: 'include' });
355
- const data = await res.json();
356
-
357
- if (!data.status.includes("Logged out")) {
358
- const name = data.status.replace("Logged in: ", "");
359
- profileBtn.innerHTML = `<div class="user-avatar">${getInitial(name)}</div>`;
360
- profileBtn.title = name;
361
- loginBtn.style.display = "none";
362
- logoutBtn.style.display = "block";
363
- } else {
364
- profileBtn.innerHTML = `<div class="user-avatar">G</div>`;
365
- profileBtn.title = "Guest";
366
- loginBtn.style.display = "block";
367
- logoutBtn.style.display = "none";
368
- }
369
- } catch (error) {
370
- console.error("Error fetching login status:", error);
371
- profileBtn.innerHTML = `<div class="user-avatar">?</div>`;
372
- profileBtn.title = "Login status unknown";
373
- }
374
- }
375
- """)
376
-
377
- app = gr.mount_gradio_app(app, demo, path="/gradio",auth_dependency=get_user)
378
-
379
- # 6. Entrypoint --------------------------------------------------------
380
- if __name__ == "__main__":
381
- import uvicorn
382
- uvicorn.run(app, host="0.0.0.0", port=7860)