mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
refactor(ui): architecture and layout
This commit is contained in:
1 parent
944d933f34
commit
28202bac0d
8 files changed
+360
-177
No files matched your search
+23
-58
@@ -1,15 +1,11 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
|
||||
import { lazy } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/app-sidebar';
|
||||
import { ThemeToggle } from '@/components/theme-toggle';
|
||||
import { GitHubLink } from '@/components/github-link';
|
||||
import { ConnectionIndicator } from '@/components/connection-indicator';
|
||||
import { LocalhostDisclaimer } from '@/components/localhost-disclaimer';
|
||||
import { Toaster } from 'sonner';
|
||||
import { queryClient } from '@/lib/query-client';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
import { Layout } from '@/components/layout';
|
||||
|
||||
// Eager load: HomePage (initial route)
|
||||
import { HomePage } from '@/pages';
|
||||
|
||||
@@ -33,58 +29,27 @@ const SettingsPage = lazy(() =>
|
||||
const HealthPage = lazy(() => import('@/pages/health').then((m) => ({ default: m.HealthPage })));
|
||||
const SharedPage = lazy(() => import('@/pages/shared').then((m) => ({ default: m.SharedPage })));
|
||||
|
||||
// Loading fallback for lazy-loaded pages
|
||||
function PageLoader() {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout() {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<main className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<header className="flex h-12 items-center justify-end px-4 border-b shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<ConnectionIndicator />
|
||||
<GitHubLink />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 overflow-auto min-h-0">
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
<LocalhostDisclaimer />
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||
<Route path="/api" element={<ApiPage />} />
|
||||
<Route path="/cliproxy" element={<CliproxyPage />} />
|
||||
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
||||
<Route path="/accounts" element={<AccountsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/health" element={<HealthPage />} />
|
||||
<Route path="/shared" element={<SharedPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster position="top-right" />
|
||||
</BrowserRouter>
|
||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||
<Route path="/api" element={<ApiPage />} />
|
||||
<Route path="/cliproxy" element={<CliproxyPage />} />
|
||||
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
||||
<Route path="/accounts" element={<AccountsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/health" element={<HealthPage />} />
|
||||
<Route path="/shared" element={<SharedPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster position="top-right" />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Suspense } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/app-sidebar';
|
||||
import { ThemeToggle } from '@/components/theme-toggle';
|
||||
import { GitHubLink } from '@/components/github-link';
|
||||
import { ConnectionIndicator } from '@/components/connection-indicator';
|
||||
import { LocalhostDisclaimer } from '@/components/localhost-disclaimer';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
function PageLoader() {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<main className="flex-1 flex flex-col min-h-0 overflow-hidden bg-background">
|
||||
<header className="flex h-14 items-center justify-between px-6 border-b shrink-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="font-semibold text-lg tracking-tight">CCS Config</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ConnectionIndicator />
|
||||
<GitHubLink />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 overflow-auto min-h-0">
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
<LocalhostDisclaimer />
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ThemeProviderContext } from '@/contexts/theme-context';
|
||||
import type { Theme } from '@/contexts/theme-context';
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'vite-ui-theme',
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
// Track system preference separately
|
||||
// Initialize with window.matchMedia value if available
|
||||
const [systemIsDark, setSystemIsDark] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
setSystemIsDark(e.matches);
|
||||
};
|
||||
|
||||
media.addEventListener('change', listener);
|
||||
return () => media.removeEventListener('change', listener);
|
||||
}, []);
|
||||
|
||||
// Derive effective theme
|
||||
const isDark = theme === 'dark' || (theme === 'system' && systemIsDark);
|
||||
|
||||
// Update DOM when effective theme changes
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(isDark ? 'dark' : 'light');
|
||||
}, [isDark]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
isDark,
|
||||
};
|
||||
|
||||
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>;
|
||||
}
|
||||
@@ -3,11 +3,17 @@ import { Button } from '@/components/ui/button';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { isDark, toggle } = useTheme();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button variant="ghost" size="icon" onClick={toggle}>
|
||||
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
export type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
isDark: boolean;
|
||||
};
|
||||
|
||||
export const initialState: ThemeProviderState = {
|
||||
theme: 'system',
|
||||
setTheme: () => null,
|
||||
isDark: false,
|
||||
};
|
||||
|
||||
export const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
@@ -1,24 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeProviderContext } from '@/contexts/theme-context';
|
||||
|
||||
function getInitialTheme() {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const stored = localStorage.getItem('ccs-theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return stored === 'dark' || (!stored && prefersDark);
|
||||
}
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
export function useTheme() {
|
||||
const [isDark, setIsDark] = useState(getInitialTheme);
|
||||
if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider');
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}, [isDark]);
|
||||
|
||||
const toggle = () => {
|
||||
const newValue = !isDark;
|
||||
setIsDark(newValue);
|
||||
localStorage.setItem('ccs-theme', newValue ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
return { isDark, toggle };
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -149,3 +149,53 @@
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-in {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: both;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
.slide-in-from-bottom-4 {
|
||||
animation-name: enter;
|
||||
}
|
||||
|
||||
.zoom-in-95 {
|
||||
animation-name: zoom-in;
|
||||
}
|
||||
+151
-95
@@ -1,6 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { StatCard } from '@/components/stat-card';
|
||||
import { HeroSection } from '@/components/hero-section';
|
||||
import { QuickCommands } from '@/components/quick-commands';
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
BookOpen,
|
||||
FolderOpen,
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { useOverview } from '@/hooks/use-overview';
|
||||
import { useSharedSummary } from '@/hooks/use-shared';
|
||||
@@ -89,7 +87,7 @@ export function HomePage() {
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="p-6 max-w-7xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{/* Hero Section */}
|
||||
<HeroSection
|
||||
version={overview?.version}
|
||||
@@ -100,7 +98,7 @@ export function HomePage() {
|
||||
|
||||
{/* Configuration Warning */}
|
||||
{shared?.symlinkStatus && !shared.symlinkStatus.valid && (
|
||||
<Alert variant="warning">
|
||||
<Alert variant="warning" className="animate-in zoom-in-95 duration-300">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Configuration Required</AlertTitle>
|
||||
<AlertDescription>{shared.symlinkStatus.message}</AlertDescription>
|
||||
@@ -108,97 +106,155 @@ export function HomePage() {
|
||||
)}
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
title="API Profiles"
|
||||
value={overview?.profiles ?? 0}
|
||||
icon={Key}
|
||||
variant="accent"
|
||||
subtitle="Settings-based"
|
||||
onClick={() => navigate('/api')}
|
||||
/>
|
||||
<StatCard
|
||||
title="CLIProxy"
|
||||
value={overview?.cliproxy ?? 0}
|
||||
icon={Zap}
|
||||
variant="accent"
|
||||
subtitle={`${overview?.cliproxyProviders ?? 0} auth + ${overview?.cliproxyVariants ?? 0} custom`}
|
||||
onClick={() => navigate('/cliproxy')}
|
||||
/>
|
||||
<StatCard
|
||||
title="Accounts"
|
||||
value={overview?.accounts ?? 0}
|
||||
icon={Users}
|
||||
variant="default"
|
||||
subtitle="Isolated instances"
|
||||
onClick={() => navigate('/accounts')}
|
||||
/>
|
||||
<StatCard
|
||||
title="Health"
|
||||
value={overview?.health ? `${overview.health.passed}/${overview.health.total}` : '-'}
|
||||
icon={Activity}
|
||||
variant={healthVariant}
|
||||
subtitle="System checks"
|
||||
onClick={() => navigate('/health')}
|
||||
/>
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold tracking-tight">System Status</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
title="API Profiles"
|
||||
value={overview?.profiles ?? 0}
|
||||
icon={Key}
|
||||
variant="accent"
|
||||
subtitle="Settings-based"
|
||||
onClick={() => navigate('/api')}
|
||||
/>
|
||||
<StatCard
|
||||
title="CLIProxy"
|
||||
value={overview?.cliproxy ?? 0}
|
||||
icon={Zap}
|
||||
variant="accent"
|
||||
subtitle={`${overview?.cliproxyProviders ?? 0} auth + ${overview?.cliproxyVariants ?? 0} custom`}
|
||||
onClick={() => navigate('/cliproxy')}
|
||||
/>
|
||||
<StatCard
|
||||
title="Accounts"
|
||||
value={overview?.accounts ?? 0}
|
||||
icon={Users}
|
||||
variant="default"
|
||||
subtitle="Isolated instances"
|
||||
onClick={() => navigate('/accounts')}
|
||||
/>
|
||||
<StatCard
|
||||
title="Health"
|
||||
value={overview?.health ? `${overview.health.passed}/${overview.health.total}` : '-'}
|
||||
icon={Activity}
|
||||
variant={healthVariant}
|
||||
subtitle="System checks"
|
||||
onClick={() => navigate('/health')}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column: Quick Actions & Shared Data */}
|
||||
<div className="space-y-8 lg:col-span-2">
|
||||
{/* Quick Actions */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold tracking-tight">Quick Actions</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Card
|
||||
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]"
|
||||
onClick={() => navigate('/api')}
|
||||
>
|
||||
<CardContent className="p-6 flex items-center gap-4">
|
||||
<div className="p-3 rounded-xl bg-primary/10 text-primary group-hover:scale-110 transition-transform">
|
||||
<Plus className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">New Profile</h3>
|
||||
<p className="text-muted-foreground text-sm">Create a new API profile</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]"
|
||||
onClick={() => navigate('/health')}
|
||||
>
|
||||
<CardContent className="p-6 flex items-center gap-4">
|
||||
<div className="p-3 rounded-xl bg-primary/10 text-primary group-hover:scale-110 transition-transform">
|
||||
<Stethoscope className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Run Doctor</h3>
|
||||
<p className="text-muted-foreground text-sm">Check system health</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]"
|
||||
onClick={() => window.open('https://github.com/kaitranntt/ccs', '_blank')}
|
||||
>
|
||||
<CardContent className="p-6 flex items-center gap-4">
|
||||
<div className="p-3 rounded-xl bg-muted text-muted-foreground group-hover:text-primary transition-colors">
|
||||
<BookOpen className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Documentation</h3>
|
||||
<p className="text-muted-foreground text-sm">View guides & reference</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="group cursor-pointer hover:border-primary/50 transition-all hover:shadow-md active:scale-[0.99]"
|
||||
onClick={() => navigate('/shared')}
|
||||
>
|
||||
<CardContent className="p-6 flex items-center gap-4">
|
||||
<div className="p-3 rounded-xl bg-muted text-muted-foreground group-hover:text-primary transition-colors">
|
||||
<FolderOpen className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Shared Data</h3>
|
||||
<p className="text-muted-foreground text-sm">Manage resources</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Shared Data Stats */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold tracking-tight">Resource Summary</h2>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col items-center justify-center p-4 rounded-xl bg-muted/30 hover:bg-muted/50 transition-colors">
|
||||
<span className="text-3xl font-bold font-mono text-primary mb-1">
|
||||
{shared?.commands ?? 0}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">Commands</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center p-4 rounded-xl bg-muted/30 hover:bg-muted/50 transition-colors">
|
||||
<span className="text-3xl font-bold font-mono text-primary mb-1">
|
||||
{shared?.skills ?? 0}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">Skills</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center p-4 rounded-xl bg-muted/30 hover:bg-muted/50 transition-colors">
|
||||
<span className="text-3xl font-bold font-mono text-primary mb-1">
|
||||
{shared?.agents ?? 0}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">Agents</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Quick Commands */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold tracking-tight">Command Palette</h2>
|
||||
<QuickCommands />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-3">
|
||||
<Button onClick={() => navigate('/api')} className="gap-2">
|
||||
<Plus className="w-4 h-4" /> New Profile
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => navigate('/health')} className="gap-2">
|
||||
<Stethoscope className="w-4 h-4" /> Run Doctor
|
||||
</Button>
|
||||
<Button variant="outline" asChild className="gap-2">
|
||||
<a href="https://github.com/kaitranntt/ccs" target="_blank" rel="noopener noreferrer">
|
||||
<BookOpen className="w-4 h-4" /> Documentation
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Commands */}
|
||||
<QuickCommands />
|
||||
|
||||
{/* Shared Data Summary */}
|
||||
<Card className="group hover:border-primary/50 transition-colors">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<FolderOpen className="w-5 h-5 text-muted-foreground" />
|
||||
Shared Data
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate('/shared')}
|
||||
className="gap-1 opacity-70 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
View All <ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-6 text-sm">
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/50">
|
||||
<span className="text-xl font-bold font-mono">{shared?.commands ?? 0}</span>
|
||||
<span className="text-muted-foreground">Commands</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/50">
|
||||
<span className="text-xl font-bold font-mono">{shared?.skills ?? 0}</span>
|
||||
<span className="text-muted-foreground">Skills</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/50">
|
||||
<span className="text-xl font-bold font-mono">{shared?.agents ?? 0}</span>
|
||||
<span className="text-muted-foreground">Agents</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user