From 37a8c979d5cded8c94f5857f1edeaf77dc8b53cc Mon Sep 17 00:00:00 2001 From: River Date: Tue, 18 Aug 2026 20:05:29 +0000 Subject: [PATCH 1/3] feat: Add Prototype Banner settings panel Banner content comes from the backend instead of a frontend deploy. Banners are stored in user settings under the `system:web_ui/banners` key as a newest-first list of `{iso_timestamp: banner_text}`. Settings -> Prototype Banner (below AI Configuration) saves a new banner, shows the latest one, and lists the full history. Writes go through `PATCH /api/users/me/settings`, reads through `GET /api/users/me/settings`. Prototype scope: `/api/users/me/settings` keys the row by the authenticated caller, so a banner is visible only to the person who saved it. A server-pinned route reading the same key for `system:web_ui` makes it global without client changes beyond the URL. Requested by Yue Chao Qin Co-authored-by: Yue Chao Qin --- src/routes/Settings/SettingsLayout.tsx | 16 +- .../sections/PrototypeBannerSettings.tsx | 169 ++++++++++++++++++ .../sections/prototypeBanners.test.ts | 56 ++++++ .../Settings/sections/prototypeBanners.ts | 124 +++++++++++++ src/routes/appRoutes.ts | 1 + src/routes/router.ts | 8 + src/utils/constants.ts | 4 + 7 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 src/routes/Settings/sections/PrototypeBannerSettings.tsx create mode 100644 src/routes/Settings/sections/prototypeBanners.test.ts create mode 100644 src/routes/Settings/sections/prototypeBanners.ts diff --git a/src/routes/Settings/SettingsLayout.tsx b/src/routes/Settings/SettingsLayout.tsx index db1e04704b..3697d2738e 100644 --- a/src/routes/Settings/SettingsLayout.tsx +++ b/src/routes/Settings/SettingsLayout.tsx @@ -50,14 +50,22 @@ const AGENT_ITEM: SidebarItem = { testId: "settings-nav-agent", }; +const PROTOTYPE_BANNER_ITEM: SidebarItem = { + to: "/settings/prototype-banner", + label: "Prototype Banner", + icon: "Megaphone", + testId: "settings-nav-prototype-banner", +}; + export function SettingsLayout() { const router = useRouter(); const componentSearchEnabled = useFlagValue("component-search-v2"); const aiAssistantEnabled = useFlagValue("ai-assistant"); - const sidebarItems = - componentSearchEnabled || aiAssistantEnabled - ? [...SIDEBAR_ITEMS, AGENT_ITEM] - : SIDEBAR_ITEMS; + const sidebarItems = [ + ...SIDEBAR_ITEMS, + ...(componentSearchEnabled || aiAssistantEnabled ? [AGENT_ITEM] : []), + PROTOTYPE_BANNER_ITEM, + ]; const handleGoBack = () => { router.history.back(); diff --git a/src/routes/Settings/sections/PrototypeBannerSettings.tsx b/src/routes/Settings/sections/PrototypeBannerSettings.tsx new file mode 100644 index 0000000000..673fc2e20e --- /dev/null +++ b/src/routes/Settings/sections/PrototypeBannerSettings.tsx @@ -0,0 +1,169 @@ +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { Heading, Paragraph, Text } from "@/components/ui/typography"; +import useToastNotification from "@/hooks/useToastNotification"; +import { useBackend } from "@/providers/BackendProvider"; +import { SYSTEM_UI_USER_ID } from "@/utils/constants"; + +import { + bannerText, + bannerTimestamp, + latestBanner, + type PrototypeBanner, + usePrototypeBanners, + useSavePrototypeBanner, +} from "./prototypeBanners"; + +function formatTimestamp(timestamp: string): string { + const date = new Date(timestamp); + return Number.isNaN(date.getTime()) ? timestamp : date.toLocaleString(); +} + +function BannerRow({ banner }: { banner: PrototypeBanner }) { + return ( + + + {formatTimestamp(bannerTimestamp(banner))} + + {bannerText(banner)} + + ); +} + +/** + * Prototype panel for banner content served from the backend instead of a + * frontend deploy. Banners are stored as one JSON list in user settings under + * the `system:web_ui` namespace. + */ +export function PrototypeBannerSettings() { + const notify = useToastNotification(); + const { available } = useBackend(); + const [draft, setDraft] = useState(""); + + const { + data: banners = [], + isLoading, + isFetching, + error, + refetch, + } = usePrototypeBanners(); + const { mutate: saveBanner, isPending } = useSavePrototypeBanner(); + + const latest = latestBanner(banners); + const trimmedDraft = draft.trim(); + + const handleSave = () => { + if (!trimmedDraft) return; + + saveBanner(trimmedDraft, { + onSuccess: () => { + setDraft(""); + notify("Banner saved", "success"); + }, + onError: (saveError) => { + notify(`Failed to save banner: ${saveError.message}`, "error"); + }, + }); + }; + + return ( + + + Prototype Banner + + Store banner text in the backend so it can change without a frontend + deploy. Banners are kept as a history list under the{" "} + {SYSTEM_UI_USER_ID} settings namespace. + + + + + + {!available ? ( + + Connect a backend to read and write banners. + + ) : ( + + + + Latest banner + + +
+ {isLoading ? ( + + Loading… + + ) : error ? ( + + {error.message} + + ) : latest ? ( + + ) : ( + + No banners yet. + + )} +
+
+ + + + + New banner +