/** * ConnectionStatus Component * * Displays the current Gateway connection status with visual indicators. * Supports automatic reconnect and manual reconnect button. * Includes health status indicator for ZCLAW backend. */ import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Wifi, WifiOff, Loader2, RefreshCw, Heart, HeartPulse } from 'lucide-react'; import { useConnectionStore, getClient } from '../store/connectionStore'; import { createHealthCheckScheduler, getHealthStatusLabel, formatHealthCheckTime, type HealthCheckResult, type HealthStatus, } from '../lib/health-check'; interface ConnectionStatusProps { /** Show compact version (just icon and status text) */ compact?: boolean; /** Show reconnect button when disconnected */ showReconnectButton?: boolean; /** Additional CSS classes */ className?: string; } interface ReconnectInfo { attempt: number; delay: number; maxAttempts: number; } type StatusType = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting'; const statusConfig: Record = { disconnected: { color: 'text-red-500', bgColor: 'bg-red-50 dark:bg-red-900/20', label: '已断开', icon: WifiOff, }, connecting: { color: 'text-yellow-500', bgColor: 'bg-yellow-50 dark:bg-yellow-900/20', label: '连接中...', icon: Loader2, animate: true, }, handshaking: { color: 'text-yellow-500', bgColor: 'bg-yellow-50 dark:bg-yellow-900/20', label: '认证中...', icon: Loader2, animate: true, }, connected: { color: 'text-green-500', bgColor: 'bg-green-50 dark:bg-green-900/20', label: '已连接', icon: Wifi, }, reconnecting: { color: 'text-orange-500', bgColor: 'bg-orange-50 dark:bg-orange-900/20', label: '重连中...', icon: RefreshCw, animate: true, }, }; export function ConnectionStatus({ compact = false, showReconnectButton = true, className = '', }: ConnectionStatusProps) { const connectionState = useConnectionStore((s) => s.connectionState); const connect = useConnectionStore((s) => s.connect); const [showPrompt, setShowPrompt] = useState(false); const [reconnectInfo, setReconnectInfo] = useState(null); // Listen for reconnect events useEffect(() => { const client = getClient(); const unsubReconnecting = client.on('reconnecting', (info) => { setReconnectInfo(info as ReconnectInfo); }); const unsubFailed = client.on('reconnect_failed', () => { setShowPrompt(true); setReconnectInfo(null); }); const unsubConnected = client.on('connected', () => { setShowPrompt(false); setReconnectInfo(null); }); return () => { unsubReconnecting(); unsubFailed(); unsubConnected(); }; }, []); const config = statusConfig[connectionState]; const Icon = config.icon; const isDisconnected = connectionState === 'disconnected'; const isReconnecting = connectionState === 'reconnecting'; const handleReconnect = async () => { setShowPrompt(false); try { await connect(); } catch (error) { console.error('Manual reconnect failed:', error); } }; // Compact version if (compact) { return (
{isReconnecting && reconnectInfo ? `${config.label} (${reconnectInfo.attempt}/${reconnectInfo.maxAttempts})` : config.label} {showPrompt && showReconnectButton && ( )}
); } // Full version return (
{isReconnecting && reconnectInfo ? `${config.label} (${reconnectInfo.attempt}/${reconnectInfo.maxAttempts})` : config.label}
{reconnectInfo && (
{Math.round(reconnectInfo.delay / 1000)}秒后重试
)}
{showPrompt && isDisconnected && showReconnectButton && ( 重新连接 )}
); } /** * ConnectionIndicator - Minimal connection indicator for headers */ export function ConnectionIndicator({ className = '' }: { className?: string }) { const connectionState = useConnectionStore((s) => s.connectionState); const isConnected = connectionState === 'connected'; const isReconnecting = connectionState === 'reconnecting'; return ( {isConnected ? 'Gateway 已连接' : isReconnecting ? '重连中...' : 'Gateway 未连接'} ); } /** * HealthStatusIndicator - Displays ZCLAW backend health status */ export function HealthStatusIndicator({ className = '', showDetails = false, }: { className?: string; showDetails?: boolean; }) { const [healthResult, setHealthResult] = useState(null); useEffect(() => { // Start periodic health checks const cleanup = createHealthCheckScheduler((result) => { setHealthResult(result); }, 30000); // Check every 30 seconds return cleanup; }, []); if (!healthResult) { return ( 检查中... ); } const statusColors: Record = { healthy: { dot: 'bg-green-400', text: 'text-green-500', icon: Heart }, unhealthy: { dot: 'bg-red-400', text: 'text-red-500', icon: HeartPulse }, unknown: { dot: 'bg-gray-400', text: 'text-gray-500', icon: Heart }, }; const config = statusColors[healthResult.status]; const Icon = config.icon; return ( {getHealthStatusLabel(healthResult.status)} {showDetails && healthResult.message && ( ({formatHealthCheckTime(healthResult.timestamp)}) )} ); } export default ConnectionStatus;