Files
ccs/ui/src/components/cliproxy/cliproxy-header.tsx
T
kaitranntt 0f029f960a feat(ui): integrate Kiro and Copilot providers, rename to CLIProxy Plus
- Rename sidebar/header from CLIProxy to CLIProxy Plus
- Add Kiro and Copilot to all provider lists and type unions
- Add provider logos and color configs
- Add model catalogs with upstream CLIProxyAPIPlus models
- Update Quick Setup Wizard with new providers
2025-12-21 22:27:36 -05:00

187 lines
5.4 KiB
TypeScript

/**
* CLIProxy Header Component
* Fixed header with OAuth login buttons, status indicator, and refresh
*/
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { RefreshCw, Loader2 } from 'lucide-react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { cn } from '@/lib/utils';
interface LoginButtonProps {
provider: string;
displayName: string;
isAuthenticated: boolean;
accountCount: number;
isAuthenticating: boolean;
onLogin: () => void;
}
function LoginButton({
displayName,
isAuthenticated,
accountCount,
isAuthenticating,
onLogin,
}: LoginButtonProps) {
if (isAuthenticating) {
return (
<Button variant="outline" size="sm" disabled className="gap-2">
<Loader2 className="w-3 h-3 animate-spin" />
{displayName}
</Button>
);
}
if (isAuthenticated) {
return (
<Button
variant="outline"
size="sm"
className="gap-2 border-green-500/30 text-green-600 dark:text-green-400"
>
<span className="w-2 h-2 rounded-full bg-green-500" />
{displayName}
{accountCount > 1 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5 text-[10px]">
{accountCount}
</Badge>
)}
</Button>
);
}
return (
<Button variant="default" size="sm" className="gap-2" onClick={onLogin}>
+ {displayName}
</Button>
);
}
// Helper to format relative time
function formatRelativeTime(date: Date): string {
const diff = Date.now() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'just now';
if (minutes === 1) return '1m ago';
return `${minutes}m ago`;
}
// Hook for relative time display that updates periodically
function useRelativeTime(date: Date | undefined): string | null {
const [text, setText] = useState<string | null>(() => (date ? formatRelativeTime(date) : null));
useEffect(() => {
if (!date) return;
// Update every 30 seconds via interval only
const interval = setInterval(() => {
setText(formatRelativeTime(date));
}, 30000);
return () => clearInterval(interval);
}, [date]);
// Compute current value on each render if date changes
// This is the pure computation part
const currentText = date ? formatRelativeTime(date) : null;
// Return the more recent of computed or state-based value
// State value will be updated by interval
return date ? currentText : text;
}
interface CliproxyHeaderProps {
onRefresh: () => void;
isRefreshing: boolean;
lastUpdated?: Date;
isRunning?: boolean;
}
export function CliproxyHeader({
onRefresh,
isRefreshing,
lastUpdated,
isRunning = true,
}: CliproxyHeaderProps) {
const { data: authData } = useCliproxyAuth();
const { provider: authProvider, isAuthenticating, startAuth } = useCliproxyAuthFlow();
const lastUpdatedText = useRelativeTime(lastUpdated);
const providers = [
{ id: 'claude', displayName: 'Claude' },
{ id: 'gemini', displayName: 'Gemini' },
{ id: 'codex', displayName: 'Codex' },
{ id: 'agy', displayName: 'Agy' },
{ id: 'kiro', displayName: 'Kiro' },
];
const getProviderStatus = (providerId: string) => {
const status = authData?.authStatus.find((s) => s.provider === providerId);
return {
isAuthenticated: status?.authenticated ?? false,
accountCount: status?.accounts?.length ?? 0,
};
};
return (
<div className="flex flex-col gap-4 pb-4 border-b">
{/* Top row: Title and Login Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">CLIProxy Plus</h1>
<p className="text-sm text-muted-foreground mt-1">CCS-level account management</p>
</div>
{/* Login Buttons - Wrap on mobile */}
<div className="flex flex-wrap items-center gap-2">
{providers.map((p) => {
const status = getProviderStatus(p.id);
return (
<LoginButton
key={p.id}
provider={p.id}
displayName={p.displayName}
isAuthenticated={status.isAuthenticated}
accountCount={status.accountCount}
isAuthenticating={authProvider === p.id && isAuthenticating}
onLogin={() => startAuth(p.id)}
/>
);
})}
</div>
</div>
{/* Bottom row: Status and Refresh */}
<div className="flex items-center gap-3">
<Badge variant={isRunning ? 'default' : 'secondary'} className="gap-1.5">
<span
className={cn(
'w-2 h-2 rounded-full',
isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground'
)}
/>
{isRunning ? 'Running' : 'Offline'}
</Badge>
{lastUpdatedText && (
<span className="text-xs text-muted-foreground">{lastUpdatedText}</span>
)}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={onRefresh}
disabled={isRefreshing}
>
<RefreshCw className={cn('w-4 h-4', isRefreshing && 'animate-spin')} />
</Button>
</div>
</div>
);
}