Spaces:
Runtime error
Implement clean OAuth login with gr.LoginButton and improve UI design
Browse files- Add hf_oauth: true to README.md for HuggingFace Spaces OAuth integration
- Replace custom token input with official gr.LoginButton for seamless authentication
- Remove cluttered UI elements: instructions dropdown, unnecessary labels, tab bar when logged out
- Hide tabs until user is authenticated, show clean login screen first
- Implement OAuth flow with proper user creation and session management
- Extract user info from OAuth request context (preferred_username, sub, email, etc.)
- Maintain backward compatibility with existing user database structure
- Clean, minimal login interface that matches modern OAuth patterns
This provides a much cleaner, professional authentication experience.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
- README.md +1 -0
- digipal/ui/gradio_interface.py +150 -44
@@ -8,6 +8,7 @@ app_port: 7860
|
|
8 |
pinned: false
|
9 |
license: mit
|
10 |
short_description: AI-powered digital pet inspired by Digimon World 1
|
|
|
11 |
---
|
12 |
|
13 |
# 🥚 DigiPal - Your AI Digital Pet
|
|
|
8 |
pinned: false
|
9 |
license: mit
|
10 |
short_description: AI-powered digital pet inspired by Digimon World 1
|
11 |
+
hf_oauth: true
|
12 |
---
|
13 |
|
14 |
# 🥚 DigiPal - Your AI Digital Pet
|
@@ -65,12 +65,12 @@ class GradioInterface:
|
|
65 |
user_state = gr.State(None)
|
66 |
token_state = gr.State(None)
|
67 |
|
68 |
-
#
|
69 |
-
with gr.
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
|
75 |
# Egg Selection Tab
|
76 |
with gr.Tab("Choose Your Egg", id="egg_tab") as egg_tab:
|
@@ -83,7 +83,7 @@ class GradioInterface:
|
|
83 |
# Event handlers
|
84 |
self._setup_event_handlers(
|
85 |
auth_components, egg_components, main_components,
|
86 |
-
user_state, token_state, main_tabs
|
87 |
)
|
88 |
|
89 |
self.app = interface
|
@@ -104,19 +104,10 @@ class GradioInterface:
|
|
104 |
with gr.Column(scale=1):
|
105 |
pass # Empty column for centering
|
106 |
|
107 |
-
with gr.Column(scale=2, elem_classes=["auth-form"]):
|
108 |
-
gr.
|
109 |
-
|
110 |
-
|
111 |
-
label="HuggingFace Token",
|
112 |
-
placeholder="Enter your HuggingFace token...",
|
113 |
-
type="password",
|
114 |
-
elem_classes=["token-input"]
|
115 |
-
)
|
116 |
-
|
117 |
-
login_btn = gr.Button(
|
118 |
-
"Login",
|
119 |
-
variant="primary",
|
120 |
elem_classes=["login-btn"]
|
121 |
)
|
122 |
|
@@ -128,24 +119,8 @@ class GradioInterface:
|
|
128 |
with gr.Column(scale=1):
|
129 |
pass # Empty column for centering
|
130 |
|
131 |
-
# Instructions
|
132 |
-
gr.HTML("""
|
133 |
-
<details class="instructions-details">
|
134 |
-
<summary>How to get a HuggingFace Token</summary>
|
135 |
-
<div class="instructions">
|
136 |
-
<ol>
|
137 |
-
<li>Go to <a href="https://huggingface.co/settings/tokens" target="_blank">HuggingFace Tokens</a></li>
|
138 |
-
<li>Click "New token"</li>
|
139 |
-
<li>Give it a name like "DigiPal"</li>
|
140 |
-
<li>Select "Read" permissions</li>
|
141 |
-
<li>Copy the token and paste it above</li>
|
142 |
-
</ol>
|
143 |
-
</div>
|
144 |
-
</details>
|
145 |
-
""")
|
146 |
|
147 |
return {
|
148 |
-
'token_input': token_input,
|
149 |
'login_btn': login_btn,
|
150 |
'auth_status': auth_status
|
151 |
}
|
@@ -414,14 +389,13 @@ class GradioInterface:
|
|
414 |
|
415 |
def _setup_event_handlers(self, auth_components: Dict, egg_components: Dict,
|
416 |
main_components: Dict, user_state: gr.State,
|
417 |
-
token_state: gr.State, main_tabs: gr.Tabs):
|
418 |
"""Set up event handlers for all interface components."""
|
419 |
|
420 |
# Authentication handlers
|
421 |
auth_components['login_btn'].click(
|
422 |
-
fn=self.
|
423 |
inputs=[
|
424 |
-
auth_components['token_input'],
|
425 |
user_state,
|
426 |
token_state
|
427 |
],
|
@@ -429,7 +403,8 @@ class GradioInterface:
|
|
429 |
auth_components['auth_status'],
|
430 |
user_state,
|
431 |
token_state,
|
432 |
-
main_tabs
|
|
|
433 |
]
|
434 |
)
|
435 |
|
@@ -591,6 +566,133 @@ class GradioInterface:
|
|
591 |
outputs=[main_components['status_info']]
|
592 |
)
|
593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
594 |
def _handle_login(self, token: str, current_user: Optional[str], current_token: Optional[str]) -> Tuple:
|
595 |
"""Handle user login with HuggingFace authentication."""
|
596 |
if not token or not token.strip():
|
@@ -598,7 +700,8 @@ class GradioInterface:
|
|
598 |
'<div class="error">Please enter a valid HuggingFace token</div>',
|
599 |
current_user,
|
600 |
current_token,
|
601 |
-
gr.update(
|
|
|
602 |
)
|
603 |
|
604 |
# Authenticate user using official HuggingFace Hub methods
|
@@ -619,7 +722,8 @@ class GradioInterface:
|
|
619 |
status_msg,
|
620 |
auth_result.user.id,
|
621 |
token.strip(),
|
622 |
-
gr.update(selected="main_tab") #
|
|
|
623 |
)
|
624 |
else:
|
625 |
# New user, go to egg selection
|
@@ -629,7 +733,8 @@ class GradioInterface:
|
|
629 |
status_msg,
|
630 |
auth_result.user.id,
|
631 |
token.strip(),
|
632 |
-
gr.update(selected="egg_tab") #
|
|
|
633 |
)
|
634 |
else:
|
635 |
error_msg = f'<div class="error">Login failed: {auth_result.error_message}</div>'
|
@@ -637,7 +742,8 @@ class GradioInterface:
|
|
637 |
error_msg,
|
638 |
current_user,
|
639 |
current_token,
|
640 |
-
gr.update(
|
|
|
641 |
)
|
642 |
|
643 |
def _handle_egg_selection(self, egg_type: EggType, user_id: Optional[str]) -> Tuple:
|
|
|
65 |
user_state = gr.State(None)
|
66 |
token_state = gr.State(None)
|
67 |
|
68 |
+
# Authentication Interface (visible by default)
|
69 |
+
with gr.Column(visible=True) as auth_container:
|
70 |
+
auth_components = self._create_authentication_tab()
|
71 |
+
|
72 |
+
# Main interface tabs (hidden until login)
|
73 |
+
with gr.Tabs(selected="egg_tab", visible=False) as main_tabs:
|
74 |
|
75 |
# Egg Selection Tab
|
76 |
with gr.Tab("Choose Your Egg", id="egg_tab") as egg_tab:
|
|
|
83 |
# Event handlers
|
84 |
self._setup_event_handlers(
|
85 |
auth_components, egg_components, main_components,
|
86 |
+
user_state, token_state, main_tabs, auth_container
|
87 |
)
|
88 |
|
89 |
self.app = interface
|
|
|
104 |
with gr.Column(scale=1):
|
105 |
pass # Empty column for centering
|
106 |
|
107 |
+
with gr.Column(scale=2, elem_classes=["auth-form"]):
|
108 |
+
login_btn = gr.LoginButton(
|
109 |
+
value="Sign in with HuggingFace",
|
110 |
+
size="lg",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
elem_classes=["login-btn"]
|
112 |
)
|
113 |
|
|
|
119 |
with gr.Column(scale=1):
|
120 |
pass # Empty column for centering
|
121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
|
123 |
return {
|
|
|
124 |
'login_btn': login_btn,
|
125 |
'auth_status': auth_status
|
126 |
}
|
|
|
389 |
|
390 |
def _setup_event_handlers(self, auth_components: Dict, egg_components: Dict,
|
391 |
main_components: Dict, user_state: gr.State,
|
392 |
+
token_state: gr.State, main_tabs: gr.Tabs, auth_container: gr.Column):
|
393 |
"""Set up event handlers for all interface components."""
|
394 |
|
395 |
# Authentication handlers
|
396 |
auth_components['login_btn'].click(
|
397 |
+
fn=self._handle_oauth_login,
|
398 |
inputs=[
|
|
|
399 |
user_state,
|
400 |
token_state
|
401 |
],
|
|
|
403 |
auth_components['auth_status'],
|
404 |
user_state,
|
405 |
token_state,
|
406 |
+
main_tabs,
|
407 |
+
auth_container
|
408 |
]
|
409 |
)
|
410 |
|
|
|
566 |
outputs=[main_components['status_info']]
|
567 |
)
|
568 |
|
569 |
+
def _handle_oauth_login(self, current_user: Optional[str], current_token: Optional[str]) -> Tuple:
|
570 |
+
"""Handle OAuth login with HuggingFace."""
|
571 |
+
try:
|
572 |
+
# Check if user is authenticated via OAuth
|
573 |
+
request = gr.request()
|
574 |
+
if hasattr(request, 'user') and request.user:
|
575 |
+
# User is authenticated via OAuth
|
576 |
+
user_info = request.user
|
577 |
+
|
578 |
+
# Extract username from OAuth info
|
579 |
+
username = user_info.get('preferred_username', user_info.get('name', 'Unknown'))
|
580 |
+
user_id = user_info.get('sub', username)
|
581 |
+
|
582 |
+
# Create or update user in database
|
583 |
+
auth_result = self._create_oauth_user(user_id, username, user_info)
|
584 |
+
|
585 |
+
if auth_result and auth_result.status == AuthStatus.SUCCESS:
|
586 |
+
self.current_user_id = auth_result.user.id
|
587 |
+
|
588 |
+
# Check if user has existing DigiPal
|
589 |
+
existing_pet = self.digipal_core.load_existing_pet(auth_result.user.id)
|
590 |
+
|
591 |
+
if existing_pet:
|
592 |
+
# User has existing pet, go to main interface
|
593 |
+
status_msg = f'<div class="success">Welcome back, {auth_result.user.username}!</div>'
|
594 |
+
|
595 |
+
return (
|
596 |
+
status_msg,
|
597 |
+
auth_result.user.id,
|
598 |
+
"oauth_authenticated",
|
599 |
+
gr.update(selected="main_tab", visible=True), # Show main tabs
|
600 |
+
gr.update(visible=False) # Hide auth container
|
601 |
+
)
|
602 |
+
else:
|
603 |
+
# New user, go to egg selection
|
604 |
+
status_msg = f'<div class="success">Welcome, {auth_result.user.username}! Choose your first egg.</div>'
|
605 |
+
|
606 |
+
return (
|
607 |
+
status_msg,
|
608 |
+
auth_result.user.id,
|
609 |
+
"oauth_authenticated",
|
610 |
+
gr.update(selected="egg_tab", visible=True), # Show tabs with egg selection
|
611 |
+
gr.update(visible=False) # Hide auth container
|
612 |
+
)
|
613 |
+
else:
|
614 |
+
return (
|
615 |
+
'<div class="error">Failed to create user account</div>',
|
616 |
+
current_user,
|
617 |
+
current_token,
|
618 |
+
gr.update(visible=False), # Keep main tabs hidden
|
619 |
+
gr.update(visible=True) # Keep auth container visible
|
620 |
+
)
|
621 |
+
else:
|
622 |
+
# User not authenticated, show login prompt
|
623 |
+
return (
|
624 |
+
'<div class="info">Please sign in with your HuggingFace account</div>',
|
625 |
+
current_user,
|
626 |
+
current_token,
|
627 |
+
gr.update(visible=False), # Keep main tabs hidden
|
628 |
+
gr.update(visible=True) # Keep auth container visible
|
629 |
+
)
|
630 |
+
|
631 |
+
except Exception as e:
|
632 |
+
logger.error(f"OAuth authentication error: {e}")
|
633 |
+
return (
|
634 |
+
'<div class="error">Authentication failed. Please try again.</div>',
|
635 |
+
current_user,
|
636 |
+
current_token,
|
637 |
+
gr.update(visible=False), # Keep main tabs hidden
|
638 |
+
gr.update(visible=True) # Keep auth container visible
|
639 |
+
)
|
640 |
+
|
641 |
+
def _create_oauth_user(self, user_id: str, username: str, oauth_info: Dict) -> Optional[AuthResult]:
|
642 |
+
"""Create or update user from OAuth information."""
|
643 |
+
try:
|
644 |
+
from ..auth.models import User, AuthResult, AuthStatus
|
645 |
+
from datetime import datetime
|
646 |
+
|
647 |
+
# Extract user data from OAuth info
|
648 |
+
email = oauth_info.get('email')
|
649 |
+
full_name = oauth_info.get('name', username)
|
650 |
+
avatar_url = oauth_info.get('picture')
|
651 |
+
|
652 |
+
# Check if user exists
|
653 |
+
existing_user = self.auth_manager.get_user(user_id)
|
654 |
+
now = datetime.now()
|
655 |
+
|
656 |
+
if existing_user:
|
657 |
+
# Update existing user
|
658 |
+
existing_user.username = username
|
659 |
+
existing_user.last_login = now
|
660 |
+
user = existing_user
|
661 |
+
else:
|
662 |
+
# Create new user
|
663 |
+
user = User(
|
664 |
+
id=user_id,
|
665 |
+
username=username,
|
666 |
+
email=email,
|
667 |
+
full_name=full_name,
|
668 |
+
avatar_url=avatar_url,
|
669 |
+
created_at=now,
|
670 |
+
last_login=now
|
671 |
+
)
|
672 |
+
|
673 |
+
# Save to database
|
674 |
+
self.auth_manager.db.execute_update(
|
675 |
+
'''INSERT OR REPLACE INTO users
|
676 |
+
(id, username, huggingface_token, created_at, last_login)
|
677 |
+
VALUES (?, ?, ?, ?, ?)''',
|
678 |
+
(user.id, user.username, "oauth_authenticated",
|
679 |
+
user.created_at.isoformat(), user.last_login.isoformat())
|
680 |
+
)
|
681 |
+
|
682 |
+
# Create session
|
683 |
+
session = self.auth_manager.session_manager.create_session(user, "oauth_authenticated")
|
684 |
+
|
685 |
+
logger.info(f"OAuth user created/updated: {username}")
|
686 |
+
return AuthResult(
|
687 |
+
status=AuthStatus.SUCCESS,
|
688 |
+
user=user,
|
689 |
+
session=session
|
690 |
+
)
|
691 |
+
|
692 |
+
except Exception as e:
|
693 |
+
logger.error(f"Error creating OAuth user: {e}")
|
694 |
+
return None
|
695 |
+
|
696 |
def _handle_login(self, token: str, current_user: Optional[str], current_token: Optional[str]) -> Tuple:
|
697 |
"""Handle user login with HuggingFace authentication."""
|
698 |
if not token or not token.strip():
|
|
|
700 |
'<div class="error">Please enter a valid HuggingFace token</div>',
|
701 |
current_user,
|
702 |
current_token,
|
703 |
+
gr.update(visible=False), # Keep main tabs hidden
|
704 |
+
gr.update(visible=True) # Keep auth container visible
|
705 |
)
|
706 |
|
707 |
# Authenticate user using official HuggingFace Hub methods
|
|
|
722 |
status_msg,
|
723 |
auth_result.user.id,
|
724 |
token.strip(),
|
725 |
+
gr.update(selected="main_tab", visible=True), # Show main tabs
|
726 |
+
gr.update(visible=False) # Hide auth container
|
727 |
)
|
728 |
else:
|
729 |
# New user, go to egg selection
|
|
|
733 |
status_msg,
|
734 |
auth_result.user.id,
|
735 |
token.strip(),
|
736 |
+
gr.update(selected="egg_tab", visible=True), # Show tabs with egg selection
|
737 |
+
gr.update(visible=False) # Hide auth container
|
738 |
)
|
739 |
else:
|
740 |
error_msg = f'<div class="error">Login failed: {auth_result.error_message}</div>'
|
|
|
742 |
error_msg,
|
743 |
current_user,
|
744 |
current_token,
|
745 |
+
gr.update(visible=False), # Keep main tabs hidden
|
746 |
+
gr.update(visible=True) # Keep auth container visible
|
747 |
)
|
748 |
|
749 |
def _handle_egg_selection(self, egg_type: EggType, user_id: Optional[str]) -> Tuple:
|