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
This commit is contained in:
kaitranntt
2026-02-03 22:33:31 -05:00
parent cb8de2c8e8
commit 4d87a649de
4 changed files with 61 additions and 9 deletions
@@ -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 (
<div className={`flex items-center gap-1 text-sm ${config.color}`}>
<Icon className="w-4 h-4" />
<Icon className={`w-4 h-4 ${config.animate ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline">{config.label}</span>
</div>
);
+10 -1
View File
@@ -18,6 +18,7 @@ type ConnectionStatus = 'connecting' | 'connected' | 'disconnected';
export function useWebSocket() {
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
const [isReconnecting, setIsReconnecting] = useState(false);
const wsRef = useRef<WebSocket | null>(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]
);
}
+19 -1
View File
@@ -115,7 +115,25 @@ function SettingsPageInner() {
return (
<div className="h-[calc(100vh-100px)]">
<PanelGroup direction="horizontal" className="h-full">
{/* Mobile View - Stacked vertically */}
<div className="md:hidden h-full flex flex-col">
<div className="border-b bg-background p-4">
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} />
</div>
<SectionErrorBoundary>
<Suspense fallback={<SectionSkeleton />}>
{activeTab === 'websearch' && <WebSearchSection />}
{activeTab === 'globalenv' && <GlobalEnvSection />}
{activeTab === 'thinking' && <ThinkingSection />}
{activeTab === 'proxy' && <ProxySection />}
{activeTab === 'auth' && <AuthSection />}
{activeTab === 'backups' && <BackupsSection />}
</Suspense>
</SectionErrorBoundary>
</div>
{/* Desktop View - Side-by-side panels */}
<PanelGroup direction="horizontal" className="h-full hidden md:flex">
{/* Left Panel - Settings Controls */}
<Panel defaultSize={40} minSize={30} maxSize={55}>
<div className="h-full border-r flex flex-col bg-muted/30 relative">
@@ -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() {
<div className="space-y-3">
<h3 className="text-base font-medium">Providers</h3>
{/* Empty state when no providers available */}
{!status?.geminiCli && !status?.opencodeCli && !status?.grokCli && !statusLoading && (
<div className="flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-lg text-center bg-muted/30">
<Package className="w-12 h-12 text-muted-foreground mb-3 opacity-30" />
<p className="font-medium text-foreground mb-1">No providers configured</p>
<p className="text-sm text-muted-foreground mb-4">
Install CLI tools to enable web search providers
</p>
<Button variant="outline" size="sm" onClick={fetchStatus}>
<RefreshCw className="w-4 h-4 mr-2" />
Check for providers
</Button>
</div>
)}
<ProviderCard
name="gemini"
label="Google Gemini CLI (1000 req/day free)"