- HubPage: agent summary stats, cards, status badges, progress bars, refresh - LogsPage: activity feed from tasks, status filter, loading skeleton - ProjectsPage: project cards with status badges and agent counts - SessionsPage: responsive table/card view with model/token info - SettingsPage: dark mode toggle, gateway URL, refresh interval persist - ThemeProvider with dark/light mode via CSS custom properties - useLocalStorage hook for settings persistence - Loading/error/empty states across all pages - npm run build passes cleanly
127 lines
4.7 KiB
TypeScript
127 lines
4.7 KiB
TypeScript
import { useTheme } from '../hooks/useTheme'
|
|
import { useLocalStorage } from '../hooks/useLocalStorage'
|
|
import { Sun, Moon, Monitor, Zap, Clock } from 'lucide-react'
|
|
|
|
const REFRESH_PRESETS = [
|
|
{ label: '5s', value: 5_000 },
|
|
{ label: '10s', value: 10_000 },
|
|
{ label: '30s', value: 30_000 },
|
|
{ label: '60s', value: 60_000 },
|
|
]
|
|
|
|
export default function SettingsPage() {
|
|
const { isDark, toggleTheme } = useTheme()
|
|
const [gatewayUrl, setGatewayUrl] = useLocalStorage('cc-gateway-url', '')
|
|
const [refreshInterval, setRefreshInterval] = useLocalStorage('cc-refresh-interval', 30_000)
|
|
|
|
return (
|
|
<div className="space-y-8 max-w-2xl">
|
|
<header>
|
|
<h1 className="text-2xl font-bold">Settings</h1>
|
|
<p className="text-on-surface-variant">Application preferences</p>
|
|
</header>
|
|
|
|
{/* Appearance */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<Monitor size={20} className="text-primary" />
|
|
Appearance
|
|
</h2>
|
|
|
|
<div className="p-5 rounded-xl border border-surface-light bg-surface-dark">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="font-medium">Dark Mode</p>
|
|
<p className="text-sm text-on-surface-variant">Toggle between dark and light themes</p>
|
|
</div>
|
|
<button
|
|
onClick={toggleTheme}
|
|
className={`relative w-14 h-8 rounded-full transition-colors duration-200 ${
|
|
isDark ? 'bg-primary' : 'bg-surface-lighter'
|
|
}`}
|
|
aria-label="Toggle dark mode"
|
|
>
|
|
<div
|
|
className={`absolute top-1 h-6 w-6 rounded-full bg-white transition-transform duration-200 flex items-center justify-center ${
|
|
isDark ? 'translate-x-7' : 'translate-x-1'
|
|
}`}
|
|
>
|
|
{isDark ? <Moon size={14} className="text-surface-darkest" /> : <Sun size={14} className="text-yellow-500" />}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Connection */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<Zap size={20} className="text-primary" />
|
|
Connection
|
|
</h2>
|
|
|
|
<div className="p-5 rounded-xl border border-surface-light bg-surface-dark space-y-3">
|
|
<div>
|
|
<label htmlFor="gateway-url" className="block text-sm font-medium mb-1">
|
|
Gateway URL
|
|
</label>
|
|
<input
|
|
id="gateway-url"
|
|
type="url"
|
|
value={gatewayUrl}
|
|
onChange={(e) => setGatewayUrl(e.target.value)}
|
|
placeholder="http://localhost:8080"
|
|
className="w-full px-3 py-2 rounded-lg border border-surface-light bg-surface-darkest text-on-surface placeholder:text-on-surface-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
|
|
/>
|
|
<p className="text-xs text-on-surface-muted mt-1">
|
|
The backend Gateway address for API requests
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Refresh */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<Clock size={20} className="text-primary" />
|
|
Auto Refresh
|
|
</h2>
|
|
|
|
<div className="p-5 rounded-xl border border-surface-light bg-surface-dark space-y-3">
|
|
<p className="text-sm text-on-surface-variant">Data refresh interval for agent status and logs</p>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="3"
|
|
step="1"
|
|
value={REFRESH_PRESETS.findIndex((p) => p.value === refreshInterval)}
|
|
onChange={(e) => {
|
|
const idx = parseInt(e.target.value)
|
|
setRefreshInterval(REFRESH_PRESETS[idx].value)
|
|
}}
|
|
className="w-full accent-primary"
|
|
/>
|
|
<div className="flex justify-between text-xs text-on-surface-muted">
|
|
{REFRESH_PRESETS.map((p) => (
|
|
<button
|
|
key={p.label}
|
|
onClick={() => setRefreshInterval(p.value)}
|
|
className={`px-3 py-1 rounded-lg transition-colors ${
|
|
refreshInterval === p.value
|
|
? 'bg-primary/10 text-primary'
|
|
: 'hover:bg-surface-light'
|
|
}`}
|
|
>
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
)
|
|
}
|