wu981526092 commited on
Commit
eb7193f
·
1 Parent(s): 041fce6

Implement client-side OAuth authentication with popup modal

Browse files

✨ New Features:
- Frontend-first authentication using @huggingface/hub
- Popup auth modal with demo video and research paper
- AuthContext for managing client-side auth state
- Seamless OAuth flow without backend session complexity

🔧 Technical Implementation:
- AuthModal component with beautiful UI design
- Type-safe OAuth result conversion for HF API compatibility
- localStorage-based token persistence with expiration checks
- Environment-aware authentication (HF Spaces vs local dev)
- Integration with existing Modal system

🎨 User Experience:
- Users see full demo interface immediately
- Authentication required only when accessing protected features
- Modal shows video and research paper during auth flow
- Consistent design with existing shadcn/ui components

🚀 Benefits:
- Simplified architecture (no complex backend OAuth)
- Better UX (demo first, auth when needed)
- HF Spaces native integration
- Easier maintenance and debugging

This replaces the previous backend-heavy OAuth system with a modern,
client-side approach that's more suitable for SPA applications.

backend/app.py CHANGED
@@ -177,11 +177,10 @@ async def shutdown_event():
177
  # scheduler_service.stop() # This line is now commented out
178
 
179
 
180
- # Root redirect to React app (requires authentication)
181
  @app.get("/")
182
- async def root(request: Request, auth_check = Depends(require_auth_in_hf_spaces)):
183
- # This endpoint is protected by dependency injection
184
- # If user reaches here, they are authenticated (or in local dev)
185
  return RedirectResponse(url="/agentgraph")
186
 
187
 
 
177
  # scheduler_service.stop() # This line is now commented out
178
 
179
 
180
+ # Root route - serve React app directly (authentication handled by frontend)
181
  @app.get("/")
182
+ async def root(request: Request):
183
+ """Serve the React app directly - authentication is now handled by frontend"""
 
184
  return RedirectResponse(url="/agentgraph")
185
 
186
 
frontend/package-lock.json CHANGED
@@ -8,6 +8,7 @@
8
  "name": "agentgraph-react",
9
  "version": "1.0.0",
