mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 04:17:54 +00:00
feat(privacy): add privacy/demo mode for personal info blurring
This commit is contained in:
+19
-16
@@ -4,6 +4,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import { queryClient } from '@/lib/query-client';
|
import { queryClient } from '@/lib/query-client';
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { ThemeProvider } from '@/components/theme-provider';
|
||||||
|
import { PrivacyProvider } from '@/contexts/privacy-context';
|
||||||
import { Layout } from '@/components/layout';
|
import { Layout } from '@/components/layout';
|
||||||
|
|
||||||
// Eager load: HomePage (initial route)
|
// Eager load: HomePage (initial route)
|
||||||
@@ -33,22 +34,24 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||||
<BrowserRouter>
|
<PrivacyProvider>
|
||||||
<Routes>
|
<BrowserRouter>
|
||||||
<Route element={<Layout />}>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route element={<Layout />}>
|
||||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/api" element={<ApiPage />} />
|
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||||
<Route path="/cliproxy" element={<CliproxyPage />} />
|
<Route path="/api" element={<ApiPage />} />
|
||||||
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
<Route path="/cliproxy" element={<CliproxyPage />} />
|
||||||
<Route path="/accounts" element={<AccountsPage />} />
|
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/accounts" element={<AccountsPage />} />
|
||||||
<Route path="/health" element={<HealthPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
<Route path="/shared" element={<SharedPage />} />
|
<Route path="/health" element={<HealthPage />} />
|
||||||
</Route>
|
<Route path="/shared" element={<SharedPage />} />
|
||||||
</Routes>
|
</Route>
|
||||||
<Toaster position="top-right" />
|
</Routes>
|
||||||
</BrowserRouter>
|
<Toaster position="top-right" />
|
||||||
|
</BrowserRouter>
|
||||||
|
</PrivacyProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { cn } from '@/lib/utils';
|
|||||||
import { ProviderIcon } from '@/components/provider-icon';
|
import { ProviderIcon } from '@/components/provider-icon';
|
||||||
import { PROVIDER_COLORS } from '@/lib/provider-config';
|
import { PROVIDER_COLORS } from '@/lib/provider-config';
|
||||||
import { STATUS_COLORS } from '@/lib/utils';
|
import { STATUS_COLORS } from '@/lib/utils';
|
||||||
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
X,
|
X,
|
||||||
@@ -94,7 +95,13 @@ function formatTimelineTime(date: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Connection Timeline Component - right sidebar panel */
|
/** Connection Timeline Component - right sidebar panel */
|
||||||
function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) {
|
function ConnectionTimeline({
|
||||||
|
events,
|
||||||
|
privacyMode,
|
||||||
|
}: {
|
||||||
|
events: ConnectionEvent[];
|
||||||
|
privacyMode: boolean;
|
||||||
|
}) {
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center justify-center rounded-xl bg-muted/20 dark:bg-zinc-900/40 border border-border/30 dark:border-white/[0.05]">
|
<div className="h-full flex items-center justify-center rounded-xl bg-muted/20 dark:bg-zinc-900/40 border border-border/30 dark:border-white/[0.05]">
|
||||||
@@ -149,7 +156,12 @@ function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) {
|
|||||||
{/* Event content */}
|
{/* Event content */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[10px] font-mono text-foreground truncate">
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-[10px] font-mono text-foreground truncate',
|
||||||
|
privacyMode && PRIVACY_BLUR_CLASS
|
||||||
|
)}
|
||||||
|
>
|
||||||
{cleanEmail(event.accountEmail)}
|
{cleanEmail(event.accountEmail)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-muted-foreground font-mono flex-shrink-0">
|
<span className="text-[9px] text-muted-foreground font-mono flex-shrink-0">
|
||||||
@@ -217,6 +229,9 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
|
|||||||
const [selectedAccount, setSelectedAccount] = useState<AccountData | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<AccountData | null>(null);
|
||||||
const [paths, setPaths] = useState<string[]>([]);
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Privacy mode for demo purposes
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
|
|
||||||
// Drag state for all cards (account IDs + 'provider')
|
// Drag state for all cards (account IDs + 'provider')
|
||||||
const [dragOffsets, setDragOffsets] = useState<Record<string, DragOffset>>({});
|
const [dragOffsets, setDragOffsets] = useState<Record<string, DragOffset>>({});
|
||||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||||
@@ -494,7 +509,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
|
|||||||
{/* Drag handle indicator */}
|
{/* Drag handle indicator */}
|
||||||
<GripVertical className="absolute top-1/2 left-1 -translate-y-1/2 w-3 h-3 text-muted-foreground/40" />
|
<GripVertical className="absolute top-1/2 left-1 -translate-y-1/2 w-3 h-3 text-muted-foreground/40" />
|
||||||
<div className="flex justify-between items-start mb-1 ml-3">
|
<div className="flex justify-between items-start mb-1 ml-3">
|
||||||
<span className="text-xs font-semibold text-foreground tracking-tight truncate max-w-[100px]">
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-xs font-semibold text-foreground tracking-tight truncate max-w-[100px]',
|
||||||
|
privacyMode && PRIVACY_BLUR_CLASS
|
||||||
|
)}
|
||||||
|
>
|
||||||
{cleanEmail(account.email)}
|
{cleanEmail(account.email)}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
@@ -672,7 +692,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
|
|||||||
isHovered ? 'opacity-100' : 'opacity-0'
|
isHovered ? 'opacity-100' : 'opacity-0'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-semibold text-foreground tracking-tight truncate max-w-[100px]">
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-xs font-semibold text-foreground tracking-tight truncate max-w-[100px]',
|
||||||
|
privacyMode && PRIVACY_BLUR_CLASS
|
||||||
|
)}
|
||||||
|
>
|
||||||
{cleanEmail(account.email)}
|
{cleanEmail(account.email)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -704,7 +729,7 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
|
|||||||
|
|
||||||
{/* Right Section: Connection Timeline - Fixed compact width */}
|
{/* Right Section: Connection Timeline - Fixed compact width */}
|
||||||
<div className="w-56 flex-shrink-0">
|
<div className="w-56 flex-shrink-0">
|
||||||
<ConnectionTimeline events={connectionEvents} />
|
<ConnectionTimeline events={connectionEvents} privacyMode={privacyMode} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -733,7 +758,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
|
|||||||
className="w-2 h-2 rounded-full"
|
className="w-2 h-2 rounded-full"
|
||||||
style={{ backgroundColor: selectedAccount.color }}
|
style={{ backgroundColor: selectedAccount.color }}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-semibold text-foreground tracking-tight truncate">
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-semibold text-foreground tracking-tight truncate',
|
||||||
|
privacyMode && PRIVACY_BLUR_CLASS
|
||||||
|
)}
|
||||||
|
>
|
||||||
{cleanEmail(selectedAccount.email)}
|
{cleanEmail(selectedAccount.email)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config';
|
|||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { ProviderIcon } from '@/components/provider-icon';
|
import { ProviderIcon } from '@/components/provider-icon';
|
||||||
import { AccountFlowViz } from '@/components/account-flow-viz';
|
import { AccountFlowViz } from '@/components/account-flow-viz';
|
||||||
|
import { usePrivacy } from '@/contexts/privacy-context';
|
||||||
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
||||||
import { Activity, CheckCircle2, XCircle, ChevronRight, Radio } from 'lucide-react';
|
import { Activity, CheckCircle2, XCircle, ChevronRight, Radio } from 'lucide-react';
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ function InlineStatsBadge({ success, failure }: { success: number; failure: numb
|
|||||||
export function AuthMonitor() {
|
export function AuthMonitor() {
|
||||||
const { data, isLoading, error } = useCliproxyAuth();
|
const { data, isLoading, error } = useCliproxyAuth();
|
||||||
const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
|
const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
||||||
const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
|
const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
|
||||||
const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
|
const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
|
||||||
@@ -406,7 +408,7 @@ export function AuthMonitor() {
|
|||||||
key={acc.id}
|
key={acc.id}
|
||||||
className="w-2 h-2 rounded-full"
|
className="w-2 h-2 rounded-full"
|
||||||
style={{ backgroundColor: acc.color }}
|
style={{ backgroundColor: acc.color }}
|
||||||
title={cleanEmail(acc.email)}
|
title={privacyMode ? '••••••' : cleanEmail(acc.email)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{ps.accounts.length > 5 && (
|
{ps.accounts.length > 5 && (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy';
|
import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||||
|
import { usePrivacy } from '@/contexts/privacy-context';
|
||||||
|
|
||||||
const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow'] as const;
|
const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow'] as const;
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ const providerOptions = [
|
|||||||
export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
|
export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
|
||||||
const createMutation = useCreateVariant();
|
const createMutation = useCreateVariant();
|
||||||
const { data: authData } = useCliproxyAuth();
|
const { data: authData } = useCliproxyAuth();
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -115,7 +117,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
|
|||||||
<option value="">Use default account</option>
|
<option value="">Use default account</option>
|
||||||
{providerAccounts.map((acc) => (
|
{providerAccounts.map((acc) => (
|
||||||
<option key={acc.id} value={acc.id}>
|
<option key={acc.id} value={acc.id}>
|
||||||
{acc.email || acc.id}
|
{privacyMode ? '••••••' : acc.email || acc.id}
|
||||||
{acc.isDefault ? ' (default)' : ''}
|
{acc.isDefault ? ' (default)' : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { CheckCircle2, AlertCircle, XCircle, MinusCircle, RefreshCw } from 'lucide-react';
|
import { CheckCircle2, AlertCircle, XCircle, MinusCircle, RefreshCw } from 'lucide-react';
|
||||||
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
|
||||||
type CredentialStatus = 'ready' | 'warning' | 'error' | 'disabled';
|
type CredentialStatus = 'ready' | 'warning' | 'error' | 'disabled';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ interface CredentialRowProps {
|
|||||||
email?: string;
|
email?: string;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
|
privacyMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CredentialRow({
|
function CredentialRow({
|
||||||
@@ -30,6 +32,7 @@ function CredentialRow({
|
|||||||
email,
|
email,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
privacyMode,
|
||||||
}: CredentialRowProps) {
|
}: CredentialRowProps) {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
ready: {
|
ready: {
|
||||||
@@ -76,7 +79,9 @@ function CredentialRow({
|
|||||||
<Icon className={cn('w-4 h-4', config.color)} />
|
<Icon className={cn('w-4 h-4', config.color)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-sm">{email ?? name}</div>
|
<div className={cn('font-medium text-sm', privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||||
|
{email ?? name}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-muted-foreground capitalize">{provider}</div>
|
<div className="text-xs text-muted-foreground capitalize">{provider}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,6 +129,7 @@ function CredentialHealthSkeleton() {
|
|||||||
|
|
||||||
export function CredentialHealthList() {
|
export function CredentialHealthList() {
|
||||||
const { data: authData, isLoading } = useCliproxyAuth();
|
const { data: authData, isLoading } = useCliproxyAuth();
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <CredentialHealthSkeleton />;
|
return <CredentialHealthSkeleton />;
|
||||||
@@ -164,7 +170,11 @@ export function CredentialHealthList() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{credentials.map((cred, i) => (
|
{credentials.map((cred, i) => (
|
||||||
<CredentialRow key={`${cred.provider}-${cred.name}-${i}`} {...cred} />
|
<CredentialRow
|
||||||
|
key={`${cred.provider}-${cred.name}-${i}`}
|
||||||
|
{...cred}
|
||||||
|
privacyMode={privacyMode}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
useDeletePreset,
|
useDeletePreset,
|
||||||
} from '@/hooks/use-cliproxy';
|
} from '@/hooks/use-cliproxy';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
|
||||||
// Lazy load CodeEditor
|
// Lazy load CodeEditor
|
||||||
const CodeEditor = lazy(() =>
|
const CodeEditor = lazy(() =>
|
||||||
@@ -100,6 +101,7 @@ export function ProviderEditor({
|
|||||||
const [conflictDialog, setConflictDialog] = useState(false);
|
const [conflictDialog, setConflictDialog] = useState(false);
|
||||||
const [customPresetOpen, setCustomPresetOpen] = useState(false);
|
const [customPresetOpen, setCustomPresetOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
|
|
||||||
// Fetch available models from CLIProxy API
|
// Fetch available models from CLIProxy API
|
||||||
const { data: modelsData } = useCliproxyModels();
|
const { data: modelsData } = useCliproxyModels();
|
||||||
@@ -433,6 +435,7 @@ export function ProviderEditor({
|
|||||||
onSetDefault={() => onSetDefault(account.id)}
|
onSetDefault={() => onSetDefault(account.id)}
|
||||||
onRemove={() => onRemoveAccount(account.id)}
|
onRemove={() => onRemoveAccount(account.id)}
|
||||||
isRemoving={isRemovingAccount}
|
isRemoving={isRemovingAccount}
|
||||||
|
privacyMode={privacyMode}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -685,11 +688,13 @@ function AccountItem({
|
|||||||
onSetDefault,
|
onSetDefault,
|
||||||
onRemove,
|
onRemove,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
|
privacyMode,
|
||||||
}: {
|
}: {
|
||||||
account: OAuthAccount;
|
account: OAuthAccount;
|
||||||
onSetDefault: () => void;
|
onSetDefault: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
isRemoving?: boolean;
|
isRemoving?: boolean;
|
||||||
|
privacyMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -709,7 +714,9 @@ function AccountItem({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-sm">{account.email || account.id}</span>
|
<span className={cn('font-medium text-sm', privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||||
|
{account.email || account.id}
|
||||||
|
</span>
|
||||||
{account.isDefault && (
|
{account.isDefault && (
|
||||||
<Badge variant="secondary" className="text-[10px] h-4 px-1.5 gap-0.5">
|
<Badge variant="secondary" className="text-[10px] h-4 px-1.5 gap-0.5">
|
||||||
<Star className="w-2.5 h-2.5 fill-current" />
|
<Star className="w-2.5 h-2.5 fill-current" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom';
|
|||||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||||
import { AppSidebar } from '@/components/app-sidebar';
|
import { AppSidebar } from '@/components/app-sidebar';
|
||||||
import { ThemeToggle } from '@/components/theme-toggle';
|
import { ThemeToggle } from '@/components/theme-toggle';
|
||||||
|
import { PrivacyToggle } from '@/components/privacy-toggle';
|
||||||
import { GitHubLink } from '@/components/github-link';
|
import { GitHubLink } from '@/components/github-link';
|
||||||
import { DocsLink } from '@/components/docs-link';
|
import { DocsLink } from '@/components/docs-link';
|
||||||
import { ConnectionIndicator } from '@/components/connection-indicator';
|
import { ConnectionIndicator } from '@/components/connection-indicator';
|
||||||
@@ -29,6 +30,7 @@ export function Layout() {
|
|||||||
<ConnectionIndicator />
|
<ConnectionIndicator />
|
||||||
<DocsLink />
|
<DocsLink />
|
||||||
<GitHubLink />
|
<GitHubLink />
|
||||||
|
<PrivacyToggle />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Privacy Toggle Button
|
||||||
|
* Toggles demo mode to blur personal information (emails, account IDs)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { usePrivacy } from '@/contexts/privacy-context';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export function PrivacyToggle() {
|
||||||
|
const { privacyMode, togglePrivacyMode } = usePrivacy();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={togglePrivacyMode}
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 transition-colors',
|
||||||
|
privacyMode && 'text-amber-600 hover:text-amber-700 dark:text-amber-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{privacyMode ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{privacyMode
|
||||||
|
? 'Privacy mode ON - Click to show data'
|
||||||
|
: 'Privacy mode OFF - Click to hide data'}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ import {
|
|||||||
import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy';
|
import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy';
|
||||||
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
|
||||||
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
import { MODEL_CATALOGS } from '@/lib/model-catalogs';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
|
||||||
interface QuickSetupWizardProps {
|
interface QuickSetupWizardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -68,6 +70,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
|||||||
const { data: authData, refetch } = useCliproxyAuth();
|
const { data: authData, refetch } = useCliproxyAuth();
|
||||||
const createMutation = useCreateVariant();
|
const createMutation = useCreateVariant();
|
||||||
const startAuthMutation = useStartAuth();
|
const startAuthMutation = useStartAuth();
|
||||||
|
const { privacyMode } = usePrivacy();
|
||||||
|
|
||||||
// Get auth status for selected provider
|
// Get auth status for selected provider
|
||||||
const providerAuth = authData?.authStatus.find(
|
const providerAuth = authData?.authStatus.find(
|
||||||
@@ -347,7 +350,9 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
|||||||
<User className="w-4 h-4 text-muted-foreground" />
|
<User className="w-4 h-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{acc.email || acc.id}</div>
|
<div className={cn('font-medium', privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||||
|
{acc.email || acc.id}
|
||||||
|
</div>
|
||||||
{acc.isDefault && (
|
{acc.isDefault && (
|
||||||
<div className="text-xs text-muted-foreground">Default account</div>
|
<div className="text-xs text-muted-foreground">Default account</div>
|
||||||
)}
|
)}
|
||||||
@@ -403,7 +408,12 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
|||||||
{selectedAccount && (
|
{selectedAccount && (
|
||||||
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded-md text-sm">
|
<div className="flex items-center gap-2 p-2 bg-muted/50 rounded-md text-sm">
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
<span>Using: {selectedAccount.email || selectedAccount.id}</span>
|
<span>
|
||||||
|
Using:{' '}
|
||||||
|
<span className={cn(privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||||
|
{selectedAccount.email || selectedAccount.id}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Privacy Context - Global demo mode for censoring personal information
|
||||||
|
* Provides blur effect on emails, account IDs, and other PII
|
||||||
|
* Persists state to localStorage
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
|
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface PrivacyContextValue {
|
||||||
|
/** Whether privacy/demo mode is enabled */
|
||||||
|
privacyMode: boolean;
|
||||||
|
/** Toggle privacy mode on/off */
|
||||||
|
togglePrivacyMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrivacyContext = createContext<PrivacyContextValue | null>(null);
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ccs-privacy-mode';
|
||||||
|
|
||||||
|
export function PrivacyProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [privacyMode, setPrivacyMode] = useState(() => {
|
||||||
|
// Initialize from localStorage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem(STORAGE_KEY) === 'true';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Persist to localStorage when changed
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, String(privacyMode));
|
||||||
|
}, [privacyMode]);
|
||||||
|
|
||||||
|
const togglePrivacyMode = () => setPrivacyMode((prev) => !prev);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrivacyContext.Provider value={{ privacyMode, togglePrivacyMode }}>
|
||||||
|
{children}
|
||||||
|
</PrivacyContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePrivacy() {
|
||||||
|
const context = useContext(PrivacyContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('usePrivacy must be used within a PrivacyProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSS class for blurring sensitive content */
|
||||||
|
export const PRIVACY_BLUR_CLASS =
|
||||||
|
'blur-[4px] select-none hover:blur-none transition-all duration-200';
|
||||||
Reference in New Issue
Block a user