From 8f9d806eef3a6d77c3c7b017f8ff23c1177a649b Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Mon, 27 Apr 2026 14:18:01 -0500 Subject: [PATCH] feat(demo): replace blur with asterisk masking of user data via React context --- packages/web/src/components/chat/ChatPage.tsx | 4 ++- .../conclusions/ConclusionBrowser.tsx | 10 +++--- .../web/src/components/peers/PeerDetail.tsx | 6 ++-- .../web/src/components/peers/PeerList.tsx | 4 ++- .../src/components/sessions/SessionDetail.tsx | 13 ++++--- packages/web/src/context/DemoContext.tsx | 34 +++++++++++++++++++ packages/web/src/hooks/useDemo.ts | 17 +--------- packages/web/src/index.css | 6 ---- packages/web/src/lib/demo.ts | 4 +++ packages/web/src/main.tsx | 3 -- packages/web/src/routes/__root.tsx | 21 +++++++----- 11 files changed, 75 insertions(+), 47 deletions(-) create mode 100644 packages/web/src/context/DemoContext.tsx diff --git a/packages/web/src/components/chat/ChatPage.tsx b/packages/web/src/components/chat/ChatPage.tsx index cbc3b69..4a51dd1 100644 --- a/packages/web/src/components/chat/ChatPage.tsx +++ b/packages/web/src/components/chat/ChatPage.tsx @@ -7,6 +7,7 @@ import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/input"; import { SectionHeading } from "@/components/ui/typography"; +import { useDemo } from "@/hooks/useDemo"; interface Message { id: string; @@ -15,6 +16,7 @@ interface Message { } export function ChatPage() { + const { mask } = useDemo(); const { workspaceId, peerId } = useParams({ strict: false }) as { workspaceId: string; peerId: string; @@ -143,7 +145,7 @@ export function ChatPage() { } } > -

{msg.content}

+

