| "use client"; |
|
|
| import { usePathname, useRouter } from "next/navigation"; |
| import Link from "next/link"; |
| import Image from "next/image"; |
| import PropTypes from "prop-types"; |
| 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"; |
|
|
| 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: [] }; |
|
|
| |
| 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: [] }; |
| |
|
|
| return { title: "", description: "", breadcrumbs: [] }; |
| } |
|
|
| export default function Header({ onMenuClick, showMenuButton = true }) { |
| 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.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/80 px-8 py-5 backdrop-blur-xl 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> |
| ); |
| } |
|
|
| Header.propTypes = { |
| onMenuClick: PropTypes.func, |
| showMenuButton: PropTypes.bool, |
| }; |
|
|