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 (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}