Skip to content

Commit 8f9d806

Browse files
feat(demo): replace blur with asterisk masking of user data via React context
1 parent 4f51cea commit 8f9d806

11 files changed

Lines changed: 75 additions & 47 deletions

File tree

packages/web/src/components/chat/ChatPage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
77
import { Button } from "@/components/ui/button";
88
import { Textarea } from "@/components/ui/input";
99
import { SectionHeading } from "@/components/ui/typography";
10+
import { useDemo } from "@/hooks/useDemo";
1011

1112
interface Message {
1213
id: string;
@@ -15,6 +16,7 @@ interface Message {
1516
}
1617

1718
export function ChatPage() {
19+
const { mask } = useDemo();
1820
const { workspaceId, peerId } = useParams({ strict: false }) as {
1921
workspaceId: string;
2022
peerId: string;
@@ -143,7 +145,7 @@ export function ChatPage() {
143145
}
144146
}
145147
>
146-
<p className="whitespace-pre-wrap leading-relaxed">{msg.content}</p>
148+
<p className="whitespace-pre-wrap leading-relaxed">{mask(msg.content)}</p>
147149
</div>
148150
</motion.div>
149151
))}

packages/web/src/components/conclusions/ConclusionBrowser.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Button } from "@/components/ui/button";
2222
import { Input, Textarea } from "@/components/ui/input";
2323
import { Label } from "@/components/ui/label";
2424
import { Body, Caption, MonoCaption, Muted, PageTitle } from "@/components/ui/typography";
25+
import { useDemo } from "@/hooks/useDemo";
2526
import { COLOR } from "@/lib/constants";
2627

2728
type Conclusion = components["schemas"]["Conclusion"];
@@ -49,6 +50,7 @@ const itemVariants = {
4950
};
5051

5152
export function ConclusionBrowser() {
53+
const { mask } = useDemo();
5254
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
5355
const [page, setPage] = useState(1);
5456
const [sortField, setSortField] = useState("created_at");
@@ -233,7 +235,7 @@ export function ConclusionBrowser() {
233235
}}
234236
>
235237
<div className="flex items-start justify-between gap-3">
236-
<Body className="whitespace-pre-wrap flex-1">{c.content}</Body>
238+
<Body className="whitespace-pre-wrap flex-1">{mask(c.content)}</Body>
237239
<Button
238240
variant="ghost"
239241
size="icon"
@@ -250,12 +252,12 @@ export function ConclusionBrowser() {
250252
>
251253
<div className="flex items-center gap-1.5">
252254
<Eye className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} />
253-
<MonoCaption>{c.observer_id}</MonoCaption>
255+
<MonoCaption>{mask(c.observer_id)}</MonoCaption>
254256
</div>
255257
{c.observed_id && (
256258
<div className="flex items-center gap-1">
257259
<Caption></Caption>
258-
<MonoCaption>{c.observed_id}</MonoCaption>
260+
<MonoCaption>{mask(c.observed_id)}</MonoCaption>
259261
</div>
260262
)}
261263
{c.session_id && (
@@ -266,7 +268,7 @@ export function ConclusionBrowser() {
266268
className="flex items-center gap-1 text-xs font-mono hover:underline"
267269
style={{ color: "var(--accent-text)" }}
268270
>
269-
{c.session_id}
271+
{mask(c.session_id)}
270272
</Link>
271273
)}
272274
{c.created_at && (

packages/web/src/components/peers/PeerDetail.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ import {
3636
PageTitle,
3737
SectionHeading,
3838
} from "@/components/ui/typography";
39+
import { useDemo } from "@/hooks/useDemo";
3940
import { COLOR } from "@/lib/constants";
4041

4142
export function PeerDetail() {
43+
const { mask } = useDemo();
4244
const { workspaceId, peerId } = useParams({ strict: false }) as {
4345
workspaceId: string;
4446
peerId: string;
@@ -209,12 +211,12 @@ export function PeerDetail() {
209211
}}
210212
>
211213
<div className="flex items-center gap-2 mb-1.5">
212-
<Badge variant="blue">{r.peer_id ?? peerId}</Badge>
214+
<Badge variant="blue">{mask(r.peer_id ?? peerId)}</Badge>
213215
{r.created_at && (
214216
<Caption>{new Date(r.created_at).toLocaleString()}</Caption>
215217
)}
216218
</div>
217-
<Body className="whitespace-pre-wrap">{r.content}</Body>
219+
<Body className="whitespace-pre-wrap">{mask(r.content)}</Body>
218220
</div>
219221
))
220222
)}

