From eb5797370ae92097d277a783f2d517ccb026dd91 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 22 Aug 2026 22:46:34 -0300 Subject: [PATCH] feat(dashboard): add CheaperInference sponsor banner and route banner links through the shortener (#11196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New CheaperInferenceSponsorBanner on the dashboard home, same size/shape as KimiSponsorBanner, no version gate (durable partnership). Uses the cheaperinference ProviderIcon and the brand green (#31f889) with the dark ink CTA (contrast, per colors.ts token). - CTA points at https://link.omniroute.online/cheaper — the branded short link — so clicks land in our Kutt metrics. - VscodeCopilotBanner CTA now points at https://link.omniroute.online/vsx instead of the raw Marketplace URL, for the same reason. - i18n strings in en + pt (en is the namespace-level fallback for the other 41 locales). - Tests: new cheaperInferenceSponsorBanner.test.tsx (render, CTA href, dismiss persistence); vscodeCopilotBanner.test.tsx updated to the new CTA URL. Co-authored-by: Xiangzhe --- .../CheaperInferenceSponsorBanner.tsx | 108 ++++++++++++++++++ .../dashboard/VscodeCopilotBanner.tsx | 4 +- src/app/(dashboard)/home/page.tsx | 2 + src/i18n/messages/en.json | 7 ++ src/i18n/messages/pt.json | 7 ++ .../ui/cheaperInferenceSponsorBanner.test.tsx | 92 +++++++++++++++ tests/unit/ui/vscodeCopilotBanner.test.tsx | 2 +- 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/CheaperInferenceSponsorBanner.tsx create mode 100644 tests/unit/ui/cheaperInferenceSponsorBanner.test.tsx diff --git a/src/app/(dashboard)/dashboard/CheaperInferenceSponsorBanner.tsx b/src/app/(dashboard)/dashboard/CheaperInferenceSponsorBanner.tsx new file mode 100644 index 0000000000..77cf193298 --- /dev/null +++ b/src/app/(dashboard)/dashboard/CheaperInferenceSponsorBanner.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useSyncExternalStore } from "react"; +import { useTranslations } from "next-intl"; +import ProviderIcon from "@/shared/components/ProviderIcon"; + +// Branded short link through our own link.omniroute.online shortener, so the +// click lands in our Kutt metrics. Points at cheaperinference.com?utm_source=omniroute +// (the URL in README.md's Open Source Friends section). Keep in sync with the +// `cheaper` slug on the shortener. +const CHEAPER_INFERENCE_URL = "https://link.omniroute.online/cheaper"; + +// Cheaper Inference brand green (#31f889). White text on it fails contrast, so +// the CTA pairs it with the dark ink from the provider's color token (colors.ts: +// cheaperinference.text = #04170d). Hex values stay in sync with that token. + +const DISMISS_STORAGE_KEY = "omniroute-cheaperinference-sponsor-banner-dismissed-v1"; +// Same-tab signal for the dismiss button, since writing localStorage doesn't +// fire a "storage" event in the tab that wrote it. +const DISMISS_EVENT = "omniroute:cheaperinference-sponsor-banner-dismissed"; + +function isNotDismissed(): boolean { + try { + return !localStorage.getItem(DISMISS_STORAGE_KEY); + } catch { + return true; + } +} + +function subscribe(callback: () => void) { + window.addEventListener(DISMISS_EVENT, callback); + return () => window.removeEventListener(DISMISS_EVENT, callback); +} + +// SSR has no localStorage, so the server always renders the banner visible; +// useSyncExternalStore reconciles that against the real client-side value +// right after hydration, mirroring KimiSponsorBanner's pattern. +function getServerSnapshot() { + return true; +} + +/** + * Dismissable banner announcing the Cheaper Inference OmniRoute partnership on + * the dashboard home page — same size/shape as KimiSponsorBanner, no version + * gate (durable partnership, not a time-boxed offer). The logomark reuses + * . + */ +export default function CheaperInferenceSponsorBanner() { + const t = useTranslations("cheaperInferenceSponsorBanner"); + const visible = useSyncExternalStore(subscribe, isNotDismissed, getServerSnapshot); + + if (!visible) { + return null; + } + + const dismiss = () => { + try { + localStorage.setItem(DISMISS_STORAGE_KEY, "true"); + } catch { + // ignore — worst case the banner reappears next visit + } + window.dispatchEvent(new Event(DISMISS_EVENT)); + }; + + return ( +
+
+
+ +
+
+

{t("title")}

+

{t("description")}

+
+
+ +
+
+ + {t("cta")} + + + {t("partnerLinkNote")} +
+ +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/VscodeCopilotBanner.tsx b/src/app/(dashboard)/dashboard/VscodeCopilotBanner.tsx index 0b23976531..1715f17c83 100644 --- a/src/app/(dashboard)/dashboard/VscodeCopilotBanner.tsx +++ b/src/app/(dashboard)/dashboard/VscodeCopilotBanner.tsx @@ -6,7 +6,9 @@ import { useTranslations } from "next-intl"; // Marketplace listing is the primary CTA; Open VSX (Cursor/Windsurf/VSCodium/etc.) // is called out via secondaryNote instead of a second button, to keep this banner // the same size as KimiSponsorBanner. -const MARKETPLACE_URL = "https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot"; +// Branded short link through our own link.omniroute.online shortener (the `vsx` +// slug), so the click lands in our Kutt metrics. +const MARKETPLACE_URL = "https://link.omniroute.online/vsx"; const DISMISS_STORAGE_KEY = "omniroute-vscode-copilot-banner-dismissed-v1"; // Same-tab signal for the dismiss button, since writing localStorage doesn't diff --git a/src/app/(dashboard)/home/page.tsx b/src/app/(dashboard)/home/page.tsx index beccde2261..bc10df88f4 100644 --- a/src/app/(dashboard)/home/page.tsx +++ b/src/app/(dashboard)/home/page.tsx @@ -4,6 +4,7 @@ import { getSettings } from "@/lib/localDb"; import HomePageClient from "../dashboard/HomePageClient"; import BootstrapBanner from "../dashboard/BootstrapBanner"; import KimiSponsorBanner from "../dashboard/KimiSponsorBanner"; +import CheaperInferenceSponsorBanner from "../dashboard/CheaperInferenceSponsorBanner"; import VscodeCopilotBanner from "../dashboard/VscodeCopilotBanner"; import NewsBanner from "../dashboard/NewsBanner"; @@ -20,6 +21,7 @@ export default async function HomePage() { <> {isBootstrapped && } + diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index ff778753c6..930f5b5958 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -13869,5 +13869,12 @@ "toolsMismatch": "Provider does not support tool calling", "structuredOutputMismatch": "Provider does not support structured output", "contextWindowMismatch": "Request exceeds provider context window" + }, + "cheaperInferenceSponsorBanner": { + "title": "Cheaper Inference is an OmniRoute Open Source Friend", + "description": "A cost-ranked gateway reselling dozens of frontier models behind one OpenAI-compatible endpoint — routing each request to the cheapest eligible provider, never above list price.", + "cta": "Get an API Key", + "partnerLinkNote": "Partner link", + "dismissAriaLabel": "Dismiss" } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 11a3606a54..1921f3fba7 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -13845,5 +13845,12 @@ "toolsMismatch": "Provider does not support tool calling", "structuredOutputMismatch": "Provider does not support structured output", "contextWindowMismatch": "Request exceeds provider context window" + }, + "cheaperInferenceSponsorBanner": { + "title": "A Cheaper Inference é uma Amiga do Código Aberto do OmniRoute", + "description": "Um gateway com custo ordenado que revende dezenas de modelos de fronteira num único endpoint compatível com OpenAI — roteando cada requisição ao provedor elegível mais barato, nunca acima do preço de tabela.", + "cta": "Obter uma Chave de API", + "partnerLinkNote": "Link de parceiro", + "dismissAriaLabel": "Dispensar" } } diff --git a/tests/unit/ui/cheaperInferenceSponsorBanner.test.tsx b/tests/unit/ui/cheaperInferenceSponsorBanner.test.tsx new file mode 100644 index 0000000000..b86f4188c6 --- /dev/null +++ b/tests/unit/ui/cheaperInferenceSponsorBanner.test.tsx @@ -0,0 +1,92 @@ +// @vitest-environment jsdom +/** + * CheaperInferenceSponsorBanner — render gate (localStorage dismissal), CTA + * pointing at our link.omniroute.online branded short link, and discreet + * partner-link note. Mirrors kimiSponsorBanner.test.tsx, minus the version gate + * (this banner is a durable partnership, not a time-boxed offer). + */ +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const STORAGE_KEY = "omniroute-cheaperinference-sponsor-banner-dismissed-v1"; +const DISMISS_EVENT = "omniroute:cheaperinference-sponsor-banner-dismissed"; +const SHORT_URL = "https://link.omniroute.online/cheaper"; + +vi.mock("next-intl", () => ({ useTranslations: () => (k: string) => k })); +vi.mock("@/shared/components/ProviderIcon", () => ({ default: () => null })); + +async function renderBanner(): Promise { + vi.resetModules(); + const { default: CheaperInferenceSponsorBanner } = + await import("../../../src/app/(dashboard)/dashboard/CheaperInferenceSponsorBanner"); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + act(() => { + root.render(); + }); + return container; +} + +describe("CheaperInferenceSponsorBanner", () => { + beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + localStorage.removeItem(STORAGE_KEY); + }); + + afterEach(() => { + document.body.innerHTML = ""; + localStorage.removeItem(STORAGE_KEY); + }); + + it("renders with the CTA pointing at the branded short link", async () => { + const container = await renderBanner(); + expect(container.textContent).toContain("title"); + expect(container.textContent).toContain("cta"); + const link = container.querySelector("a[href]"); + expect(link).not.toBeNull(); + expect(link?.getAttribute("href")).toBe(SHORT_URL); + expect(link?.getAttribute("target")).toBe("_blank"); + expect(link?.getAttribute("rel")).toContain("noopener"); + }); + + it("shows the discreet partner-link note near the CTA", async () => { + const container = await renderBanner(); + expect(container.textContent).toContain("partnerLinkNote"); + const link = container.querySelector("a[href]"); + expect(link?.getAttribute("title")).toBe("partnerLinkNote"); + }); + + it("hides after dismissal and stays hidden on re-render", async () => { + const first = await renderBanner(); + const button = first.querySelector("button"); + expect(button).not.toBeNull(); + act(() => { + button?.click(); + }); + expect(localStorage.getItem(STORAGE_KEY)).toBe("true"); + expect(first.textContent).not.toContain("title"); + + // a fresh render (simulating a later visit) stays hidden + const second = await renderBanner(); + expect(second.textContent).not.toContain("title"); + }); + + it("re-renders visible again only after the key is cleared", async () => { + const first = await renderBanner(); + const button = first.querySelector("button"); + act(() => { + button?.click(); + }); + expect(localStorage.getItem(STORAGE_KEY)).toBe("true"); + + localStorage.removeItem(STORAGE_KEY); + const second = await renderBanner(); + expect(second.textContent).toContain("title"); + }); +}); diff --git a/tests/unit/ui/vscodeCopilotBanner.test.tsx b/tests/unit/ui/vscodeCopilotBanner.test.tsx index ed75ad4950..fad50b2b26 100644 --- a/tests/unit/ui/vscodeCopilotBanner.test.tsx +++ b/tests/unit/ui/vscodeCopilotBanner.test.tsx @@ -11,7 +11,7 @@ import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const STORAGE_KEY = "omniroute-vscode-copilot-banner-dismissed-v1"; -const MARKETPLACE_URL = "https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot"; +const MARKETPLACE_URL = "https://link.omniroute.online/vsx"; vi.mock("next-intl", () => ({ useTranslations: () => (k: string) => k }));