Spaces:
Running
Implement popup login modal system
Browse files✨ Major UX Improvement:
- Replace full-page login with overlay modal
- Users can now see the platform in background before login
- Created AuthContext for centralized auth state management
- Added LoginModal component with embedded video and paper
- Modified API to trigger login modal on 401 errors
- Removed forced auth redirect from root route
🎨 Design Features:
- Glass-morphism modal design consistent with platform
- Embedded demo video and research paper download
- Responsive layout with proper backdrop blur
- Smooth loading states and transitions
🔧 Technical Changes:
- New AuthContext with HF Spaces detection
- Custom auth-required events from API layer
- Conditional authentication based on environment
- Proper error handling for unauthorized requests
This provides a much better user experience where users can
explore the interface before deciding to authenticate.
- backend/app.py +4 -4
- backend/routers/knowledge_graphs.py +1 -1
- backend/templates/login.html +25 -6
- frontend/src/App.tsx +32 -11
- frontend/src/components/auth/LoginModal.tsx +136 -0
- frontend/src/context/AuthContext.tsx +158 -0
- frontend/src/lib/api.ts +12 -0
|
@@ -177,11 +177,11 @@ async def shutdown_event():
|
|
| 177 |
# scheduler_service.stop() # This line is now commented out
|
| 178 |
|
| 179 |
|
| 180 |
-
# Root redirect to React app (
|
| 181 |
@app.get("/")
|
| 182 |
-
async def root(request: Request
|
| 183 |
-
#
|
| 184 |
-
#
|
| 185 |
return RedirectResponse(url="/agentgraph")
|
| 186 |
|
| 187 |
|
|
|
|
| 177 |
# scheduler_service.stop() # This line is now commented out
|
| 178 |
|
| 179 |
|
| 180 |
+
# Root redirect to React app (no authentication required - frontend handles login modal)
|
| 181 |
@app.get("/")
|
| 182 |
+
async def root(request: Request):
|
| 183 |
+
# Allow all users to access the main interface
|
| 184 |
+
# Authentication will be handled by frontend login modal
|
| 185 |
return RedirectResponse(url="/agentgraph")
|
| 186 |
|
| 187 |
|
|
@@ -23,7 +23,7 @@ import math
|
|
| 23 |
# Add the project root to the Python path for proper imports
|
| 24 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 25 |
|
| 26 |
-
from backend.dependencies import get_db_session
|
| 27 |
from backend.services import KnowledgeGraphService
|
| 28 |
from backend.models import KnowledgeGraphResponse, PlatformStatsResponse
|
| 29 |
from backend.database import get_db
|
|
|
|
| 23 |
# Add the project root to the Python path for proper imports
|
| 24 |
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 25 |
|
| 26 |
+
from backend.dependencies import get_db_session, get_current_user_optional
|
| 27 |
from backend.services import KnowledgeGraphService
|
| 28 |
from backend.models import KnowledgeGraphResponse, PlatformStatsResponse
|
| 29 |
from backend.database import get_db
|
|
@@ -210,9 +210,17 @@
|
|
| 210 |
<div class="glass-card p-6 rounded-2xl">
|
| 211 |
<div class="flex items-center space-x-4">
|
| 212 |
<div class="flex-shrink-0">
|
| 213 |
-
<svg
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
</svg>
|
| 217 |
</div>
|
| 218 |
<div class="flex-1">
|
|
@@ -220,15 +228,26 @@
|
|
| 220 |
Research Paper
|
| 221 |
</h3>
|
| 222 |
<p class="text-sm text-muted-foreground mb-3">
|
| 223 |
-
AgentGraph: Trace-to-Graph Platform for Interactive Analysis
|
|
|
|
| 224 |
</p>
|
| 225 |
<a
|
| 226 |
href="/static/papers/agentgraph_paper.pdf"
|
| 227 |
target="_blank"
|
| 228 |
class="inline-flex items-center text-primary hover:text-primary/80 transition-colors"
|
| 229 |
>
|
| 230 |
-
<svg
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
</svg>
|
| 233 |
Download PDF
|
| 234 |
</a>
|
|
|
|
| 210 |
<div class="glass-card p-6 rounded-2xl">
|
| 211 |
<div class="flex items-center space-x-4">
|
| 212 |
<div class="flex-shrink-0">
|
| 213 |
+
<svg
|
| 214 |
+
class="w-8 h-8 text-primary"
|
| 215 |
+
fill="currentColor"
|
| 216 |
+
viewBox="0 0 20 20"
|
| 217 |
+
>
|
| 218 |
+
<path
|
| 219 |
+
fill-rule="evenodd"
|
| 220 |
+
d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6z"
|
| 221 |
+
clip-rule="evenodd"
|
| 222 |
+
/>
|
| 223 |
+
<path d="M8 6h4v1H8V6zM8 8h4v1H8V8zM8 10h2v1H8v-1z" />
|
| 224 |
</svg>
|
| 225 |
</div>
|
| 226 |
<div class="flex-1">
|
|
|
|
| 228 |
Research Paper
|
| 229 |
</h3>
|
| 230 |
<p class="text-sm text-muted-foreground mb-3">
|
| 231 |
+
AgentGraph: Trace-to-Graph Platform for Interactive Analysis
|
| 232 |
+
and Robustness Testing in Agentic AI Systems
|
| 233 |
</p>
|
| 234 |
<a
|
| 235 |
href="/static/papers/agentgraph_paper.pdf"
|
| 236 |
target="_blank"
|
| 237 |
class="inline-flex items-center text-primary hover:text-primary/80 transition-colors"
|
| 238 |
>
|
| 239 |
+
<svg
|
| 240 |
+
class="w-4 h-4 mr-2"
|
| 241 |
+
fill="none"
|
| 242 |
+
stroke="currentColor"
|
| 243 |
+
viewBox="0 0 24 24"
|
| 244 |
+
>
|
| 245 |
+
<path
|
| 246 |
+
stroke-linecap="round"
|
| 247 |
+
stroke-linejoin="round"
|
| 248 |
+
stroke-width="2"
|
| 249 |
+
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
| 250 |
+
/>
|
| 251 |
</svg>
|
| 252 |
Download PDF
|
| 253 |
</a>
|
|
@@ -5,14 +5,29 @@ import { NotificationProvider } from "./context/NotificationContext";
|
|
| 5 |
import { ModalProvider, useModal } from "./context/ModalContext";
|
| 6 |
import { NavigationProvider } from "./context/NavigationContext";
|
| 7 |
import { KGDisplayModeProvider } from "./context/KGDisplayModeContext";
|
|
|
|
| 8 |
import { MainLayout } from "./components/layout/MainLayout";
|
| 9 |
import { ModalSystem } from "./components/shared/ModalSystem";
|
|
|
|
| 10 |
import { Toaster } from "./components/ui/toaster";
|
| 11 |
import { ErrorBoundary } from "./components/shared/ErrorBoundary";
|
| 12 |
import "./styles/globals.css";
|
| 13 |
|
| 14 |
function AppContent() {
|
| 15 |
const { modalState, closeModal } = useModal();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
return (
|
| 18 |
<div className="h-screen bg-background text-foreground flex flex-col">
|
|
@@ -21,6 +36,10 @@ function AppContent() {
|
|
| 21 |
</ErrorBoundary>
|
| 22 |
<Toaster />
|
| 23 |
<ModalSystem modalState={modalState} onClose={closeModal} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
</div>
|
| 25 |
);
|
| 26 |
}
|
|
@@ -29,17 +48,19 @@ function App() {
|
|
| 29 |
return (
|
| 30 |
<ErrorBoundary>
|
| 31 |
<ThemeProvider>
|
| 32 |
-
<
|
| 33 |
-
<
|
| 34 |
-
<
|
| 35 |
-
<
|
| 36 |
-
<
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
| 43 |
</ThemeProvider>
|
| 44 |
</ErrorBoundary>
|
| 45 |
);
|
|
|
|
| 5 |
import { ModalProvider, useModal } from "./context/ModalContext";
|
| 6 |
import { NavigationProvider } from "./context/NavigationContext";
|
| 7 |
import { KGDisplayModeProvider } from "./context/KGDisplayModeContext";
|
| 8 |
+
import { AuthProvider, useAuth } from "./context/AuthContext";
|
| 9 |
import { MainLayout } from "./components/layout/MainLayout";
|
| 10 |
import { ModalSystem } from "./components/shared/ModalSystem";
|
| 11 |
+
import { LoginModal } from "./components/auth/LoginModal";
|
| 12 |
import { Toaster } from "./components/ui/toaster";
|
| 13 |
import { ErrorBoundary } from "./components/shared/ErrorBoundary";
|
| 14 |
import "./styles/globals.css";
|
| 15 |
|
| 16 |
function AppContent() {
|
| 17 |
const { modalState, closeModal } = useModal();
|
| 18 |
+
const { showLoginModal, setShowLoginModal, isLoading } = useAuth();
|
| 19 |
+
|
| 20 |
+
// Show loading screen while checking auth status
|
| 21 |
+
if (isLoading) {
|
| 22 |
+
return (
|
| 23 |
+
<div className="h-screen bg-background text-foreground flex items-center justify-center">
|
| 24 |
+
<div className="text-center">
|
| 25 |
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
| 26 |
+
<p className="text-muted-foreground">Loading AgentGraph...</p>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
}
|
| 31 |
|
| 32 |
return (
|
| 33 |
<div className="h-screen bg-background text-foreground flex flex-col">
|
|
|
|
| 36 |
</ErrorBoundary>
|
| 37 |
<Toaster />
|
| 38 |
<ModalSystem modalState={modalState} onClose={closeModal} />
|
| 39 |
+
<LoginModal
|
| 40 |
+
isOpen={showLoginModal}
|
| 41 |
+
onClose={() => setShowLoginModal(false)}
|
| 42 |
+
/>
|
| 43 |
</div>
|
| 44 |
);
|
| 45 |
}
|
|
|
|
| 48 |
return (
|
| 49 |
<ErrorBoundary>
|
| 50 |
<ThemeProvider>
|
| 51 |
+
<AuthProvider>
|
| 52 |
+
<NotificationProvider>
|
| 53 |
+
<NavigationProvider>
|
| 54 |
+
<ModalProvider>
|
| 55 |
+
<KGDisplayModeProvider>
|
| 56 |
+
<AgentGraphProvider>
|
| 57 |
+
<AppContent />
|
| 58 |
+
</AgentGraphProvider>
|
| 59 |
+
</KGDisplayModeProvider>
|
| 60 |
+
</ModalProvider>
|
| 61 |
+
</NavigationProvider>
|
| 62 |
+
</NotificationProvider>
|
| 63 |
+
</AuthProvider>
|
| 64 |
</ThemeProvider>
|
| 65 |
</ErrorBoundary>
|
| 66 |
);
|
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import { X, FileText, ExternalLink } from "lucide-react";
|
| 3 |
+
import { useAuth } from "../../context/AuthContext";
|
| 4 |
+
|
| 5 |
+
interface LoginModalProps {
|
| 6 |
+
isOpen: boolean;
|
| 7 |
+
onClose: () => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
| 11 |
+
const { login, isHFSpaces } = useAuth();
|
| 12 |
+
|
| 13 |
+
if (!isOpen) return null;
|
| 14 |
+
|
| 15 |
+
const handleLogin = () => {
|
| 16 |
+
login();
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const handleOverlayClick = (e: React.MouseEvent) => {
|
| 20 |
+
if (e.target === e.currentTarget) {
|
| 21 |
+
onClose();
|
| 22 |
+
}
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div
|
| 27 |
+
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
| 28 |
+
onClick={handleOverlayClick}
|
| 29 |
+
>
|
| 30 |
+
<div className="bg-card border border-border rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
|
| 31 |
+
{/* Header */}
|
| 32 |
+
<div className="flex justify-between items-center p-6 border-b border-border">
|
| 33 |
+
<h2 className="text-2xl font-bold text-foreground">
|
| 34 |
+
Welcome to AgentGraph
|
| 35 |
+
</h2>
|
| 36 |
+
<button
|
| 37 |
+
onClick={onClose}
|
| 38 |
+
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
| 39 |
+
>
|
| 40 |
+
<X className="w-5 h-5" />
|
| 41 |
+
</button>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div className="grid lg:grid-cols-2 gap-8 p-6">
|
| 45 |
+
{/* Content Section */}
|
| 46 |
+
<div className="space-y-6">
|
| 47 |
+
{/* Title */}
|
| 48 |
+
<div className="space-y-4">
|
| 49 |
+
<h1 className="text-3xl lg:text-4xl font-bold bg-gradient-to-r from-foreground to-primary bg-clip-text text-transparent">
|
| 50 |
+
AgentGraph
|
| 51 |
+
</h1>
|
| 52 |
+
<p className="text-lg text-muted-foreground">
|
| 53 |
+
Trace-to-Graph Platform for Interactive Analysis and Robustness
|
| 54 |
+
Testing in Agentic AI Systems
|
| 55 |
+
</p>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
{/* Description */}
|
| 59 |
+
<p className="text-muted-foreground">
|
| 60 |
+
Convert execution logs into interactive knowledge graphs with
|
| 61 |
+
actionable insights for AI system analysis and robustness testing.
|
| 62 |
+
</p>
|
| 63 |
+
|
| 64 |
+
{/* CTA Section */}
|
| 65 |
+
<div className="space-y-4">
|
| 66 |
+
<button
|
| 67 |
+
onClick={handleLogin}
|
| 68 |
+
className="w-full bg-primary hover:bg-primary/90 text-primary-foreground font-semibold py-3 px-6 rounded-lg transition-colors flex items-center justify-center gap-2"
|
| 69 |
+
>
|
| 70 |
+
{isHFSpaces ? (
|
| 71 |
+
<>
|
| 72 |
+
Login with Hugging Face
|
| 73 |
+
<ExternalLink className="w-4 h-4" />
|
| 74 |
+
</>
|
| 75 |
+
) : (
|
| 76 |
+
"Continue to Platform"
|
| 77 |
+
)}
|
| 78 |
+
</button>
|
| 79 |
+
|
| 80 |
+
<div className="bg-muted/30 p-4 rounded-lg">
|
| 81 |
+
<p className="text-sm text-muted-foreground">
|
| 82 |
+
{isHFSpaces
|
| 83 |
+
? "Authentication required for responsible AI resource usage"
|
| 84 |
+
: "Local development mode - no authentication required"}
|
| 85 |
+
</p>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
{/* Video and Paper Section */}
|
| 91 |
+
<div className="space-y-6">
|
| 92 |
+
{/* Demo Video */}
|
| 93 |
+
<div className="bg-muted/20 rounded-xl overflow-hidden">
|
| 94 |
+
<div className="relative aspect-video">
|
| 95 |
+
<iframe
|
| 96 |
+
src="https://www.youtube.com/embed/btrS9pfDYJY?si=dDX4tIs-oS2O2d2p"
|
| 97 |
+
title="AgentGraph: Interactive Analysis Platform Demo"
|
| 98 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
| 99 |
+
allowFullScreen
|
| 100 |
+
className="w-full h-full rounded-xl"
|
| 101 |
+
/>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
{/* Research Paper */}
|
| 106 |
+
<div className="bg-muted/20 p-4 rounded-xl">
|
| 107 |
+
<div className="flex items-center space-x-3">
|
| 108 |
+
<div className="flex-shrink-0">
|
| 109 |
+
<FileText className="w-6 h-6 text-primary" />
|
| 110 |
+
</div>
|
| 111 |
+
<div className="flex-1">
|
| 112 |
+
<h3 className="font-semibold text-foreground mb-1">
|
| 113 |
+
Research Paper
|
| 114 |
+
</h3>
|
| 115 |
+
<p className="text-sm text-muted-foreground mb-2">
|
| 116 |
+
AgentGraph: Trace-to-Graph Platform for Interactive Analysis
|
| 117 |
+
and Robustness Testing in Agentic AI Systems
|
| 118 |
+
</p>
|
| 119 |
+
<a
|
| 120 |
+
href="/static/papers/agentgraph_paper.pdf"
|
| 121 |
+
target="_blank"
|
| 122 |
+
rel="noopener noreferrer"
|
| 123 |
+
className="inline-flex items-center text-primary hover:text-primary/80 transition-colors text-sm"
|
| 124 |
+
>
|
| 125 |
+
<FileText className="w-4 h-4 mr-1" />
|
| 126 |
+
Download PDF
|
| 127 |
+
</a>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
);
|
| 136 |
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, {
|
| 2 |
+
createContext,
|
| 3 |
+
useContext,
|
| 4 |
+
useState,
|
| 5 |
+
useEffect,
|
| 6 |
+
ReactNode,
|
| 7 |
+
} from "react";
|
| 8 |
+
|
| 9 |
+
export interface User {
|
| 10 |
+
id: string;
|
| 11 |
+
username: string;
|
| 12 |
+
name: string;
|
| 13 |
+
email?: string;
|
| 14 |
+
avatar_url?: string;
|
| 15 |
+
auth_method: string;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface AuthState {
|
| 19 |
+
user: User | null;
|
| 20 |
+
isAuthenticated: boolean;
|
| 21 |
+
isLoading: boolean;
|
| 22 |
+
isHFSpaces: boolean;
|
| 23 |
+
authRequired: boolean;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
interface AuthContextType extends AuthState {
|
| 27 |
+
login: () => void;
|
| 28 |
+
logout: () => void;
|
| 29 |
+
checkAuthStatus: () => Promise<void>;
|
| 30 |
+
showLoginModal: boolean;
|
| 31 |
+
setShowLoginModal: (show: boolean) => void;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
| 35 |
+
|
| 36 |
+
export function useAuth() {
|
| 37 |
+
const context = useContext(AuthContext);
|
| 38 |
+
if (context === undefined) {
|
| 39 |
+
throw new Error("useAuth must be used within an AuthProvider");
|
| 40 |
+
}
|
| 41 |
+
return context;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
interface AuthProviderProps {
|
| 45 |
+
children: ReactNode;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function AuthProvider({ children }: AuthProviderProps) {
|
| 49 |
+
const [user, setUser] = useState<User | null>(null);
|
| 50 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 51 |
+
const [isHFSpaces, setIsHFSpaces] = useState(false);
|
| 52 |
+
const [authRequired, setAuthRequired] = useState(false);
|
| 53 |
+
const [showLoginModal, setShowLoginModal] = useState(false);
|
| 54 |
+
|
| 55 |
+
const isAuthenticated = user !== null;
|
| 56 |
+
|
| 57 |
+
const checkAuthStatus = async () => {
|
| 58 |
+
try {
|
| 59 |
+
setIsLoading(true);
|
| 60 |
+
const response = await fetch("/auth/status");
|
| 61 |
+
const data = await response.json();
|
| 62 |
+
|
| 63 |
+
setIsHFSpaces(data.environment === "huggingface_spaces");
|
| 64 |
+
setAuthRequired(
|
| 65 |
+
data.login_required && data.environment === "huggingface_spaces"
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
if (data.user_authenticated && data.user_info) {
|
| 69 |
+
setUser({
|
| 70 |
+
id: data.user_info.user_id || "unknown",
|
| 71 |
+
username: data.user_info.username || "Unknown User",
|
| 72 |
+
name:
|
| 73 |
+
data.user_info.name || data.user_info.username || "Unknown User",
|
| 74 |
+
email: data.user_info.email,
|
| 75 |
+
avatar_url: data.user_info.avatar_url,
|
| 76 |
+
auth_method: data.user_info.auth_method || "unknown",
|
| 77 |
+
});
|
| 78 |
+
setShowLoginModal(false);
|
| 79 |
+
} else {
|
| 80 |
+
setUser(null);
|
| 81 |
+
// Show login modal if in HF Spaces and auth is required
|
| 82 |
+
if (data.environment === "huggingface_spaces" && data.login_required) {
|
| 83 |
+
setShowLoginModal(true);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
} catch (error) {
|
| 87 |
+
console.error("Failed to check auth status:", error);
|
| 88 |
+
setUser(null);
|
| 89 |
+
// Assume local development if fetch fails
|
| 90 |
+
setIsHFSpaces(false);
|
| 91 |
+
setAuthRequired(false);
|
| 92 |
+
} finally {
|
| 93 |
+
setIsLoading(false);
|
| 94 |
+
}
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const login = () => {
|
| 98 |
+
if (isHFSpaces) {
|
| 99 |
+
// For HF Spaces, redirect to OAuth login
|
| 100 |
+
window.location.href = "/auth/login";
|
| 101 |
+
} else {
|
| 102 |
+
// For local development, just close the modal
|
| 103 |
+
setShowLoginModal(false);
|
| 104 |
+
setUser({
|
| 105 |
+
id: "local_dev",
|
| 106 |
+
username: "local_user",
|
| 107 |
+
name: "Local Development User",
|
| 108 |
+
auth_method: "local_dev",
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
const logout = async () => {
|
| 114 |
+
try {
|
| 115 |
+
if (isHFSpaces) {
|
| 116 |
+
await fetch("/auth/logout");
|
| 117 |
+
window.location.reload();
|
| 118 |
+
} else {
|
| 119 |
+
setUser(null);
|
| 120 |
+
setShowLoginModal(false);
|
| 121 |
+
}
|
| 122 |
+
} catch (error) {
|
| 123 |
+
console.error("Failed to logout:", error);
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
useEffect(() => {
|
| 128 |
+
checkAuthStatus();
|
| 129 |
+
|
| 130 |
+
// Listen for auth-required events from API calls
|
| 131 |
+
const handleAuthRequired = () => {
|
| 132 |
+
if (authRequired && !isAuthenticated) {
|
| 133 |
+
setShowLoginModal(true);
|
| 134 |
+
}
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
window.addEventListener('auth-required', handleAuthRequired);
|
| 138 |
+
|
| 139 |
+
return () => {
|
| 140 |
+
window.removeEventListener('auth-required', handleAuthRequired);
|
| 141 |
+
};
|
| 142 |
+
}, [authRequired, isAuthenticated]);
|
| 143 |
+
|
| 144 |
+
const value: AuthContextType = {
|
| 145 |
+
user,
|
| 146 |
+
isAuthenticated,
|
| 147 |
+
isLoading,
|
| 148 |
+
isHFSpaces,
|
| 149 |
+
authRequired,
|
| 150 |
+
login,
|
| 151 |
+
logout,
|
| 152 |
+
checkAuthStatus,
|
| 153 |
+
showLoginModal,
|
| 154 |
+
setShowLoginModal,
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
| 158 |
+
}
|
|
@@ -33,6 +33,18 @@ async function fetchApi<T>(
|
|
| 33 |
});
|
| 34 |
|
| 35 |
if (!response.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
// Handle 429 (Too Many Requests) with exponential backoff
|
| 37 |
if (response.status === 429 && retryCount < 3) {
|
| 38 |
const backoffDelay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
|
|
|
|
| 33 |
});
|
| 34 |
|
| 35 |
if (!response.ok) {
|
| 36 |
+
// Handle 401 (Unauthorized) - trigger login modal
|
| 37 |
+
if (response.status === 401) {
|
| 38 |
+
// Check if running in browser environment
|
| 39 |
+
if (typeof window !== 'undefined') {
|
| 40 |
+
// Dispatch a custom event to trigger login modal
|
| 41 |
+
window.dispatchEvent(new CustomEvent('auth-required', {
|
| 42 |
+
detail: { message: 'Authentication required to access this feature' }
|
| 43 |
+
}));
|
| 44 |
+
}
|
| 45 |
+
throw new ApiError(response.status, 'Authentication required');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
// Handle 429 (Too Many Requests) with exponential backoff
|
| 49 |
if (response.status === 429 && retryCount < 3) {
|
| 50 |
const backoffDelay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
|