Spaces:
Sleeping
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 +3 -4
- frontend/package-lock.json +60 -0
- frontend/package.json +2 -1
- frontend/src/App.tsx +8 -5
- frontend/src/components/auth/AuthModal.tsx +167 -0
- frontend/src/components/shared/ModalSystem.tsx +6 -1
- frontend/src/context/AuthContext.tsx +229 -0
- frontend/src/types/auth.ts +41 -0
- frontend/src/types/index.ts +1 -0
- frontend/src/types/window.d.ts +13 -0
|
@@ -177,11 +177,10 @@ async def shutdown_event():
|
|
| 177 |
# scheduler_service.stop() # This line is now commented out
|
| 178 |
|
| 179 |
|
| 180 |
-
# Root
|
| 181 |
@app.get("/")
|
| 182 |
-
async def root(request: Request
|
| 183 |
-
|
| 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 |
|
|
@@ -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",
|
|
@@ -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"
|
|
@@ -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 |
-
<
|
| 36 |
-
<
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
| 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>
|
|
@@ -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 |
+
}
|
|
@@ -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" ||
|
|
|
|
|
|
|
| 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()}</>;
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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;
|
|
@@ -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 {};
|