10
  "dependencies": {
 
11
  "@monaco-editor/react": "^4.7.0",
12
  "@radix-ui/react-accordion": "^1.2.11",
13
  "@radix-ui/react-checkbox": "^1.3.2",
@@ -908,6 +909,30 @@
908
  "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
909
  "license": "MIT"
910
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
911
  "node_modules/@humanwhocodes/config-array": {
912
  "version": "0.13.0",
913
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -3488,6 +3513,41 @@
3488
  "url": "https://polar.sh/cva"
3489
  }
3490
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3491
  "node_modules/clsx": {
3492
  "version": "2.1.1",
3493
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
 
8
  "name": "agentgraph-react",
9
  "version": "1.0.0",
10
  "dependencies": {
11
+ "@huggingface/hub": "^2.6.4",
12
  "@monaco-editor/react": "^4.7.0",
13
  "@radix-ui/react-accordion": "^1.2.11",
14
  "@radix-ui/react-checkbox": "^1.3.2",
 
909
  "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
910
  "license": "MIT"
911
  },
912
+ "node_modules/@huggingface/hub": {
913
+ "version": "2.6.4",
914
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.6.4.tgz",
915
+ "integrity": "sha512-eqJjP0DAIShc8O90neoK1SFAlgikK6jpreTcOue4hXkKRa+BkC4WCLGintI8HIDk2Y+pHgQwUvsdb4Ruo4inSg==",
916
+ "license": "MIT",
917
+ "dependencies": {
918
+ "@huggingface/tasks": "^0.19.45"
919
+ },
920
+ "bin": {
921
+ "hfjs": "dist/cli.js"
922
+ },
923
+ "engines": {
924
+ "node": ">=18"
925
+ },
926
+ "optionalDependencies": {
927
+ "cli-progress": "^3.12.0"
928
+ }
929
+ },
930
+ "node_modules/@huggingface/tasks": {
931
+ "version": "0.19.46",
932
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.46.tgz",
933
+ "integrity": "sha512-c6F/r7zRQjmyo6Ji8c2TUbdeeu6WAdZxYLRd+G7Xxvfbadi6iDwk2szt/oinC5v5Ljyc2sjzesaqGB6hLWy/DA==",
934
+ "license": "MIT"
935
+ },
936
  "node_modules/@humanwhocodes/config-array": {
937
  "version": "0.13.0",
938
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
 
3513
  "url": "https://polar.sh/cva"
3514
  }
3515
  },
3516
+ "node_modules/cli-progress": {
3517
+ "version": "3.12.0",
3518
+ "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
3519
+ "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
3520
+ "license": "MIT",
3521
+ "optional": true,
3522
+ "dependencies": {
3523
+ "string-width": "^4.2.3"
3524
+ },
3525
+ "engines": {
3526
+ "node": ">=4"
3527
+ }
3528
+ },
3529
+ "node_modules/cli-progress/node_modules/emoji-regex": {
3530
+ "version": "8.0.0",
3531
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
3532
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
3533
+ "license": "MIT",
3534
+ "optional": true
3535
+ },
3536
+ "node_modules/cli-progress/node_modules/string-width": {
3537
+ "version": "4.2.3",
3538
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
3539
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
3540
+ "license": "MIT",
3541
+ "optional": true,
3542
+ "dependencies": {
3543
+ "emoji-regex": "^8.0.0",
3544
+ "is-fullwidth-code-point": "^3.0.0",
3545
+ "strip-ansi": "^6.0.1"
3546
+ },
3547
+ "engines": {
3548
+ "node": ">=8"
3549
+ }
3550
+ },
3551
  "node_modules/clsx": {
3552
  "version": "2.1.1",
3553
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
frontend/package.json CHANGED
@@ -12,6 +12,7 @@
12
  "type-check": "tsc --noEmit"
13
  },
14
  "dependencies": {
 
15
  "@monaco-editor/react": "^4.7.0",
16
  "@radix-ui/react-accordion": "^1.2.11",
17
  "@radix-ui/react-checkbox": "^1.3.2",
@@ -35,7 +36,6 @@
35
  "@types/d3": "^7.4.3",
36
  "@types/d3-cloud": "^1.2.9",
37
  "@types/node": "^22.15.29",
38
- "TagCloud": "^2.5.0",
39
  "class-variance-authority": "^0.7.0",
40
  "clsx": "^2.0.0",
41
  "cmdk": "^0.2.1",
@@ -49,6 +49,7 @@
49
  "react-force-graph-2d": "^1.27.1",
50
  "react-markdown": "^10.1.0",
51
  "recharts": "^2.15.4",
 
52
  "tailwind-merge": "^1.14.0",
53
  "tailwindcss-animate": "^1.0.7",
54
  "use-text-analyzer": "^2.1.6"
 
12
  "type-check": "tsc --noEmit"
13
  },
14
  "dependencies": {
15
+ "@huggingface/hub": "^2.6.4",
16
  "@monaco-editor/react": "^4.7.0",
17
  "@radix-ui/react-accordion": "^1.2.11",
18
  "@radix-ui/react-checkbox": "^1.3.2",
 
36
  "@types/d3": "^7.4.3",
37
  "@types/d3-cloud": "^1.2.9",
38
  "@types/node": "^22.15.29",
 
39
  "class-variance-authority": "^0.7.0",
40
  "clsx": "^2.0.0",
41
  "cmdk": "^0.2.1",
 
49
  "react-force-graph-2d": "^1.27.1",
50
  "react-markdown": "^10.1.0",
51
  "recharts": "^2.15.4",
52
+ "TagCloud": "^2.5.0",
53
  "tailwind-merge": "^1.14.0",
54
  "tailwindcss-animate": "^1.0.7",
55
  "use-text-analyzer": "^2.1.6"
frontend/src/App.tsx CHANGED
@@ -5,6 +5,7 @@ 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";
@@ -32,11 +33,13 @@ function App() {
32
  <NotificationProvider>
33
  <NavigationProvider>
34
  <ModalProvider>
35
- <KGDisplayModeProvider>
36
- <AgentGraphProvider>
37
- <AppContent />
38
- </AgentGraphProvider>
39
- </KGDisplayModeProvider>
 
 
40
  </ModalProvider>
41
  </NavigationProvider>
42
  </NotificationProvider>
 
5
  import { ModalProvider, useModal } from "./context/ModalContext";
6
  import { NavigationProvider } from "./context/NavigationContext";
7
  import { KGDisplayModeProvider } from "./context/KGDisplayModeContext";
8
+ import { AuthProvider } from "./context/AuthContext";
9
  import { MainLayout } from "./components/layout/MainLayout";
10
  import { ModalSystem } from "./components/shared/ModalSystem";
11
  import { Toaster } from "./components/ui/toaster";
 
33
  <NotificationProvider>
34
  <NavigationProvider>
35
  <ModalProvider>
36
+ <AuthProvider>
37
+ <KGDisplayModeProvider>
38
+ <AgentGraphProvider>
39
+ <AppContent />
40
+ </AgentGraphProvider>
41
+ </KGDisplayModeProvider>
42
+ </AuthProvider>
43
  </ModalProvider>
44
  </NavigationProvider>
45
  </NotificationProvider>
frontend/src/components/auth/AuthModal.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogHeader,
6
+ DialogTitle,
7
+ } from "@/components/ui/dialog";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Card, CardContent } from "@/components/ui/card";
10
+ import { useAuth } from "@/context/AuthContext";
11
+ import { ExternalLink, FileText, Play } from "lucide-react";
12
+
13
+ interface AuthModalProps {
14
+ open: boolean;
15
+ onOpenChange: (open: boolean) => void;
16
+ }
17
+
18
+ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
19
+ const { login, authState } = useAuth();
20
+
21
+ const handleLogin = async () => {
22
+ try {
23
+ await login();
24
+ // Don't close modal here, it will close after successful auth
25
+ } catch (error) {
26
+ console.error("Login failed:", error);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <Dialog open={open} onOpenChange={onOpenChange}>
32
+ <DialogContent className="max-w-6xl p-0 overflow-hidden">
33
+ <div className="grid lg:grid-cols-2 gap-0 min-h-[600px]">
34
+ {/* Left side - Authentication */}
35
+ <div className="p-8 lg:p-12 flex flex-col justify-center bg-background">
36
+ <div className="space-y-8">
37
+ {/* Header */}
38
+ <div className="space-y-4">
39
+ <h1 className="text-3xl lg:text-4xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
40
+ AgentGraph
41
+ </h1>
42
+ <p className="text-xl text-muted-foreground leading-relaxed">
43
+ Trace-to-Graph Platform for Interactive Analysis and
44
+ Robustness Testing in Agentic AI Systems
45
+ </p>
46
+ </div>
47
+
48
+ {/* Description */}
49
+ <p className="text-lg text-muted-foreground leading-relaxed">
50
+ Convert execution logs into interactive knowledge graphs with
51
+ actionable insights for AI system analysis and robustness
52
+ testing.
53
+ </p>
54
+
55
+ {/* Authentication Section */}
56
+ <div className="space-y-6">
57
+ <div className="space-y-4">
58
+ <h2 className="text-xl font-semibold">
59
+ Access Research Platform
60
+ </h2>
61
+
62
+ <Button
63
+ onClick={handleLogin}
64
+ disabled={authState.isLoading}
65
+ size="lg"
66
+ className="w-full h-12 text-lg bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 transition-all duration-300"
67
+ >
68
+ {authState.isLoading ? (
69
+ <div className="flex items-center space-x-2">
70
+ <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
71
+ <span>Connecting...</span>
72
+ </div>
73
+ ) : (
74
+ <div className="flex items-center space-x-2">
75
+ <svg
76
+ className="w-5 h-5"
77
+ viewBox="0 0 24 24"
78
+ fill="currentColor"
79
+ >
80
+ <path d="M12 0C5.374 0 0 5.373 0 12s5.374 12 12 12 12-5.373 12-12S18.626 0 12 0zm5.568 8.16c-.169 1.858-.896 3.375-2.043 4.519-1.146 1.144-2.663 1.874-4.521 2.043-.151.014-.302.021-.454.021-.156 0-.31-.007-.463-.021-1.858-.169-3.375-.899-4.519-2.043C4.424 11.535 3.694 10.018 3.525 8.16c-.014-.151-.021-.302-.021-.454 0-.156.007-.31.021-.463.169-1.858.899-3.375 2.043-4.519C6.712 1.58 8.229.85 10.087.681c.151-.014.302-.021.454-.021.156 0 .31.007.463.021 1.858.169 3.375.899 4.519 2.043 1.144 1.144 1.874 2.661 2.043 4.519.014.151.021.302.021.454 0 .156-.007.31-.021.463z" />
81
+ </svg>
82
+ <span>Sign in with Hugging Face</span>
83
+ <ExternalLink className="w-4 h-4" />
84
+ </div>
85
+ )}
86
+ </Button>
87
+ </div>
88
+
89
+ {authState.error && (
90
+ <Card className="border-destructive/20 bg-destructive/5">
91
+ <CardContent className="p-4">
92
+ <p className="text-sm text-destructive">
93
+ {authState.error}
94
+ </p>
95
+ </CardContent>
96
+ </Card>
97
+ )}
98
+
99
+ <Card className="bg-muted/30">
100
+ <CardContent className="p-4">
101
+ <p className="text-sm text-muted-foreground">
102
+ Authentication required for responsible AI resource usage
103
+ </p>
104
+ </CardContent>
105
+ </Card>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Right side - Demo Content */}
111
+ <div className="bg-secondary/20 p-8 lg:p-12 space-y-6">
112
+ {/* Demo Video */}
113
+ <Card className="bg-background/50 backdrop-blur-sm border-border/50">
114
+ <CardContent className="p-0">
115
+ <div className="relative aspect-video rounded-lg overflow-hidden">
116
+ <iframe
117
+ src="https://www.youtube.com/embed/btrS9pfDYJY?si=dDX4tIs-oS2O2d2p"
118
+ title="AgentGraph: Interactive Analysis Platform Demo"
119
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
120
+ allowFullScreen
121
+ className="w-full h-full"
122
+ />
123
+ </div>
124
+ <div className="p-4">
125
+ <div className="flex items-center space-x-2 text-sm text-muted-foreground">
126
+ <Play className="w-4 h-4" />
127
+ <span>Interactive Demo</span>
128
+ </div>
129
+ </div>
130
+ </CardContent>
131
+ </Card>
132
+
133
+ {/* Research Paper */}
134
+ <Card className="bg-background/50 backdrop-blur-sm border-border/50">
135
+ <CardContent className="p-6">
136
+ <div className="flex items-start space-x-4">
137
+ <div className="flex-shrink-0">
138
+ <FileText className="w-8 h-8 text-primary" />
139
+ </div>
140
+ <div className="flex-1">
141
+ <h3 className="text-lg font-semibold mb-2">
142
+ Research Paper
143
+ </h3>
144
+ <p className="text-sm text-muted-foreground mb-4">
145
+ AgentGraph: Trace-to-Graph Platform for Interactive
146
+ Analysis and Robustness Testing in Agentic AI Systems
147
+ </p>
148
+ <a
149
+ href="/static/papers/agentgraph_paper.pdf"
150
+ target="_blank"
151
+ rel="noopener noreferrer"
152
+ className="inline-flex items-center text-primary hover:text-primary/80 transition-colors"
153
+ >
154
+ <FileText className="w-4 h-4 mr-2" />
155
+ <span>Download PDF</span>
156
+ <ExternalLink className="w-4 h-4 ml-1" />
157
+ </a>
158
+ </div>
159
+ </div>
160
+ </CardContent>
161
+ </Card>
162
+ </div>
163
+ </div>
164
+ </DialogContent>
165
+ </Dialog>
166
+ );
167
+ }
frontend/src/components/shared/ModalSystem.tsx CHANGED
@@ -14,6 +14,7 @@ import { TraceSegmentModal } from "@/components/shared/modals/TraceSegmentModal"
14
  import ExampleTraceModal from "./modals/ExampleTraceModal";
