feat(demo): replace blur with asterisk masking of user data via React context
This commit is contained in:
@@ -7,6 +7,7 @@ import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/input";
|
import { Textarea } from "@/components/ui/input";
|
||||||
import { SectionHeading } from "@/components/ui/typography";
|
import { SectionHeading } from "@/components/ui/typography";
|
||||||
|
import { useDemo } from "@/hooks/useDemo";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -15,6 +16,7 @@ interface Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPage() {
|
export function ChatPage() {
|
||||||
|
const { mask } = useDemo();
|
||||||
const { workspaceId, peerId } = useParams({ strict: false }) as {
|
const { workspaceId, peerId } = useParams({ strict: false }) as {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
peerId: string;
|
peerId: string;
|
||||||
@@ -143,7 +145,7 @@ export function ChatPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p className="whitespace-pre-wrap leading-relaxed">{msg.content}</p>
|
<p className="whitespace-pre-wrap leading-relaxed">{mask(msg.content)}</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input, Textarea } from "@/components/ui/input";
|
import { Input, Textarea } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Body, Caption, MonoCaption, Muted, PageTitle } from "@/components/ui/typography";
|
import { Body, Caption, MonoCaption, Muted, PageTitle } from "@/components/ui/typography";
|
||||||
|
import { useDemo } from "@/hooks/useDemo";
|
||||||
import { COLOR } from "@/lib/constants";
|
import { COLOR } from "@/lib/constants";
|
||||||
|
|
||||||
type Conclusion = components["schemas"]["Conclusion"];
|
type Conclusion = components["schemas"]["Conclusion"];
|
||||||
@@ -49,6 +50,7 @@ const itemVariants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ConclusionBrowser() {
|
export function ConclusionBrowser() {
|
||||||
|
const { mask } = useDemo();
|
||||||
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
|
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [sortField, setSortField] = useState("created_at");
|
const [sortField, setSortField] = useState("created_at");
|
||||||
@@ -233,7 +235,7 @@ export function ConclusionBrowser() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<Body className="whitespace-pre-wrap flex-1">{c.content}</Body>
|
<Body className="whitespace-pre-wrap flex-1">{mask(c.content)}</Body>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -250,12 +252,12 @@ export function ConclusionBrowser() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Eye className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} />
|
<Eye className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} />
|
||||||
<MonoCaption>{c.observer_id}</MonoCaption>
|
<MonoCaption>{mask(c.observer_id)}</MonoCaption>
|
||||||
</div>
|
</div>
|
||||||
{c.observed_id && (
|
{c.observed_id && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Caption>→</Caption>
|
<Caption>→</Caption>
|
||||||
<MonoCaption>{c.observed_id}</MonoCaption>
|
<MonoCaption>{mask(c.observed_id)}</MonoCaption>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{c.session_id && (
|
{c.session_id && (
|
||||||
@@ -266,7 +268,7 @@ export function ConclusionBrowser() {
|
|||||||
className="flex items-center gap-1 text-xs font-mono hover:underline"
|
className="flex items-center gap-1 text-xs font-mono hover:underline"
|
||||||
style={{ color: "var(--accent-text)" }}
|
style={{ color: "var(--accent-text)" }}
|
||||||
>
|
>
|
||||||
{c.session_id}
|
{mask(c.session_id)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{c.created_at && (
|
{c.created_at && (
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ import {
|
|||||||
PageTitle,
|
PageTitle,
|
||||||
SectionHeading,
|
SectionHeading,
|
||||||
} from "@/components/ui/typography";
|
} from "@/components/ui/typography";
|
||||||
|
import { useDemo } from "@/hooks/useDemo";
|
||||||
import { COLOR } from "@/lib/constants";
|
import { COLOR } from "@/lib/constants";
|
||||||
|
|
||||||
export function PeerDetail() {
|
export function PeerDetail() {
|
||||||
|
const { mask } = useDemo();
|
||||||
const { workspaceId, peerId } = useParams({ strict: false }) as {
|
const { workspaceId, peerId } = useParams({ strict: false }) as {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
peerId: string;
|
peerId: string;
|
||||||
@@ -209,12 +211,12 @@ export function PeerDetail() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
<div className="flex items-center gap-2 mb-1.5">
|
||||||
<Badge variant="blue">{r.peer_id ?? peerId}</Badge>
|
<Badge variant="blue">{mask(r.peer_id ?? peerId)}</Badge>
|
||||||
{r.created_at && (
|
{r.created_at && (
|
||||||
<Caption>{new Date(r.created_at).toLocaleString()}</Caption>
|
<Caption>{new Date(r.created_at).toLocaleString()}</Caption>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Body className="whitespace-pre-wrap">{r.content}</Body>
|
<Body className="whitespace-pre-wrap">{mask(r.content)}</Body>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { PageLoader } from "@/components/shared/LoadingSpinner";
|
|||||||
import { Pagination } from "@/components/shared/Pagination";
|
import { Pagination } from "@/components/shared/Pagination";
|
||||||
import { SortControl, type SortDir } from "@/components/shared/SortControl";
|
import { SortControl, type SortDir } from "@/components/shared/SortControl";
|
||||||
import { MonoCaption, PageTitle } from "@/components/ui/typography";
|
import { MonoCaption, PageTitle } from "@/components/ui/typography";
|
||||||
|
import { useDemo } from "@/hooks/useDemo";
|
||||||
import { COLOR } from "@/lib/constants";
|
import { COLOR } from "@/lib/constants";
|
||||||
|
|
||||||
type Peer = components["schemas"]["Peer"];
|
type Peer = components["schemas"]["Peer"];
|
||||||
@@ -45,6 +46,7 @@ const item: Variants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function PeerList() {
|
export function PeerList() {
|
||||||
|
const { mask } = useDemo();
|
||||||
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
|
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [sortField, setSortField] = useState("created_at");
|
const [sortField, setSortField] = useState("created_at");
|
||||||
@@ -246,7 +248,7 @@ export function PeerList() {
|
|||||||
className="font-mono text-sm font-medium truncate"
|
className="font-mono text-sm font-medium truncate"
|
||||||
style={{ color: COLOR.accentSoft }}
|
style={{ color: COLOR.accentSoft }}
|
||||||
>
|
>
|
||||||
{peer.id}
|
{mask(peer.id)}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className="w-4 h-4 shrink-0 ml-2 opacity-30 group-hover:opacity-70 transition-opacity"
|
className="w-4 h-4 shrink-0 ml-2 opacity-30 group-hover:opacity-70 transition-opacity"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
PageTitle,
|
PageTitle,
|
||||||
SectionHeading,
|
SectionHeading,
|
||||||
} from "@/components/ui/typography";
|
} from "@/components/ui/typography";
|
||||||
|
import { useDemo } from "@/hooks/useDemo";
|
||||||
|
|
||||||
type Message = components["schemas"]["Message"];
|
type Message = components["schemas"]["Message"];
|
||||||
type SessionSummaries = components["schemas"]["SessionSummaries"];
|
type SessionSummaries = components["schemas"]["SessionSummaries"];
|
||||||
@@ -37,6 +38,7 @@ type Summary = components["schemas"]["Summary"];
|
|||||||
type Tab = "messages" | "summaries" | "context" | "peers";
|
type Tab = "messages" | "summaries" | "context" | "peers";
|
||||||
|
|
||||||
export function SessionDetail() {
|
export function SessionDetail() {
|
||||||
|
const { mask } = useDemo();
|
||||||
const { workspaceId, sessionId } = useParams({ strict: false }) as {
|
const { workspaceId, sessionId } = useParams({ strict: false }) as {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -207,8 +209,8 @@ export function SessionDetail() {
|
|||||||
className="text-sm py-2"
|
className="text-sm py-2"
|
||||||
style={{ borderBottom: "1px solid var(--border)", color: "var(--text-2)" }}
|
style={{ borderBottom: "1px solid var(--border)", color: "var(--text-2)" }}
|
||||||
>
|
>
|
||||||
{r.peer_id && <Badge variant="blue">{r.peer_id}</Badge>}
|
{r.peer_id && <Badge variant="blue">{mask(r.peer_id)}</Badge>}
|
||||||
<p className="mt-1 whitespace-pre-wrap">{r.content}</p>
|
<p className="mt-1 whitespace-pre-wrap">{mask(r.content)}</p>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -269,14 +271,14 @@ export function SessionDetail() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
<Badge variant={msg.peer_id ? "blue" : "default"}>
|
<Badge variant={msg.peer_id ? "blue" : "default"}>
|
||||||
{msg.peer_id ?? "system"}
|
{msg.peer_id ? mask(msg.peer_id) : "system"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{msg.token_count != null && <Caption>{msg.token_count} tokens</Caption>}
|
{msg.token_count != null && <Caption>{msg.token_count} tokens</Caption>}
|
||||||
{msg.created_at && (
|
{msg.created_at && (
|
||||||
<Caption>{new Date(msg.created_at).toLocaleString()}</Caption>
|
<Caption>{new Date(msg.created_at).toLocaleString()}</Caption>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Body className="whitespace-pre-wrap">{msg.content}</Body>
|
<Body className="whitespace-pre-wrap">{mask(msg.content)}</Body>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -414,6 +416,7 @@ function SessionPeersTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SummaryCard({ label, summary }: { label: string; summary: Summary }) {
|
function SummaryCard({ label, summary }: { label: string; summary: Summary }) {
|
||||||
|
const { mask } = useDemo();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4"
|
className="rounded-xl p-4"
|
||||||
@@ -436,7 +439,7 @@ function SummaryCard({ label, summary }: { label: string; summary: Summary }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body className="whitespace-pre-wrap">{summary.content}</Body>
|
<Body className="whitespace-pre-wrap">{mask(summary.content)}</Body>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
34
packages/web/src/context/DemoContext.tsx
Normal file
34
packages/web/src/context/DemoContext.tsx
Normal file
@@ -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<DemoContextValue | null>(null);
|
||||||
|
|
||||||
|
export function DemoProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [demo, setDemo] = useState<boolean>(() => getDemoMode());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyDemoMode(demo);
|
||||||
|
}, [demo]);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
setDemo((d) => !d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mask(value: string): string {
|
||||||
|
return demo ? maskValue(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DemoContext.Provider value={{ demo, toggle, mask }}>{children}</DemoContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDemoContext(): DemoContextValue {
|
||||||
|
const ctx = useContext(DemoContext);
|
||||||
|
if (!ctx) throw new Error("useDemoContext must be used within DemoProvider");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
@@ -1,16 +1 @@
|
|||||||
import { useEffect, useState } from "react";
|
export { useDemoContext as useDemo } from "@/context/DemoContext";
|
||||||
import { applyDemoMode, getDemoMode } from "@/lib/demo";
|
|
||||||
|
|
||||||
export function useDemo() {
|
|
||||||
const [demo, setDemo] = useState<boolean>(() => getDemoMode());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
applyDemoMode(demo);
|
|
||||||
}, [demo]);
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
setDemo((d) => !d);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { demo, toggle };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,12 +5,6 @@
|
|||||||
@import "@fontsource/dm-sans/500.css";
|
@import "@fontsource/dm-sans/500.css";
|
||||||
@import "@fontsource/dm-sans/600.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 ─── */
|
/* ─── Tailwind v4 theme bridge ─── */
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--bg);
|
--color-background: var(--bg);
|
||||||
|
|||||||
@@ -8,3 +8,7 @@ export function applyDemoMode(enabled: boolean): void {
|
|||||||
document.documentElement.setAttribute("data-demo", String(enabled));
|
document.documentElement.setAttribute("data-demo", String(enabled));
|
||||||
localStorage.setItem(DEMO_KEY, String(enabled));
|
localStorage.setItem(DEMO_KEY, String(enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function maskValue(value: string): string {
|
||||||
|
return value.replace(/\S/g, "*");
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { applyDemoMode, getDemoMode } from "@/lib/demo";
|
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
applyDemoMode(getDemoMode());
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRootRoute, Outlet, useRouter } from "@tanstack/react-router";
|
import { createRootRoute, Outlet, useRouter } from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Sidebar } from "@/components/layout/Sidebar";
|
import { Sidebar } from "@/components/layout/Sidebar";
|
||||||
|
import { DemoProvider } from "@/context/DemoContext";
|
||||||
import { loadConfig } from "@/lib/config";
|
import { loadConfig } from "@/lib/config";
|
||||||
import { applyTheme, getStoredTheme } from "@/lib/theme";
|
import { applyTheme, getStoredTheme } from "@/lib/theme";
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ function RootLayout() {
|
|||||||
if (!config) return null;
|
if (!config) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<DemoProvider>
|
||||||
<div
|
<div
|
||||||
className="flex h-screen w-full overflow-hidden"
|
className="flex h-screen w-full overflow-hidden"
|
||||||
style={{ background: "var(--bg)", position: "relative", zIndex: 1 }}
|
style={{ background: "var(--bg)", position: "relative", zIndex: 1 }}
|
||||||
@@ -35,6 +37,7 @@ function RootLayout() {
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</DemoProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user