import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation, useNavigate, } from "react-router-dom"; import AppLayout from "@cloudscape-design/components/app-layout"; import Badge from "@cloudscape-design/components/badge"; import Button from "@cloudscape-design/components/button"; import SideNavigation from "@cloudscape-design/components/side-navigation"; import TopNavigation from "@cloudscape-design/components/top-navigation"; import Flashbar, { type FlashbarProps, } from "@cloudscape-design/components/flashbar"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Helmet } from "react-helmet-async"; import { PoolsList } from "./pages/PoolsList"; import { ClaimDetail } from "./pages/ClaimDetail"; import { InstanceDetail } from "./pages/InstanceDetail"; import { PoolDetail } from "./pages/PoolDetail"; import { PoolNew } from "./pages/PoolNew"; import { UserApiKeys } from "./pages/UserApiKeys"; import { Settings } from "./pages/Settings"; import { AgentChat } from "./pages/AgentChat"; import { ArchivedThreads } from "./pages/ArchivedThreads"; import { BillingUsagePage } from "./pages/BillingUsage"; import { PageShell } from "./components/PageShell"; import { FeatureFlags } from "./pages/FeatureFlags"; import { FeatureFlagProvider, useFeatureFlags, } from "./components/FeatureFlagContext"; import { AdminRoute } from "./components/AdminRoute"; import { FlashContext, type FlashMsg } from "./components/FlashContext"; import { logout, userInfo } from "./auth/keycloak"; import cuaMark from "@cua/design/assets/brand/cua-mark-white.svg"; import designTokens from "@cua/design/tokens.json"; import { localVisualPreviewPath } from "./local-visual-preview"; import { ChatThreadsProvider } from "./chat/ChatThreadsContext"; import { isThreadNavigationHref, useThreadNavigation, } from "./components/ThreadNavigation"; import { useI18n } from "./i18n/I18nProvider"; const VERSION_CHECK_INTERVAL_MS = 60_000; // Cloudscape icon paths (@cloudscape-design/components/icon), inlined as // real DOM nodes so they inherit `currentColor` from the nav link's text // color — matches hover/active states without a second theming pass. const SVG_OPEN = '`, history: `${SVG_OPEN}`, key: `${SVG_OPEN}`, settings: `${SVG_OPEN}`, flag: `${SVG_OPEN}`, "add-plus": `${SVG_OPEN}`, folder: `${SVG_OPEN}`, }; function TitledPage({ title, children }: { title: string; children: ReactNode }) { return ( <> {title} · Cua {children} ); } function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState( () => window.matchMedia(query).matches, ); useEffect(() => { const media = window.matchMedia(query); const update = () => setMatches(media.matches); media.addEventListener("change", update); return () => media.removeEventListener("change", update); }, [query]); return matches; } function useStaleCheck(): { stale: boolean } { const [stale, setStale] = useState(false); const currentEntry = useRef(null); useEffect(() => { if (stale) return; // The entry script is what's actually executing; lazy chunk hashes // changing don't make the running app stale on their own. const entry = Array.from( document.querySelectorAll( 'script[type="module"][src]', ), ) .map((s) => s.src) .find((src) => src.includes("/assets/")); if (!entry) return; currentEntry.current = entry; const check = async () => { try { // High-signal: if the CDN no longer serves our entry chunk, // the deploy invalidated us — next dynamic import would fail anyway. const head = await fetch(currentEntry.current!, { method: "HEAD", cache: "no-cache", }); if (head.status === 404) { setStale(true); return; } const res = await fetch("/", { cache: "no-cache" }); const html = await res.text(); const match = html.match( /]+type="module"[^>]+src="(\/assets\/[^"']+)"/, ); const latestEntry = match?.[1]; if (latestEntry && !currentEntry.current!.endsWith(latestEntry)) { setStale(true); } } catch { // Network error — ignore } }; const id = window.setInterval(check, VERSION_CHECK_INTERVAL_MS); return () => window.clearInterval(id); }, [stale]); return { stale }; } function Shell() { const [flashes, setFlashes] = useState([]); const pushFlash = useCallback((msg: FlashMsg) => { const id = crypto.randomUUID(); const dismiss = () => setFlashes((prev) => prev.filter((flash) => flash.id !== id)); setFlashes((prev) => [ ...prev, { ...msg, id, dismissible: true, onDismiss: dismiss }, ]); if (msg.type === "success") window.setTimeout(dismiss, 5000); }, []); const flashContext = useMemo(() => ({ push: pushFlash }), [pushFlash]); return ( ); } function ShellLayout({ flashes, }: { flashes: FlashbarProps.MessageDefinition[]; }) { const location = useLocation(); const navigate = useNavigate(); const [staleDismissed, setStaleDismissed] = useState(false); const { stale } = useStaleCheck(); const { t } = useI18n(); const user = userInfo(); const { admin, chat } = useFeatureFlags(); const threadNavigation = useThreadNavigation(); const tabletOrWider = useMediaQuery( `(min-width: ${designTokens.layout.breakpoint.tablet}px)`, ); const [navigationOpen, setNavigationOpen] = useState( () => window.innerWidth >= designTokens.layout.breakpoint.tablet, ); useEffect(() => { if (tabletOrWider) setNavigationOpen(true); }, [tabletOrWider]); useEffect(() => { const titlesByHref = new Map( threadNavigation.threadTitles.map(({ href, title }) => [href, title]), ); for (const link of document.querySelectorAll( ".cua-shell__nav a", )) { const title = titlesByHref.get(link.getAttribute("href") ?? ""); if (title) link.title = title; } }, [threadNavigation.threadTitles]); useEffect(() => { const iconsByHrefPrefix: Array<[string, string]> = [ ["#/pools", NAV_ICONS["grid-view"]], ["#/usage", NAV_ICONS.history], ["#/user-keys", NAV_ICONS.key], ["#/settings", NAV_ICONS.settings], ["#/admin/feature-flags", NAV_ICONS.flag], ["#/agent/new", NAV_ICONS["add-plus"]], ["#/agent/archived", NAV_ICONS.folder], ]; for (const link of document.querySelectorAll( ".cua-shell__nav a", )) { if (link.querySelector(".cua-nav-icon")) continue; const href = link.getAttribute("href") ?? ""; const icon = iconsByHrefPrefix.find(([prefix]) => href.startsWith(prefix))?.[1]; if (!icon) continue; const span = document.createElement("span"); span.className = "cua-nav-icon"; span.innerHTML = icon; link.insertBefore(span, link.firstChild); } }); return (
{ if (event.detail.id === "signout") logout(); }, }, ]} />
{ // Desktop/tablet stays pinned open; only mobile's drawer toggle // (shown via CSS below designTokens.layout.breakpoint.tablet) // can actually close it. if (tabletOrWider) return; setNavigationOpen(detail.open); }} navigation={
{ if (event.detail.external) return; event.preventDefault(); const path = event.detail.href.replace(/^#/, ""); if ( isThreadNavigationHref(event.detail.href) && threadNavigation.generationLocked ) return; if (path === "/agent/new") { if (threadNavigation.isCreatePending()) return; void threadNavigation.createThread(); return; } if (path === "/agent/retry") return; navigate(localVisualPreviewPath(path)); }} items={[ { type: "link", text: t("navigation.pools"), href: "#/pools" }, { type: "link", text: t("navigation.usage"), href: "#/usage" }, { type: "link", text: t("navigation.apiKeys"), href: "#/user-keys" }, { type: "link", text: t("navigation.settings"), href: "#/settings" }, ...(admin ? [ { type: "link" as const, text: t("navigation.featureFlags"), href: "#/admin/feature-flags", info: {t("common.admin")}, }, ] : []), ...(chat ? [{ type: "divider" as const }, ...threadNavigation.items] : []), ]} />
} notifications={ window.location.reload()}> {t("common.refreshNow")} ), dismissible: true, onDismiss: () => setStaleDismissed(true), id: "__stale__", }, ] : []), ...flashes, ]} stackItems /> } content={} contentType="default" />
); } export function App() { const { t } = useI18n(); return ( }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> ); } function RedirectModule() { const path = window.location.pathname.replace(/^\/modules/, "/pools"); return ; } function ChatRoute() { const { chat, resolved } = useFeatureFlags(); const { t } = useI18n(); if (!resolved) { return (
); } return chat ? : ; } function ArchivedThreadsRoute() { const { chat, resolved } = useFeatureFlags(); const { t } = useI18n(); if (!resolved) return (
); return chat ? : ; }