Files
OmniRoute/src/shared/components/Header.tsx
Diego Rodrigues de Sa e Souza 7388623244 fix(combo): fallback to next model on all-accounts-rate-limited 503 (… (#1523)
Integrated into release/v3.7.0
2026-04-23 01:53:00 -03:00

249 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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: [] };
// 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 }) {
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>
);
}
Header.propTypes = {
onMenuClick: PropTypes.func,
showMenuButton: PropTypes.bool,
};