From 4d87a649de3873786926dad0f598d4f481b1b563 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 3 Feb 2026 22:33:31 -0500 Subject: [PATCH] feat(ui): improve settings page UX and responsiveness - Add mobile-responsive layout for settings panels - Add empty state UI for websearch providers - Improve connection indicator with WebSocket status --- .../shared/connection-indicator.tsx | 22 ++++++++++++++----- ui/src/hooks/use-websocket.ts | 11 +++++++++- ui/src/pages/settings/index.tsx | 20 ++++++++++++++++- .../settings/sections/websearch/index.tsx | 17 +++++++++++++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/ui/src/components/shared/connection-indicator.tsx b/ui/src/components/shared/connection-indicator.tsx index eb00487d..84932a53 100644 --- a/ui/src/components/shared/connection-indicator.tsx +++ b/ui/src/components/shared/connection-indicator.tsx @@ -1,19 +1,29 @@ /** * Connection Indicator (Phase 04) * - * Shows WebSocket connection status in the header. + * Shows WebSocket connection status in the header with reconnection state. */ -import { Wifi, WifiOff } from 'lucide-react'; +import { Wifi, WifiOff, RefreshCw } from 'lucide-react'; import { useWebSocket } from '@/hooks/use-websocket'; export function ConnectionIndicator() { - const { status } = useWebSocket(); + const { status, isReconnecting } = useWebSocket(); const statusConfig = { connected: { icon: Wifi, color: 'text-green-600', label: 'Connected' }, - connecting: { icon: Wifi, color: 'text-yellow-500', label: 'Connecting...' }, - disconnected: { icon: WifiOff, color: 'text-red-500', label: 'Disconnected' }, + connecting: { + icon: RefreshCw, + color: 'text-yellow-500', + label: 'Connecting...', + animate: true, + }, + disconnected: { + icon: isReconnecting ? RefreshCw : WifiOff, + color: isReconnecting ? 'text-amber-500' : 'text-red-500', + label: isReconnecting ? 'Reconnecting...' : 'Disconnected', + animate: isReconnecting, + }, }; const config = statusConfig[status]; @@ -21,7 +31,7 @@ export function ConnectionIndicator() { return (
- + {config.label}
); diff --git a/ui/src/hooks/use-websocket.ts b/ui/src/hooks/use-websocket.ts index 198b10ca..81164214 100644 --- a/ui/src/hooks/use-websocket.ts +++ b/ui/src/hooks/use-websocket.ts @@ -18,6 +18,7 @@ type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'; export function useWebSocket() { const [status, setStatus] = useState('disconnected'); + const [isReconnecting, setIsReconnecting] = useState(false); const wsRef = useRef(null); const queryClient = useQueryClient(); const reconnectAttempts = useRef(0); @@ -75,6 +76,7 @@ export function useWebSocket() { ws.onopen = () => { setStatus('connected'); + setIsReconnecting(false); reconnectAttempts.current = 0; console.log('[WS] Connected'); }; @@ -97,6 +99,7 @@ export function useWebSocket() { // Attempt reconnect with exponential backoff if (reconnectAttempts.current < maxReconnectAttempts) { + setIsReconnecting(true); const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000); reconnectAttempts.current++; console.log(`[WS] Reconnecting in ${delay}ms (attempt ${reconnectAttempts.current})`); @@ -104,6 +107,8 @@ export function useWebSocket() { reconnectTimeoutRef.current = setTimeout(() => { connectRef.current(); }, delay); + } else { + setIsReconnecting(false); } }; @@ -117,6 +122,7 @@ export function useWebSocket() { const disconnect = useCallback(() => { reconnectAttempts.current = maxReconnectAttempts; // Prevent reconnect + setIsReconnecting(false); if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; @@ -142,5 +148,8 @@ export function useWebSocket() { return () => clearInterval(interval); }, []); - return useMemo(() => ({ status, connect, disconnect }), [status, connect, disconnect]); + return useMemo( + () => ({ status, isReconnecting, connect, disconnect }), + [status, isReconnecting, connect, disconnect] + ); } diff --git a/ui/src/pages/settings/index.tsx b/ui/src/pages/settings/index.tsx index b7d447af..1571fdde 100644 --- a/ui/src/pages/settings/index.tsx +++ b/ui/src/pages/settings/index.tsx @@ -115,7 +115,25 @@ function SettingsPageInner() { return (
- + {/* Mobile View - Stacked vertically */} +
+
+ +
+ + }> + {activeTab === 'websearch' && } + {activeTab === 'globalenv' && } + {activeTab === 'thinking' && } + {activeTab === 'proxy' && } + {activeTab === 'auth' && } + {activeTab === 'backups' && } + + +
+ + {/* Desktop View - Side-by-side panels */} + {/* Left Panel - Settings Controls */}
diff --git a/ui/src/pages/settings/sections/websearch/index.tsx b/ui/src/pages/settings/sections/websearch/index.tsx index 5891f6d2..7b061fd8 100644 --- a/ui/src/pages/settings/sections/websearch/index.tsx +++ b/ui/src/pages/settings/sections/websearch/index.tsx @@ -7,7 +7,7 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { RefreshCw, CheckCircle2, AlertCircle } from 'lucide-react'; +import { RefreshCw, CheckCircle2, AlertCircle, Package } from 'lucide-react'; import { useWebSearchConfig, useRawConfig } from '../../hooks'; import { ProviderCard } from './provider-card'; @@ -141,6 +141,21 @@ export default function WebSearchSection() {

Providers

+ {/* Empty state when no providers available */} + {!status?.geminiCli && !status?.opencodeCli && !status?.grokCli && !statusLoading && ( +
+ +

No providers configured

+

+ Install CLI tools to enable web search providers +

+ +
+ )} +