15
  import { ObservabilityConnectionDialog } from "@/components/features/observability/ObservabilityConnectionDialog";
16
  import { UploadDialog } from "@/components/features/upload/UploadDialog";
 
17
 
18
  interface ModalSystemProps {
19
  modalState: ModalState;
@@ -62,6 +63,8 @@ export function ModalSystem({ modalState, onClose }: ModalSystemProps) {
62
  editConnection={data?.editConnection}
63
  />
64
  );
 
 
65
 
66
  default:
67
  return (
@@ -74,7 +77,9 @@ export function ModalSystem({ modalState, onClose }: ModalSystemProps) {
74
 
75
  // Some modals have their own Dialog wrapper
76
  const hasOwnDialog =
77
- type === "upload-trace" || type === "observability-connection";
 
 
78
 
79
  if (hasOwnDialog) {
80
  return <>{renderModalContent()}</>;
 
14
  import ExampleTraceModal from "./modals/ExampleTraceModal";
15
  import { ObservabilityConnectionDialog } from "@/components/features/observability/ObservabilityConnectionDialog";
16
  import { UploadDialog } from "@/components/features/upload/UploadDialog";
17
+ import { AuthModal } from "@/components/auth/AuthModal";
18
 
19
  interface ModalSystemProps {
20
  modalState: ModalState;
 
63
  editConnection={data?.editConnection}
64
  />
65
  );
66
+ case "auth-login":
67
+ return <AuthModal open={isOpen} onOpenChange={onClose} />;
68
 
69
  default:
70
  return (
 
77
 
78
  // Some modals have their own Dialog wrapper
79
  const hasOwnDialog =
80
+ type === "upload-trace" ||
81
+ type === "observability-connection" ||
82
+ type === "auth-login";
83
 
84
  if (hasOwnDialog) {
85
  return <>{renderModalContent()}</>;
frontend/src/context/AuthContext.tsx ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useReducer,
5
+ useEffect,
6
+ ReactNode,
7
+ useMemo,
8
+ } from "react";
9
+ import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "@huggingface/hub";
10
+ import type { OAuthResult as HFOAuthResult } from "@huggingface/hub";
11
+ import {
12
+ AuthState,
13
+ AuthContextType,
14
+ OAuthResult,
15
+ UserInfo,
16
+ } from "@/types/auth";
17
+ import { useModal } from "./ModalContext";
18
+
19
+ type AuthAction =
20
+ | { type: "AUTH_START" }
21
+ | { type: "AUTH_SUCCESS"; payload: OAuthResult }
22
+ | { type: "AUTH_ERROR"; payload: string }
23
+ | { type: "AUTH_LOGOUT" }
24
+ | { type: "SET_LOADING"; payload: boolean };
25
+
26
+ const initialState: AuthState = {
27
+ isAuthenticated: false,
28
+ user: null,
29
+ accessToken: null,
30
+ accessTokenExpiresAt: null,
31
+ scope: null,
32
+ isLoading: true,
33
+ error: null,
34
+ };
35
+
36
+ function authReducer(state: AuthState, action: AuthAction): AuthState {
37
+ switch (action.type) {
38
+ case "AUTH_START":
39
+ return {
40
+ ...state,
41
+ isLoading: true,
42
+ error: null,
43
+ };
44
+ case "AUTH_SUCCESS":
45
+ return {
46
+ ...state,
47
+ isAuthenticated: true,
48
+ user: action.payload.userInfo,
49
+ accessToken: action.payload.accessToken,
50
+ accessTokenExpiresAt:
51
+ action.payload.accessTokenExpiresAt instanceof Date
52
+ ? action.payload.accessTokenExpiresAt.toISOString()
53
+ : action.payload.accessTokenExpiresAt,
54
+ scope: action.payload.scope,
55
+ isLoading: false,
56
+ error: null,
57
+ };
58
+ case "AUTH_ERROR":
59
+ return {
60
+ ...state,
61
+ isAuthenticated: false,
62
+ user: null,
63
+ accessToken: null,
64
+ accessTokenExpiresAt: null,
65
+ scope: null,
66
+ isLoading: false,
67
+ error: action.payload,
68
+ };
69
+ case "AUTH_LOGOUT":
70
+ return {
71
+ ...initialState,
72
+ isLoading: false,
73
+ };
74
+ case "SET_LOADING":
75
+ return {
76
+ ...state,
77
+ isLoading: action.payload,
78
+ };
79
+ default:
80
+ return state;
81
+ }
82
+ }
83
+
84
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
85
+
86
+ const STORAGE_KEY = "agentgraph_oauth";
87
+
88
+ // Convert HF OAuth result to our internal format
89
+ function convertHFOAuthResult(hfResult: HFOAuthResult): OAuthResult {
90
+ const userInfo = hfResult.userInfo as any; // Type assertion for flexibility
91
+ return {
92
+ accessToken: hfResult.accessToken,
93
+ accessTokenExpiresAt: hfResult.accessTokenExpiresAt instanceof Date
94
+ ? hfResult.accessTokenExpiresAt.toISOString()
95
+ : hfResult.accessTokenExpiresAt,
96
+ userInfo: {
97
+ id: userInfo.sub || userInfo.id || "unknown",
98
+ name: userInfo.name || userInfo.preferred_username || "Unknown User",
99
+ fullname: userInfo.preferred_username,
100
+ email: userInfo.email,
101
+ emailVerified: userInfo.email_verified,
102
+ avatarUrl: userInfo.picture,
103
+ isPro: userInfo.isPro,
104
+ orgs: userInfo.orgs,
105
+ },
106
+ scope: hfResult.scope,
107
+ };
108
+ }
109
+
110
+ export function AuthProvider({ children }: { children: ReactNode }) {
111
+ const [authState, dispatch] = useReducer(authReducer, initialState);
112
+ const { openModal } = useModal();
113
+
114
+ // Check for existing auth on mount
115
+ useEffect(() => {
116
+ checkAuthStatus();
117
+ }, []);
118
+
119
+ const checkAuthStatus = async () => {
120
+ try {
121
+ dispatch({ type: "SET_LOADING", payload: true });
122
+
123
+ // First check localStorage for existing oauth data
124
+ const stored = localStorage.getItem(STORAGE_KEY);
125
+ if (stored) {
126
+ try {
127
+ const oauthResult = JSON.parse(stored) as OAuthResult;
128
+ // Check if token is still valid
129
+ const expiresAt = new Date(oauthResult.accessTokenExpiresAt);
130
+ if (expiresAt > new Date()) {
131
+ dispatch({ type: "AUTH_SUCCESS", payload: oauthResult });
132
+ return;
133
+ } else {
134
+ // Token expired, remove from storage
135
+ localStorage.removeItem(STORAGE_KEY);
136
+ }
137
+ } catch (error) {
138
+ localStorage.removeItem(STORAGE_KEY);
139
+ }
140
+ }
141
+
142
+ // Check for OAuth redirect
143
+ const hfOauthResult = await oauthHandleRedirectIfPresent();
144
+ if (hfOauthResult) {
145
+ // Convert HF result to our internal format
146
+ const normalizedResult = convertHFOAuthResult(hfOauthResult);
147
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizedResult));
148
+ dispatch({ type: "AUTH_SUCCESS", payload: normalizedResult });
149
+ } else {
150
+ dispatch({ type: "SET_LOADING", payload: false });
151
+ }
152
+ } catch (error) {
153
+ console.error("Auth check failed:", error);
154
+ dispatch({
155
+ type: "AUTH_ERROR",
156
+ payload: "Failed to check authentication status",
157
+ });
158
+ }
159
+ };
160
+
161
+ const login = async () => {
162
+ try {
163
+ dispatch({ type: "AUTH_START" });
164
+
165
+ // Check if we're in HF Spaces environment
166
+ const isHFSpaces = window.huggingface && window.huggingface.variables;
167
+
168
+ if (isHFSpaces) {
169
+ // Use HF OAuth
170
+ const scopes =
171
+ window.huggingface?.variables?.OAUTH_SCOPES ||
172
+ "openid profile read-repos";
173
+ const loginUrl = await oauthLoginUrl({ scopes });
174
+ window.location.href = loginUrl + "&prompt=consent";
175
+ } else {
176
+ // For local development, show a message or redirect to HF
177
+ dispatch({
178
+ type: "AUTH_ERROR",
179
+ payload:
180
+ "Authentication is only available when deployed to Hugging Face Spaces",
181
+ });
182
+ }
183
+ } catch (error) {
184
+ console.error("Login failed:", error);
185
+ dispatch({
186
+ type: "AUTH_ERROR",
187
+ payload: "Failed to initiate login",
188
+ });
189
+ }
190
+ };
191
+
192
+ const logout = () => {
193
+ localStorage.removeItem(STORAGE_KEY);
194
+ dispatch({ type: "AUTH_LOGOUT" });
195
+ // Optionally redirect to clear URL params
196
+ const url = new URL(window.location.href);
197
+ url.search = "";
198
+ window.history.replaceState({}, "", url.toString());
199
+ };
200
+
201
+ const requireAuth = () => {
202
+ if (!authState.isAuthenticated) {
203
+ openModal("auth-login", "Sign in to AgentGraph");
204
+ }
205
+ };
206
+
207
+ const contextValue = useMemo(
208
+ () => ({
209
+ authState,
210
+ login,
211
+ logout,
212
+ requireAuth,
213
+ checkAuthStatus,
214
+ }),
215
+ [authState]
216
+ );
217
+
218
+ return (
219
+ <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
220
+ );
221
+ }
222
+
223
+ export function useAuth() {
224
+ const context = useContext(AuthContext);
225
+ if (context === undefined) {
226
+ throw new Error("useAuth must be used within an AuthProvider");
227
+ }
228
+ return context;
229
+ }
frontend/src/types/auth.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface UserInfo {
2
+ id: string;
3
+ name: string;
4
+ fullname?: string;
5
+ email?: string;
6
+ emailVerified?: boolean;
7
+ avatarUrl?: string;
8
+ isPro?: boolean;
9
+ orgs?: Array<{
10
+ id: string;
11
+ name: string;
12
+ fullname: string;
13
+ isEnterprise: boolean;
14
+ avatarUrl: string;
15
+ }>;
16
+ }
17
+
18
+ export interface AuthState {
19
+ isAuthenticated: boolean;
20
+ user: UserInfo | null;
21
+ accessToken: string | null;
22
+ accessTokenExpiresAt: string | null;
23
+ scope: string | null;
24
+ isLoading: boolean;
25
+ error: string | null;
26
+ }
27
+
28
+ export interface OAuthResult {
29
+ accessToken: string;
30
+ accessTokenExpiresAt: string | Date;
31
+ userInfo: UserInfo;
32
+ scope: string;
33
+ }
34
+
35
+ export interface AuthContextType {
36
+ authState: AuthState;
37
+ login: () => Promise<void>;
38
+ logout: () => void;
39
+ requireAuth: () => void;
40
+ checkAuthStatus: () => Promise<void>;
41
+ }
frontend/src/types/index.ts CHANGED
@@ -138,6 +138,7 @@ export interface ModalState {
138
  | "trace-segment"
139
  | "upload-trace"
140
  | "observability-connection"
 
141
  | null;
142
  title: string;
143
  data?: any;
 
138
  | "trace-segment"
139
  | "upload-trace"
140
  | "observability-connection"
141
+ | "auth-login"
142
  | null;
143
  title: string;
144
  data?: any;
frontend/src/types/window.d.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare global {
2
+ interface Window {
3
+ huggingface?: {
4
+ variables?: {
5
+ OAUTH_SCOPES?: string;
6
+ [key: string]: any;
7
+ };
8
+ [key: string]: any;
9
+ };
10
+ }
11
+ }
12
+
13
+ export {};