Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | 8x 8x 26x 26x 8x 8x 8x 8x 8x 8x 7x 1x 8x 7x 1x 1x 1x 1x 1x 1x 1x 8x | import { useState, useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Loader2 } from 'lucide-react'
import { toast } from 'sonner'
import { useAuthStore } from './authStore'
import { APP_VERSION } from '@/version'
import { Button } from '@/components/ui/button'
export function LoginPage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
const setAuth = useAuthStore((s) => s.setAuth)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const expired = searchParams.get('expired') === 'true'
const errorParam = searchParams.get('error')
const returnUrl = searchParams.get('return') ?? undefined
useEffect(() => {
if (errorParam) {
toast.error('Authentication failed. Please try again.')
}
}, [errorParam])
useEffect(() => {
if (isAuthenticated) {
navigate('/', { replace: true })
}
}, [isAuthenticated, navigate])
async function handleLogin() {
setLoading(true)
setError(null)
if (import.meta.env.DEV) {
await new Promise((r) => setTimeout(r, 600))
setAuth(
{
sub: 'user-1',
preferred_username: 'admin',
email: 'admin@keskikuja.site',
groups: ['admin'],
},
new Date(Date.now() + 8 * 3600_000).toISOString(),
new Date(Date.now() + 30 * 60_000).toISOString(),
)
navigate('/', { replace: true })
} else E{
try {
const initUrl = returnUrl
? `/api/auth/init?return=${encodeURIComponent(returnUrl)}`
: '/api/auth/init'
const res = await fetch(initUrl)
if (!res.ok) throw new Error('Init failed')
const { authorization_url } = await res.json()
window.location.href = authorization_url
} catch {
setError('Failed to connect to authentication server. Try again.')
setLoading(false)
}
}
}
return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-[url('/agent-platform.jpg')] bg-cover bg-center bg-no-repeat">
<div className="brand-hero-overlay absolute inset-0" aria-hidden="true" />
<div className="relative z-10 flex flex-col items-center gap-6 max-w-md bg-background/30 backdrop-blur-sm p-6 rounded-xl">
<h1 className="font-heading text-5xl font-semibold text-foreground">Agent Platform</h1>
{expired && (
<p className="text-sm text-destructive">Your session has expired. Please log in again.</p>
)}
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<div className="font-mono text-center text-sm text-foreground/85 leading-relaxed">
<p>
The industrial
revolution wasn't the steam engine. It was the{' '}
<em>factory system</em> — steam engine + standardized processes +
division of labor + repeatability.
</p>
<p className="mt-3">
AI is the steam engine.{' '}
<strong className="font-semibold text-foreground">
Agent Platform is the factory.
</strong>
</p>
<p className="mt-3">
Without a platform, AI is a single tool that every engineer uses
however they see fit. With a platform, AI is an{' '}
<em>industrial process</em>: versioning, repeatability, quality
control, scaling.
</p>
<p className="mt-3 italic text-foreground/80">
This project is an answer to the question: how do we move from AI
craftsmanship to AI industry?
</p>
</div>
<Button onClick={handleLogin} disabled={loading} className="brand-amber-glow">
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Redirecting...
</>
) : (
'Sign in with OIDC'
)}
</Button>
<p className="text-foreground/85">
Sign in to manage your AI factory
</p>
</div>
<p className="fixed bottom-4 z-10 text-sm text-foreground/60">v{APP_VERSION}</p>
</div>
)
}
|