import { zodResolver } from "@hookform/resolvers/zod"; import { Box, ChevronDown, Eye, Image, KeyRound, Languages, LayoutDashboard, LogOut, Menu, MessageSquareText, Monitor, Moon, MoreHorizontal, Settings, Sparkles, Sun, Users, Video } from "lucide-react"; import { useTheme } from "next-themes"; import { useState, type ReactNode } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import { toast } from "sonner"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { useAuth } from "@/shared/auth/use-auth"; import { GitHubMark } from "@/shared/components/github-mark"; import { SiteFooter } from "@/shared/components/site-footer"; import { cn } from "@/shared/lib/cn"; import { CurrentVersionLabel } from "@/features/system/version-update"; const navigation = [ { href: "/dashboard", label: "nav.dashboard", icon: LayoutDashboard }, { href: "/accounts", label: "nav.accounts", icon: Users }, { href: "/client-keys", label: "nav.clientKeys", icon: KeyRound }, { href: "/models", label: "nav.models", icon: Box }, { href: "/gallery", label: "nav.gallery", icon: Image }, { href: "/video-gallery", label: "nav.videoGallery", icon: Video }, { href: "/request-audits", label: "nav.audits", icon: Eye }, { href: "/creative-console", label: "nav.creativeConsole", icon: Sparkles }, ] as const; const documentation = [ { label: "Chat", icon: MessageSquareText, items: [ { href: "/docs/chat/completions", label: "Chat Completions", method: "POST" }, { href: "/docs/chat/responses", label: "Responses", method: "POST" }, { href: "/docs/chat/messages", label: "Messages", method: "POST" }, ], }, { label: "Image", icon: Image, items: [ { href: "/docs/image/generations", label: "Image Generations", method: "POST" }, { href: "/docs/image/edits", label: "Image Edits", method: "POST" }, ], }, { label: "Video", icon: Video, items: [ { href: "/docs/video/generations", label: "Video Generations", method: "POST" }, { href: "/docs/video/get", label: "Get Video", method: "GET" }, ], }, ] as const; export function AppShell() { const { t, i18n } = useTranslation(); const { admin, logout, changePassword } = useAuth(); const location = useLocation(); const { setTheme } = useTheme(); const [mobileOpen, setMobileOpen] = useState(false); const [passwordOpen, setPasswordOpen] = useState(false); const [documentationOpen, setDocumentationOpen] = useState>({}); const isMediaWorkspace = ["/creative-console", "/gallery", "/video-gallery"].includes(location.pathname); const passwordSchema = z.object({ currentPassword: z.string().min(1, t("errors.required")), newPassword: z.string().min(8, t("errors.minPassword")), }); type PasswordForm = z.infer; const passwordForm = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { currentPassword: "", newPassword: "" }, }); async function submitPassword(values: PasswordForm): Promise { try { await changePassword(values.currentPassword, values.newPassword); toast.success(t("auth.passwordUpdated")); passwordForm.reset(); setPasswordOpen(false); await logout(); } catch (error) { toast.error(error instanceof Error ? error.message : t("errors.generic")); } } function navigationLinks(): ReactNode { return navigation.map(({ href, label, icon: Icon }) => ( setMobileOpen(false)} className={({ isActive }) => cn( "group flex h-8 items-center gap-2 rounded-md px-2.5 text-xs font-normal text-muted-foreground transition-colors hover:bg-secondary/55 hover:text-foreground", isActive && "bg-secondary/60 text-foreground", )} > {({ isActive }) => ( <> {t(label)} )} )); } function documentationLinks(): ReactNode { return documentation.map(({ label, icon: Icon, items }) => { const open = documentationOpen[label] ?? false; return (
{items.map((item) => ( setMobileOpen(false)} className={({ isActive }) => cn( "group flex h-7 min-w-0 items-center gap-2 rounded-md pl-[38px] pr-2.5 text-xs text-muted-foreground transition-colors hover:bg-secondary/55 hover:text-foreground", isActive && "bg-secondary/60 text-foreground", )} > {item.label} {item.method} ))}
); }); } const navigationContent = ( ); const accountControl = (
{admin?.username} {t("shell.appearance")} setTheme("light")}>{t("shell.light")} setTheme("dark")}>{t("shell.dark")} setTheme("system")}>{t("shell.system")} {t("shell.language")} void i18n.changeLanguage("zh-CN")}>简体中文 void i18n.changeLanguage("en")}>English setPasswordOpen(true)}>{t("auth.changePassword")} void logout()}>{t("auth.signOut")} setMobileOpen(false)} className={({ isActive }) => cn("flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary/55 hover:text-foreground", isActive && "bg-secondary/60 text-foreground")} aria-label={t("nav.settings")} >
); return (
{t("appName")} {t("shell.navigation")}
{accountControl}
{t("appName")}
{!isMediaWorkspace ? : null}
{t("auth.changePassword")}{admin?.username}
{passwordForm.formState.errors.currentPassword ?

{passwordForm.formState.errors.currentPassword.message}

: null}
{passwordForm.formState.errors.newPassword ?

{passwordForm.formState.errors.newPassword.message}

: null}
); }