Spaces:
Paused
Paused
| "use client"; | |
| import { usePathname, useRouter } from "next/navigation"; | |
| import Link from "next/link"; | |
| import Image from "next/image"; | |
| import ThemeToggle from "./ThemeToggle"; | |
| import TokenHealthBadge from "./TokenHealthBadge"; | |
| import DegradationBadge from "./DegradationBadge"; | |
| import LanguageSelector from "./LanguageSelector"; | |
| import ProviderIcon from "./ProviderIcon"; | |
| import { useTranslations } from "next-intl"; | |
| import { | |
| OAUTH_PROVIDERS, | |
| APIKEY_PROVIDERS, | |
| FREE_PROVIDERS, | |
| CLAUDE_CODE_COMPATIBLE_PREFIX, | |
| OPENAI_COMPATIBLE_PREFIX, | |
| ANTHROPIC_COMPATIBLE_PREFIX, | |
| } from "@/shared/constants/providers"; | |
| import { useIsElectron } from "@/shared/hooks/useElectron"; | |
| const isE2EMode = process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE === "1"; | |
| type HeaderProps = { | |
| onMenuClick?: () => void; | |
| showMenuButton?: boolean; | |
| }; | |
| function usePageInfo(pathname: string | null): { | |
| title: string; | |
| description: string; | |
| breadcrumbs: { label: string; href?: string; image?: string; providerId?: string }[]; | |
| } { | |
| const t = useTranslations("header"); | |
| if (!pathname) return { title: "", description: "", breadcrumbs: [] }; | |
| // Provider detail page: /dashboard/providers/[id] | |
| const providerMatch = pathname.match(/\/providers\/([^/]+)$/); | |
| if (providerMatch) { | |
| const providerId = providerMatch[1]; | |
| const providerInfo = | |
| OAUTH_PROVIDERS[providerId] || FREE_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId]; | |
| if (providerInfo) { | |
| return { | |
| title: providerInfo.name, | |
| description: "", | |
| breadcrumbs: [ | |
| { label: t("providers"), href: "/dashboard/providers" }, | |
| { label: providerInfo.name, providerId: providerInfo.id }, | |
| ], | |
| }; | |
| } | |
| if (providerId.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) { | |
| return { | |
| title: "CC Compatible", | |
| description: "", | |
| breadcrumbs: [ | |
| { label: t("providers"), href: "/dashboard/providers" }, | |
| { label: "CC Compatible", providerId: "claude" }, | |
| ], | |
| }; | |
| } | |
| if (providerId.startsWith(OPENAI_COMPATIBLE_PREFIX)) { | |
| return { | |
| title: t("openaiCompatible"), | |
| description: "", | |
| breadcrumbs: [ | |
| { label: t("providers"), href: "/dashboard/providers" }, | |
| { label: t("openaiCompatible"), providerId: "oai-cc" }, | |
| ], | |
| }; | |
| } | |
| if (providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX)) { | |
| return { | |
| title: t("anthropicCompatible"), | |
| description: "", | |
| breadcrumbs: [ | |
| { label: t("providers"), href: "/dashboard/providers" }, | |
| { label: t("anthropicCompatible"), providerId: "anthropic-m" }, | |
| ], | |
| }; | |
| } | |
| } | |
| if (pathname.includes("/providers")) | |
| return { | |
| title: t("providers"), | |
| description: t("providerDescription"), | |
| breadcrumbs: [], | |
| }; | |
| if (pathname.includes("/combos")) | |
| return { title: t("combos"), description: t("comboDescription"), breadcrumbs: [] }; | |
| if (pathname.includes("/usage")) | |
| return { | |
| title: t("usage"), | |
| description: t("usageDescription"), | |
| breadcrumbs: [], | |
| }; | |
| if (pathname.includes("/analytics")) | |
| return { | |
| title: t("analytics"), | |
| description: t("analyticsDescription"), | |
| breadcrumbs: [], | |
| }; | |
| if (pathname.includes("/cli-tools")) | |
| return { title: t("cliTools"), description: t("cliToolsDescription"), breadcrumbs: [] }; | |
| if (pathname === "/dashboard") | |
| return { title: t("home"), description: t("homeDescription"), breadcrumbs: [] }; | |
| if (pathname.includes("/mcp")) | |
| return { title: t("mcp"), description: t("mcpDescription"), breadcrumbs: [] }; | |
| if (pathname.includes("/a2a")) | |
| return { title: t("a2a"), description: t("a2aDescription"), breadcrumbs: [] }; | |
| if (pathname.includes("/endpoint")) | |
| return { title: t("endpoint"), description: t("endpointDescription"), breadcrumbs: [] }; | |
| if (pathname.includes("/profile")) | |
| return { title: t("settings"), description: t("settingsDescription"), breadcrumbs: [] }; | |
| // Note: /themes page removed – theme settings live in /settings → AppearanceTab | |
| return { title: "", description: "", breadcrumbs: [] }; | |
| } | |
| export default function Header({ onMenuClick, showMenuButton = true }: HeaderProps) { | |
| const pathname = usePathname(); | |
| const router = useRouter(); | |
| const isElectron = useIsElectron(); | |
| const t = useTranslations("header"); | |
| const { title, description, breadcrumbs } = usePageInfo(pathname); | |
| const isMacElectron = | |
| isElectron && | |
| typeof window !== "undefined" && | |
| (window as any).electronAPI?.platform === "darwin"; | |
| const handleLogout = async () => { | |
| try { | |
| const res = await fetch("/api/auth/logout", { method: "POST" }); | |
| if (res.ok) { | |
| router.push("/login"); | |
| router.refresh(); | |
| } | |
| } catch (err) { | |
| console.error("Failed to logout:", err); | |
| } | |
| }; | |
| return ( | |
| <header | |
| className="sticky top-0 z-10 flex items-center justify-between border-b border-black/5 bg-bg px-8 py-5 dark:border-white/5" | |
| style={{ | |
| paddingTop: isMacElectron ? "calc(1.25rem + var(--desktop-safe-top))" : undefined, | |
| }} | |
| > | |
| {/* Mobile menu button */} | |
| <div className="flex items-center gap-3 lg:hidden"> | |
| {showMenuButton && ( | |
| <button | |
| onClick={onMenuClick} | |
| className="text-text-main hover:text-primary transition-colors" | |
| > | |
| <span className="material-symbols-outlined">menu</span> | |
| </button> | |
| )} | |
| </div> | |
| {/* Page title with breadcrumbs - desktop */} | |
| <div className="hidden lg:flex flex-col"> | |
| {breadcrumbs.length > 0 ? ( | |
| <div className="flex items-center gap-2"> | |
| {breadcrumbs.map((crumb, index) => ( | |
| <div | |
| key={`${crumb.label}-${crumb.href || "current"}`} | |
| className="flex items-center gap-2" | |
| > | |
| {index > 0 && ( | |
| <span className="material-symbols-outlined text-text-muted text-base"> | |
| chevron_right | |
| </span> | |
| )} | |
| {crumb.href ? ( | |
| <Link | |
| href={crumb.href} | |
| className="text-text-muted hover:text-primary transition-colors" | |
| > | |
| {crumb.label} | |
| </Link> | |
| ) : ( | |
| <div className="flex items-center gap-2"> | |
| {crumb.image && ( | |
| <Image | |
| src={crumb.image} | |
| alt={crumb.label} | |
| width={28} | |
| height={28} | |
| className="object-contain rounded max-w-[28px] max-h-[28px]" | |
| sizes="28px" | |
| onError={(e) => { | |
| e.currentTarget.style.display = "none"; | |
| }} | |
| /> | |
| )} | |
| {crumb.providerId && ( | |
| <ProviderIcon providerId={crumb.providerId} size={28} type="color" /> | |
| )} | |
| <h1 className="text-2xl font-semibold text-text-main tracking-tight"> | |
| {crumb.label} | |
| </h1> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| ) : title ? ( | |
| <div> | |
| <h1 className="text-2xl font-semibold text-text-main tracking-tight">{title}</h1> | |
| {description && <p className="text-sm text-text-muted">{description}</p>} | |
| </div> | |
| ) : null} | |
| </div> | |
| {/* Right actions */} | |
| <div className="flex items-center gap-3 ml-auto"> | |
| {/* Language selector */} | |
| <LanguageSelector /> | |
| {/* Theme toggle */} | |
| <ThemeToggle /> | |
| {/* Degradation & Token health */} | |
| {!isE2EMode && <DegradationBadge />} | |
| {!isE2EMode && <TokenHealthBadge />} | |
| {/* Logout button */} | |
| <button | |
| onClick={handleLogout} | |
| className="flex items-center justify-center p-2 rounded-lg text-text-muted hover:text-red-500 hover:bg-red-500/10 transition-all" | |
| title={t("logout")} | |
| > | |
| <span className="material-symbols-outlined">logout</span> | |
| </button> | |
| </div> | |
| </header> | |
| ); | |
| } | |