mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
fix(frontend): standardize favicon/title, 401 redirect, SPA root route, and theme switching
- Replace default Vite favicon and title with project-specific branding - Add axios response interceptor to handle 401 by clearing token and redirecting to login - Move health check endpoint from '/' to '/api/health' so SPA index.html is served on root - Integrate next-themes ThemeProvider with system preference detection and manual toggle - Update docker-compose and k8s health check paths accordingly - Replace hardcoded dark-only colors with semantic CSS variable tokens for theme compatibility
This commit is contained in:
@@ -50,7 +50,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3334:3000"
|
- "3334:3000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -58,7 +58,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3334:3000"
|
- "3334:3000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Gitea AI Assistant</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
1
frontend/public/favicon.svg
Normal file
1
frontend/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="hsl(175, 90%, 45%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>
|
||||||
|
After Width: | Height: | Size: 342 B |
@@ -6,6 +6,7 @@ import { RepositoryManager } from './components/RepositoryManager';
|
|||||||
import { ConfigManager } from './components/ConfigManager';
|
import { ConfigManager } from './components/ConfigManager';
|
||||||
import { ReviewConfigPage } from './components/ReviewConfigPage';
|
import { ReviewConfigPage } from './components/ReviewConfigPage';
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
function AuthGuard({ children }: { children: React.ReactNode }) {
|
function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
@@ -32,7 +33,9 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function AppContent() {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -51,9 +54,13 @@ function App() {
|
|||||||
<Route path="*" element={<Navigate to="/repos" replace />} />
|
<Route path="*" element={<Navigate to="/repos" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster theme="dark" />
|
<Toaster theme={resolvedTheme === 'dark' ? 'dark' : 'light'} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <AppContent />;
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -72,11 +72,11 @@
|
|||||||
}
|
}
|
||||||
.bg-grid-pattern {
|
.bg-grid-pattern {
|
||||||
background-size: 40px 40px;
|
background-size: 40px 40px;
|
||||||
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
background-image: linear-gradient(to right, hsl(var(--foreground) / 0.05) 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
linear-gradient(to bottom, hsl(var(--foreground) / 0.05) 1px, transparent 1px);
|
||||||
}
|
}
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
@apply bg-zinc-950/50 backdrop-blur-xl border border-white/10 shadow-2xl;
|
@apply bg-card/80 backdrop-blur-xl border border-border shadow-2xl;
|
||||||
}
|
}
|
||||||
.tech-glow {
|
.tech-glow {
|
||||||
box-shadow: 0 0 20px -5px hsl(var(--primary) / 0.5);
|
box-shadow: 0 0 20px -5px hsl(var(--primary) / 0.5);
|
||||||
|
|||||||
@@ -18,4 +18,20 @@ api.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 添加响应拦截器,处理 401 未授权自动跳转登录页
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
// 避免在登录接口本身触发跳转
|
||||||
|
const isLoginRequest = error.config?.url?.includes('/login');
|
||||||
|
if (!isLoginRequest) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { ThemeProvider } from 'next-themes'
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -21,13 +22,13 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force dark mode as requested
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<App />
|
<QueryClientProvider client={queryClient}>
|
||||||
</QueryClientProvider>
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { LogOut, Bot, FolderGit2, Sliders, Menu, X, PanelLeftClose, PanelLeftOpen, FileSearch } from 'lucide-react';
|
import { LogOut, Bot, FolderGit2, Sliders, Menu, X, PanelLeftClose, PanelLeftOpen, FileSearch, Sun, Moon } from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/repos', label: '仓库管理', icon: FolderGit2 },
|
{ path: '/repos', label: '仓库管理', icon: FolderGit2 },
|
||||||
@@ -11,6 +12,7 @@ const navItems = [
|
|||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { setTheme, resolvedTheme } = useTheme();
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
@@ -33,24 +35,24 @@ export default function DashboardPage() {
|
|||||||
{/* Mobile Overlay */}
|
{/* Mobile Overlay */}
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden animate-in fade-in"
|
className="fixed inset-0 z-40 bg-foreground/60 backdrop-blur-sm lg:hidden animate-in fade-in"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-zinc-950 transition-all duration-300 ease-in-out lg:relative ${
|
className={`fixed inset-y-0 left-0 z-50 flex flex-col border-r border-border bg-card transition-all duration-300 ease-in-out lg:relative ${
|
||||||
isSidebarCollapsed ? 'w-[72px]' : 'w-64'
|
isSidebarCollapsed ? 'w-[72px]' : 'w-64'
|
||||||
} ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}
|
} ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}
|
||||||
>
|
>
|
||||||
<div className="flex h-16 items-center justify-between px-4 border-b border-border/50 bg-zinc-950">
|
<div className="flex h-16 items-center justify-between px-4 border-b border-border/50 bg-card">
|
||||||
<div className={`flex items-center gap-3 overflow-hidden transition-all duration-300 ${isSidebarCollapsed ? 'w-10 justify-center -ml-1' : 'w-full'}`}>
|
<div className={`flex items-center gap-3 overflow-hidden transition-all duration-300 ${isSidebarCollapsed ? 'w-10 justify-center -ml-1' : 'w-full'}`}>
|
||||||
<div className="flex shrink-0 h-9 w-9 items-center justify-center rounded-xl bg-primary/10 text-primary border border-primary/20 shadow-[0_0_15px_rgba(20,184,166,0.15)] ring-1 ring-primary/10">
|
<div className="flex shrink-0 h-9 w-9 items-center justify-center rounded-xl bg-primary/10 text-primary border border-primary/20 shadow-[0_0_15px_rgba(20,184,166,0.15)] ring-1 ring-primary/10">
|
||||||
<Bot className="h-5 w-5" />
|
<Bot className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<span className="truncate font-bold tracking-tight text-zinc-100 whitespace-nowrap">
|
<span className="truncate font-bold tracking-tight text-foreground whitespace-nowrap">
|
||||||
Gitea AI Assistant
|
Gitea AI Assistant
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -58,7 +60,7 @@ export default function DashboardPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="lg:hidden shrink-0 h-8 w-8 text-zinc-400 hover:text-zinc-100"
|
className="lg:hidden shrink-0 h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
@@ -76,7 +78,7 @@ export default function DashboardPage() {
|
|||||||
`group relative flex w-full items-center rounded-xl p-2.5 transition-all duration-200 ${
|
`group relative flex w-full items-center rounded-xl p-2.5 transition-all duration-200 ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-primary/10 text-primary'
|
? 'bg-primary/10 text-primary'
|
||||||
: 'text-zinc-400 hover:bg-zinc-900 hover:text-zinc-100'
|
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
|
||||||
} ${isSidebarCollapsed ? 'justify-center' : 'justify-start gap-3'}`
|
} ${isSidebarCollapsed ? 'justify-center' : 'justify-start gap-3'}`
|
||||||
}
|
}
|
||||||
title={isSidebarCollapsed ? item.label : undefined}
|
title={isSidebarCollapsed ? item.label : undefined}
|
||||||
@@ -86,7 +88,7 @@ export default function DashboardPage() {
|
|||||||
{isActive && (
|
{isActive && (
|
||||||
<div className="absolute left-0 top-1/2 h-1/2 w-1 -translate-y-1/2 rounded-r-full bg-primary shadow-[0_0_10px_rgba(20,184,166,0.5)]"></div>
|
<div className="absolute left-0 top-1/2 h-1/2 w-1 -translate-y-1/2 rounded-r-full bg-primary shadow-[0_0_10px_rgba(20,184,166,0.5)]"></div>
|
||||||
)}
|
)}
|
||||||
<Icon className={`h-5 w-5 shrink-0 transition-transform duration-300 ${isActive ? 'text-primary scale-110' : 'text-zinc-500 group-hover:text-zinc-300'}`} />
|
<Icon className={`h-5 w-5 shrink-0 transition-transform duration-300 ${isActive ? 'text-primary scale-110' : 'text-muted-foreground group-hover:text-foreground'}`} />
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<span className="font-medium tracking-wide text-sm">{item.label}</span>
|
<span className="font-medium tracking-wide text-sm">{item.label}</span>
|
||||||
)}
|
)}
|
||||||
@@ -97,10 +99,10 @@ export default function DashboardPage() {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="border-t border-border/50 p-3 bg-zinc-950">
|
<div className="border-t border-border/50 p-3 bg-card">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
||||||
className={`hidden lg:flex w-full items-center rounded-xl p-2.5 text-zinc-500 transition-colors hover:bg-zinc-900 hover:text-zinc-300 ${
|
className={`hidden lg:flex w-full items-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground ${
|
||||||
isSidebarCollapsed ? 'justify-center' : 'justify-start gap-3'
|
isSidebarCollapsed ? 'justify-center' : 'justify-start gap-3'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -124,7 +126,7 @@ export default function DashboardPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="lg:hidden text-zinc-400 hover:text-zinc-100 h-9 w-9 -ml-2"
|
className="lg:hidden text-muted-foreground hover:text-foreground h-9 w-9 -ml-2"
|
||||||
onClick={() => setIsMobileMenuOpen(true)}
|
onClick={() => setIsMobileMenuOpen(true)}
|
||||||
>
|
>
|
||||||
<Menu className="h-5 w-5" />
|
<Menu className="h-5 w-5" />
|
||||||
@@ -136,18 +138,28 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full border border-border/50 bg-zinc-900/50">
|
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full border border-border/50 bg-muted/50">
|
||||||
<div className="relative flex h-2 w-2">
|
<div className="relative flex h-2 w-2">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-500 opacity-75"></span>
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"></span>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-mono text-zinc-400 uppercase tracking-wider">System Online</span>
|
<span className="text-xs font-mono text-muted-foreground uppercase tracking-wider">System Online</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-6 w-px bg-border/50 hidden sm:block"></div>
|
<div className="h-6 w-px bg-border/50 hidden sm:block"></div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-full border border-border/50 bg-zinc-900 hover:bg-rose-500/10 hover:text-rose-400 hover:border-rose-500/20 transition-all h-9 w-9"
|
className="rounded-full border border-border/50 bg-muted hover:bg-accent/50 transition-all h-9 w-9"
|
||||||
|
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
||||||
|
title={resolvedTheme === 'dark' ? '切换为浅色主题' : '切换为深色主题'}
|
||||||
|
>
|
||||||
|
{resolvedTheme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
|
<span className="sr-only">切换主题</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full border border-border/50 bg-muted hover:bg-danger/10 hover:text-danger hover:border-danger/20 transition-all h-9 w-9"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
title="登出"
|
title="登出"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-zinc-950">
|
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-background">
|
||||||
{/* Background grid and gradient effects */}
|
{/* Background grid and gradient effects */}
|
||||||
<div className="absolute inset-0 bg-grid-pattern opacity-10"></div>
|
<div className="absolute inset-0 bg-grid-pattern opacity-10"></div>
|
||||||
<div className="absolute top-[-20%] left-[-10%] h-[500px] w-[500px] rounded-full bg-primary/20 blur-[120px] pointer-events-none"></div>
|
<div className="absolute top-[-20%] left-[-10%] h-[500px] w-[500px] rounded-full bg-primary/20 blur-[120px] pointer-events-none"></div>
|
||||||
@@ -39,17 +39,17 @@ export function LoginPage() {
|
|||||||
<div className="glass-panel relative rounded-2xl p-8 sm:p-10 transition-all duration-500 hover:border-primary/20">
|
<div className="glass-panel relative rounded-2xl p-8 sm:p-10 transition-all duration-500 hover:border-primary/20">
|
||||||
{/* Decorative terminal dots */}
|
{/* Decorative terminal dots */}
|
||||||
<div className="absolute top-4 left-4 flex gap-2">
|
<div className="absolute top-4 left-4 flex gap-2">
|
||||||
<div className="h-2.5 w-2.5 rounded-full bg-rose-500/80 shadow-[0_0_5px_rgba(244,63,94,0.5)]"></div>
|
<div className="h-2.5 w-2.5 rounded-full bg-danger/80 shadow-[0_0_5px_rgba(244,63,94,0.5)]"></div>
|
||||||
<div className="h-2.5 w-2.5 rounded-full bg-amber-500/80 shadow-[0_0_5px_rgba(245,158,11,0.5)]"></div>
|
<div className="h-2.5 w-2.5 rounded-full bg-warning/80 shadow-[0_0_5px_rgba(245,158,11,0.5)]"></div>
|
||||||
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500/80 shadow-[0_0_5px_rgba(16,185,129,0.5)]"></div>
|
<div className="h-2.5 w-2.5 rounded-full bg-success/80 shadow-[0_0_5px_rgba(16,185,129,0.5)]"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-10 mt-6 flex flex-col items-center text-center">
|
<div className="mb-10 mt-6 flex flex-col items-center text-center">
|
||||||
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-zinc-900 border border-primary/20 shadow-[0_0_20px_rgba(var(--primary),0.15)] ring-1 ring-primary/10 relative group">
|
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted border border-primary/20 shadow-[0_0_20px_rgba(var(--primary),0.15)] ring-1 ring-primary/10 relative group">
|
||||||
<div className="absolute inset-0 rounded-2xl bg-primary/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 rounded-2xl bg-primary/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
<Bot className="h-8 w-8 text-primary relative z-10" />
|
<Bot className="h-8 w-8 text-primary relative z-10" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mb-2 text-2xl font-bold tracking-tight text-white sm:text-3xl">Gitea AI Assistant</h1>
|
<h1 className="mb-2 text-2xl font-bold tracking-tight text-foreground sm:text-3xl">Gitea AI Assistant</h1>
|
||||||
<div className="flex items-center gap-2 text-xs font-mono text-primary/70 bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
|
<div className="flex items-center gap-2 text-xs font-mono text-primary/70 bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||||
@@ -62,7 +62,7 @@ export function LoginPage() {
|
|||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label htmlFor="password" className="text-xs font-mono font-medium text-zinc-400 flex items-center gap-2">
|
<label htmlFor="password" className="text-xs font-mono font-medium text-muted-foreground flex items-center gap-2">
|
||||||
<span className="text-primary font-bold">></span> enter_admin_password
|
<span className="text-primary font-bold">></span> enter_admin_password
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,14 +75,14 @@ export function LoginPage() {
|
|||||||
onKeyDown={(e) => e.key === 'Enter' && !isLoading && handleLogin()}
|
onKeyDown={(e) => e.key === 'Enter' && !isLoading && handleLogin()}
|
||||||
required
|
required
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className="h-12 border-zinc-800 bg-zinc-900/50 font-mono text-zinc-100 placeholder:text-zinc-700 focus-visible:border-primary/50 focus-visible:ring-primary/20 transition-all duration-300"
|
className="h-12 border-border bg-muted/50 font-mono text-foreground placeholder:text-muted-foreground/50 focus-visible:border-primary/50 focus-visible:ring-primary/20 transition-all duration-300"
|
||||||
/>
|
/>
|
||||||
<Terminal className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-600 transition-colors group-focus-within:text-primary/70" />
|
<Terminal className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground transition-colors group-focus-within:text-primary/70" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-start gap-2 rounded-lg border border-rose-500/20 bg-rose-500/10 px-3 py-3 text-sm text-rose-400 animate-in fade-in slide-in-from-top-1">
|
<div className="flex items-start gap-2 rounded-lg border border-danger/20 bg-danger/10 px-3 py-3 text-sm text-danger animate-in fade-in slide-in-from-top-1">
|
||||||
<Activity className="h-4 w-4 mt-0.5 shrink-0" />
|
<Activity className="h-4 w-4 mt-0.5 shrink-0" />
|
||||||
<p className="font-mono text-xs leading-relaxed">{error}</p>
|
<p className="font-mono text-xs leading-relaxed">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@ export function LoginPage() {
|
|||||||
className="tech-glow group relative mt-4 h-12 w-full overflow-hidden bg-primary text-primary-foreground transition-all hover:bg-primary/90 disabled:opacity-70 disabled:pointer-events-none"
|
className="tech-glow group relative mt-4 h-12 w-full overflow-hidden bg-primary text-primary-foreground transition-all hover:bg-primary/90 disabled:opacity-70 disabled:pointer-events-none"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 flex h-full w-full justify-center [transform:skew(-12deg)_translateX(-150%)] group-hover:duration-1000 group-hover:[transform:skew(-12deg)_translateX(150%)]">
|
<div className="absolute inset-0 flex h-full w-full justify-center [transform:skew(-12deg)_translateX(-150%)] group-hover:duration-1000 group-hover:[transform:skew(-12deg)_translateX(150%)]">
|
||||||
<div className="relative h-full w-12 bg-white/20"></div>
|
<div className="relative h-full w-12 bg-foreground/20"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="relative flex items-center gap-2 font-mono font-semibold tracking-wide">
|
<span className="relative flex items-center gap-2 font-mono font-semibold tracking-wide">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ spec:
|
|||||||
mountPath: /app/data
|
mountPath: /app/data
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /api/health
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 10
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
@@ -64,7 +64,7 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /api/health
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const app = new Hono();
|
|||||||
// --- API 路由 ---
|
// --- API 路由 ---
|
||||||
|
|
||||||
// 健康检查路由
|
// 健康检查路由
|
||||||
app.get('/', (c) => {
|
app.get('/api/health', (c) => {
|
||||||
const webhookSecretConfigured = !!config.app.webhookSecret;
|
const webhookSecretConfigured = !!config.app.webhookSecret;
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
|
|||||||
Reference in New Issue
Block a user