{mask(msg.content)}

))} diff --git a/packages/web/src/components/conclusions/ConclusionBrowser.tsx b/packages/web/src/components/conclusions/ConclusionBrowser.tsx index 2927315..49f52e6 100644 --- a/packages/web/src/components/conclusions/ConclusionBrowser.tsx +++ b/packages/web/src/components/conclusions/ConclusionBrowser.tsx @@ -22,6 +22,7 @@ import { Button } from "@/components/ui/button"; import { Input, Textarea } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Body, Caption, MonoCaption, Muted, PageTitle } from "@/components/ui/typography"; +import { useDemo } from "@/hooks/useDemo"; import { COLOR } from "@/lib/constants"; type Conclusion = components["schemas"]["Conclusion"]; @@ -49,6 +50,7 @@ const itemVariants = { }; export function ConclusionBrowser() { + const { mask } = useDemo(); const { workspaceId } = useParams({ strict: false }) as { workspaceId: string }; const [page, setPage] = useState(1); const [sortField, setSortField] = useState("created_at"); @@ -233,7 +235,7 @@ export function ConclusionBrowser() { }} >
- {c.content} + {mask(c.content)}
)) )} diff --git a/packages/web/src/components/peers/PeerList.tsx b/packages/web/src/components/peers/PeerList.tsx index f4a7b05..08669e5 100644 --- a/packages/web/src/components/peers/PeerList.tsx +++ b/packages/web/src/components/peers/PeerList.tsx @@ -11,6 +11,7 @@ import { PageLoader } from "@/components/shared/LoadingSpinner"; import { Pagination } from "@/components/shared/Pagination"; import { SortControl, type SortDir } from "@/components/shared/SortControl"; import { MonoCaption, PageTitle } from "@/components/ui/typography"; +import { useDemo } from "@/hooks/useDemo"; import { COLOR } from "@/lib/constants"; type Peer = components["schemas"]["Peer"]; @@ -45,6 +46,7 @@ const item: Variants = { }; export function PeerList() { + const { mask } = useDemo(); const { workspaceId } = useParams({ strict: false }) as { workspaceId: string }; const [page, setPage] = useState(1); const [sortField, setSortField] = useState("created_at"); @@ -246,7 +248,7 @@ export function PeerList() { className="font-mono text-sm font-medium truncate" style={{ color: COLOR.accentSoft }} > - {peer.id} + {mask(peer.id)} - {r.peer_id && {r.peer_id}} -

{r.content}

+ {r.peer_id && {mask(r.peer_id)}} +

{mask(r.content)}

)) )} @@ -269,14 +271,14 @@ export function SessionDetail() { >
- {msg.peer_id ?? "system"} + {msg.peer_id ? mask(msg.peer_id) : "system"} {msg.token_count != null && {msg.token_count} tokens} {msg.created_at && ( {new Date(msg.created_at).toLocaleString()} )}
- {msg.content} + {mask(msg.content)} ))} @@ -414,6 +416,7 @@ function SessionPeersTab({ } function SummaryCard({ label, summary }: { label: string; summary: Summary }) { + const { mask } = useDemo(); return (
- {summary.content} + {mask(summary.content)} ); } diff --git a/packages/web/src/context/DemoContext.tsx b/packages/web/src/context/DemoContext.tsx new file mode 100644 index 0000000..08eb8fc --- /dev/null +++ b/packages/web/src/context/DemoContext.tsx @@ -0,0 +1,34 @@ +import { createContext, type ReactNode, useContext, useEffect, useState } from "react"; +import { applyDemoMode, getDemoMode, maskValue } from "@/lib/demo"; + +interface DemoContextValue { + demo: boolean; + toggle: () => void; + mask: (value: string) => string; +} + +const DemoContext = createContext(null); + +export function DemoProvider({ children }: { children: ReactNode }) { + const [demo, setDemo] = useState(() => getDemoMode()); + + useEffect(() => { + applyDemoMode(demo); + }, [demo]); + + function toggle() { + setDemo((d) => !d); + } + + function mask(value: string): string { + return demo ? maskValue(value) : value; + } + + return {children}; +} + +export function useDemoContext(): DemoContextValue { + const ctx = useContext(DemoContext); + if (!ctx) throw new Error("useDemoContext must be used within DemoProvider"); + return ctx; +} diff --git a/packages/web/src/hooks/useDemo.ts b/packages/web/src/hooks/useDemo.ts index 7467cce..fcde7e4 100644 --- a/packages/web/src/hooks/useDemo.ts +++ b/packages/web/src/hooks/useDemo.ts @@ -1,16 +1 @@ -import { useEffect, useState } from "react"; -import { applyDemoMode, getDemoMode } from "@/lib/demo"; - -export function useDemo() { - const [demo, setDemo] = useState(() => getDemoMode()); - - useEffect(() => { - applyDemoMode(demo); - }, [demo]); - - function toggle() { - setDemo((d) => !d); - } - - return { demo, toggle }; -} +export { useDemoContext as useDemo } from "@/context/DemoContext"; diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 6349316..47566f0 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -5,12 +5,6 @@ @import "@fontsource/dm-sans/500.css"; @import "@fontsource/dm-sans/600.css"; -/* Demo mode — blur main content, sidebar remains interactive */ -[data-demo="true"] main { - filter: blur(6px); - user-select: none; -} - /* ─── Tailwind v4 theme bridge ─── */ @theme inline { --color-background: var(--bg); diff --git a/packages/web/src/lib/demo.ts b/packages/web/src/lib/demo.ts index bc58373..f456ec2 100644 --- a/packages/web/src/lib/demo.ts +++ b/packages/web/src/lib/demo.ts @@ -8,3 +8,7 @@ export function applyDemoMode(enabled: boolean): void { document.documentElement.setAttribute("data-demo", String(enabled)); localStorage.setItem(DEMO_KEY, String(enabled)); } + +export function maskValue(value: string): string { + return value.replace(/\S/g, "*"); +} diff --git a/packages/web/src/main.tsx b/packages/web/src/main.tsx index 9c4bf2a..6dc7e15 100644 --- a/packages/web/src/main.tsx +++ b/packages/web/src/main.tsx @@ -2,12 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRouter, RouterProvider } from "@tanstack/react-router"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { applyDemoMode, getDemoMode } from "@/lib/demo"; import { routeTree } from "./routeTree.gen"; import "./index.css"; -applyDemoMode(getDemoMode()); - const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/packages/web/src/routes/__root.tsx b/packages/web/src/routes/__root.tsx index 09d4a40..024c1bc 100644 --- a/packages/web/src/routes/__root.tsx +++ b/packages/web/src/routes/__root.tsx @@ -1,6 +1,7 @@ import { createRootRoute, Outlet, useRouter } from "@tanstack/react-router"; import { useEffect } from "react"; import { Sidebar } from "@/components/layout/Sidebar"; +import { DemoProvider } from "@/context/DemoContext"; import { loadConfig } from "@/lib/config"; import { applyTheme, getStoredTheme } from "@/lib/theme"; @@ -26,15 +27,17 @@ function RootLayout() { if (!config) return null; return ( -
- -
- -
-
+ +
+ +
+ +
+
+
); }