diff --git a/ui/public/assets/providers/iflow.png b/ui/public/assets/providers/iflow.png new file mode 100644 index 00000000..82edaaec Binary files /dev/null and b/ui/public/assets/providers/iflow.png differ diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 032a7cf8..dc10a280 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -4,6 +4,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; import { queryClient } from '@/lib/query-client'; import { ThemeProvider } from '@/components/theme-provider'; +import { PrivacyProvider } from '@/contexts/privacy-context'; import { Layout } from '@/components/layout'; // Eager load: HomePage (initial route) @@ -33,22 +34,24 @@ export default function App() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx index c23e95ea..05fa86d4 100644 --- a/ui/src/components/account-flow-viz.tsx +++ b/ui/src/components/account-flow-viz.tsx @@ -9,6 +9,7 @@ import { cn } from '@/lib/utils'; import { ProviderIcon } from '@/components/provider-icon'; import { PROVIDER_COLORS } from '@/lib/provider-config'; import { STATUS_COLORS } from '@/lib/utils'; +import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { ChevronRight, X, @@ -94,7 +95,13 @@ function formatTimelineTime(date: Date): string { } /** Connection Timeline Component - right sidebar panel */ -function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) { +function ConnectionTimeline({ + events, + privacyMode, +}: { + events: ConnectionEvent[]; + privacyMode: boolean; +}) { if (events.length === 0) { return (
@@ -149,7 +156,12 @@ function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) { {/* Event content */}
- + {cleanEmail(event.accountEmail)} @@ -217,6 +229,9 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { const [selectedAccount, setSelectedAccount] = useState(null); const [paths, setPaths] = useState([]); + // Privacy mode for demo purposes + const { privacyMode } = usePrivacy(); + // Drag state for all cards (account IDs + 'provider') const [dragOffsets, setDragOffsets] = useState>({}); const [draggingId, setDraggingId] = useState(null); @@ -494,7 +509,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { {/* Drag handle indicator */}
- + {cleanEmail(account.email)} - + {cleanEmail(account.email)}
@@ -704,7 +729,7 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { {/* Right Section: Connection Timeline - Fixed compact width */}
- +
@@ -733,7 +758,12 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { className="w-2 h-2 rounded-full" style={{ backgroundColor: selectedAccount.color }} /> - + {cleanEmail(selectedAccount.email)}
diff --git a/ui/src/components/auth-monitor.tsx b/ui/src/components/auth-monitor.tsx index 4f812050..f6bfccc1 100644 --- a/ui/src/components/auth-monitor.tsx +++ b/ui/src/components/auth-monitor.tsx @@ -12,6 +12,7 @@ import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config'; import { Skeleton } from '@/components/ui/skeleton'; import { ProviderIcon } from '@/components/provider-icon'; import { AccountFlowViz } from '@/components/account-flow-viz'; +import { usePrivacy } from '@/contexts/privacy-context'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; import { Activity, CheckCircle2, XCircle, ChevronRight, Radio } from 'lucide-react'; @@ -114,6 +115,7 @@ function InlineStatsBadge({ success, failure }: { success: number; failure: numb export function AuthMonitor() { const { data, isLoading, error } = useCliproxyAuth(); const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats(); + const { privacyMode } = usePrivacy(); const [selectedProvider, setSelectedProvider] = useState(null); const [hoveredProvider, setHoveredProvider] = useState(null); const [timeSinceUpdate, setTimeSinceUpdate] = useState(''); @@ -406,7 +408,7 @@ export function AuthMonitor() { key={acc.id} className="w-2 h-2 rounded-full" style={{ backgroundColor: acc.color }} - title={cleanEmail(acc.email)} + title={privacyMode ? '••••••' : cleanEmail(acc.email)} /> ))} {ps.accounts.length > 5 && ( diff --git a/ui/src/components/cliproxy-dialog.tsx b/ui/src/components/cliproxy-dialog.tsx index e91c1755..f699305d 100644 --- a/ui/src/components/cliproxy-dialog.tsx +++ b/ui/src/components/cliproxy-dialog.tsx @@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy'; +import { usePrivacy } from '@/contexts/privacy-context'; const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow'] as const; @@ -43,6 +44,7 @@ const providerOptions = [ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { const createMutation = useCreateVariant(); const { data: authData } = useCliproxyAuth(); + const { privacyMode } = usePrivacy(); const { register, @@ -115,7 +117,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { {providerAccounts.map((acc) => ( ))} diff --git a/ui/src/components/cliproxy/overview/credential-health-list.tsx b/ui/src/components/cliproxy/overview/credential-health-list.tsx index a1833739..a1876ee9 100644 --- a/ui/src/components/cliproxy/overview/credential-health-list.tsx +++ b/ui/src/components/cliproxy/overview/credential-health-list.tsx @@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge'; import { CheckCircle2, AlertCircle, XCircle, MinusCircle, RefreshCw } from 'lucide-react'; import { useCliproxyAuth } from '@/hooks/use-cliproxy'; import { cn } from '@/lib/utils'; +import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; type CredentialStatus = 'ready' | 'warning' | 'error' | 'disabled'; @@ -20,6 +21,7 @@ interface CredentialRowProps { email?: string; expiresAt?: string; onRefresh?: () => void; + privacyMode?: boolean; } function CredentialRow({ @@ -30,6 +32,7 @@ function CredentialRow({ email, expiresAt, onRefresh, + privacyMode, }: CredentialRowProps) { const statusConfig = { ready: { @@ -76,7 +79,9 @@ function CredentialRow({
-
{email ?? name}
+
+ {email ?? name} +
{provider}
@@ -124,6 +129,7 @@ function CredentialHealthSkeleton() { export function CredentialHealthList() { const { data: authData, isLoading } = useCliproxyAuth(); + const { privacyMode } = usePrivacy(); if (isLoading) { return ; @@ -164,7 +170,11 @@ export function CredentialHealthList() { {credentials.map((cred, i) => ( - + ))} diff --git a/ui/src/components/cliproxy/provider-editor.tsx b/ui/src/components/cliproxy/provider-editor.tsx index 3d8379bd..d69eb1ae 100644 --- a/ui/src/components/cliproxy/provider-editor.tsx +++ b/ui/src/components/cliproxy/provider-editor.tsx @@ -57,6 +57,7 @@ import { useDeletePreset, } from '@/hooks/use-cliproxy'; import { cn } from '@/lib/utils'; +import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; // Lazy load CodeEditor const CodeEditor = lazy(() => @@ -100,6 +101,7 @@ export function ProviderEditor({ const [conflictDialog, setConflictDialog] = useState(false); const [customPresetOpen, setCustomPresetOpen] = useState(false); const queryClient = useQueryClient(); + const { privacyMode } = usePrivacy(); // Fetch available models from CLIProxy API const { data: modelsData } = useCliproxyModels(); @@ -343,9 +345,9 @@ export function ProviderEditor({ ))} + + +

+ {privacyMode + ? 'Privacy mode ON - Click to show data' + : 'Privacy mode OFF - Click to hide data'} +

+
+ + ); +} diff --git a/ui/src/components/quick-setup-wizard.tsx b/ui/src/components/quick-setup-wizard.tsx index dfc3d6b4..1fc36d70 100644 --- a/ui/src/components/quick-setup-wizard.tsx +++ b/ui/src/components/quick-setup-wizard.tsx @@ -39,6 +39,8 @@ import { import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; import { MODEL_CATALOGS } from '@/lib/model-catalogs'; +import { cn } from '@/lib/utils'; +import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; interface QuickSetupWizardProps { open: boolean; @@ -68,6 +70,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { const { data: authData, refetch } = useCliproxyAuth(); const createMutation = useCreateVariant(); const startAuthMutation = useStartAuth(); + const { privacyMode } = usePrivacy(); // Get auth status for selected provider const providerAuth = authData?.authStatus.find( @@ -347,7 +350,9 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
-
{acc.email || acc.id}
+
+ {acc.email || acc.id} +
{acc.isDefault && (
Default account
)} @@ -403,7 +408,12 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { {selectedAccount && (
- Using: {selectedAccount.email || selectedAccount.id} + + Using:{' '} + + {selectedAccount.email || selectedAccount.id} + +
)} diff --git a/ui/src/contexts/privacy-context.tsx b/ui/src/contexts/privacy-context.tsx new file mode 100644 index 00000000..98cc98fb --- /dev/null +++ b/ui/src/contexts/privacy-context.tsx @@ -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(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 ( + + {children} + + ); +} + +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'; diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 2e0dfa17..981ff7c1 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -247,7 +247,7 @@ export function CliproxyPage() {

CCS-level account management