mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user