packages/web/src/components/peers/PeerList.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { PageLoader } from "@/components/shared/LoadingSpinner";
1111
import { Pagination } from "@/components/shared/Pagination";
1212
import { SortControl, type SortDir } from "@/components/shared/SortControl";
1313
import { MonoCaption, PageTitle } from "@/components/ui/typography";
14+
import { useDemo } from "@/hooks/useDemo";
1415
import { COLOR } from "@/lib/constants";
1516

1617
type Peer = components["schemas"]["Peer"];
@@ -45,6 +46,7 @@ const item: Variants = {
4546
};
4647

4748
export function PeerList() {
49+
const { mask } = useDemo();
4850
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
4951
const [page, setPage] = useState(1);
5052
const [sortField, setSortField] = useState("created_at");
@@ -246,7 +248,7 @@ export function PeerList() {
246248
className="font-mono text-sm font-medium truncate"
247249
style={{ color: COLOR.accentSoft }}
248250
>
249-
{peer.id}
251+
{mask(peer.id)}
250252
</span>
251253
<ChevronRight
252254
className="w-4 h-4 shrink-0 ml-2 opacity-30 group-hover:opacity-70 transition-opacity"

packages/web/src/components/sessions/SessionDetail.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ import {
3030
PageTitle,
3131
SectionHeading,
3232
} from "@/components/ui/typography";
33+
import { useDemo } from "@/hooks/useDemo";
3334

3435
type Message = components["schemas"]["Message"];
3536
type SessionSummaries = components["schemas"]["SessionSummaries"];
3637
type Summary = components["schemas"]["Summary"];
3738
type Tab = "messages" | "summaries" | "context" | "peers";
3839

3940
export function SessionDetail() {
41+
const { mask } = useDemo();
4042
const { workspaceId, sessionId } = useParams({ strict: false }) as {
4143
workspaceId: string;
4244
sessionId: string;
@@ -207,8 +209,8 @@ export function SessionDetail() {
207209
className="text-sm py-2"
208210
style={{ borderBottom: "1px solid var(--border)", color: "var(--text-2)" }}
209211
>
210-
{r.peer_id && <Badge variant="blue">{r.peer_id}</Badge>}
211-
<p className="mt-1 whitespace-pre-wrap">{r.content}</p>
212+
{r.peer_id && <Badge variant="blue">{mask(r.peer_id)}</Badge>}
213+
<p className="mt-1 whitespace-pre-wrap">{mask(r.content)}</p>
212214
</div>
213215
))
214216
)}
@@ -269,14 +271,14 @@ export function SessionDetail() {
269271
>
270272
<div className="flex items-center gap-2 mb-2 flex-wrap">
271273
<Badge variant={msg.peer_id ? "blue" : "default"}>
272-
{msg.peer_id ?? "system"}
274+
{msg.peer_id ? mask(msg.peer_id) : "system"}
273275
</Badge>
274276
{msg.token_count != null && <Caption>{msg.token_count} tokens</Caption>}
275277
{msg.created_at && (
276278
<Caption>{new Date(msg.created_at).toLocaleString()}</Caption>
277279
)}
278280
</div>
279-
<Body className="whitespace-pre-wrap">{msg.content}</Body>
281+
<Body className="whitespace-pre-wrap">{mask(msg.content)}</Body>
280282
</div>
281283
))}
282284
</div>
@@ -414,6 +416,7 @@ function SessionPeersTab({
414416
}
415417

416418
function SummaryCard({ label, summary }: { label: string; summary: Summary }) {
419+
const { mask } = useDemo();
417420
return (
418421
<div
419422
className="rounded-xl p-4"
@@ -436,7 +439,7 @@ function SummaryCard({ label, summary }: { label: string; summary: Summary }) {
436439
)}
437440
</div>
438441
</div>
439-
<Body className="whitespace-pre-wrap">{summary.content}</Body>
442+
<Body className="whitespace-pre-wrap">{mask(summary.content)}</Body>
440443
</div>
441444
);
442445
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
2+
import { applyDemoMode, getDemoMode, maskValue } from "@/lib/demo";
3+
4+
interface DemoContextValue {
5+
demo: boolean;
6+
toggle: () => void;
7+
mask: (value: string) => string;
8+
}
9+
10+
const DemoContext = createContext<DemoContextValue | null>(null);
11+
12+
export function DemoProvider({ children }: { children: ReactNode }) {
13+
const [demo, setDemo] = useState<boolean>(() => getDemoMode());
14+
15+
useEffect(() => {
16+
applyDemoMode(demo);
17+
}, [demo]);
18+
19+
function toggle() {
20+
setDemo((d) => !d);
21+
}
22+
23+
function mask(value: string): string {
24+
return demo ? maskValue(value) : value;
25+
}
26+
27+
return <DemoContext.Provider value={{ demo, toggle, mask }}>{children}</DemoContext.Provider>;
28+
}
29+
30+
export function useDemoContext(): DemoContextValue {
31+
const ctx = useContext(DemoContext);
32+
if (!ctx) throw new Error("useDemoContext must be used within DemoProvider");
33+
return ctx;
34+
}

packages/web/src/hooks/useDemo.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1 @@
1-
import { useEffect, useState } from "react";
2-
import { applyDemoMode, getDemoMode } from "@/lib/demo";
3-
4-
export function useDemo() {
5-
const [demo, setDemo] = useState<boolean>(() => getDemoMode());
6-
7-
useEffect(() => {
8-
applyDemoMode(demo);
9-
}, [demo]);
10-
11-
function toggle() {
12-
setDemo((d) => !d);
13-
}
14-
15-
return { demo, toggle };
16-
}
1+
export { useDemoContext as useDemo } from "@/context/DemoContext";

packages/web/src/index.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@
55
@import "@fontsource/dm-sans/500.css";
66
@import "@fontsource/dm-sans/600.css";
77

8-
/* Demo mode — blur main content, sidebar remains interactive */
9-
[data-demo="true"] main {
10-
filter: blur(6px);
11-
user-select: none;
12-
}
13-
148
/* ─── Tailwind v4 theme bridge ─── */
159
@theme inline {
1610
--color-background: var(--bg);

packages/web/src/lib/demo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ export function applyDemoMode(enabled: boolean): void {
88
document.documentElement.setAttribute("data-demo", String(enabled));
99
localStorage.setItem(DEMO_KEY, String(enabled));
1010
}
11+
12+
export function maskValue(value: string): string {
13+
return value.replace(/\S/g, "*");
14+
}

packages/web/src/main.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
22
import { createRouter, RouterProvider } from "@tanstack/react-router";
33
import { StrictMode } from "react";
44
import { createRoot } from "react-dom/client";
5-
import { applyDemoMode, getDemoMode } from "@/lib/demo";
65
import { routeTree } from "./routeTree.gen";
76
import "./index.css";
87

9-
applyDemoMode(getDemoMode());
10-
118
const queryClient = new QueryClient({
129
defaultOptions: {
1310
queries: {

0 commit comments

Comments
 (0)