feat(ui): refactor style ui & add Models page with provider settings (#379)
This commit is contained in:
committed by
GitHub
Unverified
parent
3d664c017a
commit
905ce02b0b
@@ -6,17 +6,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Home,
|
||||
MessageSquare,
|
||||
Radio,
|
||||
Network,
|
||||
Puzzle,
|
||||
Clock,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Settings as SettingsIcon,
|
||||
PanelLeftClose,
|
||||
PanelLeft,
|
||||
Plus,
|
||||
Terminal,
|
||||
ExternalLink,
|
||||
Trash2,
|
||||
Cpu,
|
||||
LayoutDashboard,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
@@ -26,6 +27,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logoSvg from '@/assets/logo.svg';
|
||||
|
||||
type SessionBucketKey =
|
||||
| 'today'
|
||||
@@ -51,23 +53,29 @@ function NavItem({ to, icon, label, badge, collapsed, onClick }: NavItemProps) {
|
||||
onClick={onClick}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[14px] font-medium transition-colors',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80',
|
||||
isActive
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground',
|
||||
collapsed && 'justify-center px-2'
|
||||
? 'bg-black/5 dark:bg-white/10 text-foreground'
|
||||
: '',
|
||||
collapsed && 'justify-center px-0'
|
||||
)
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
{!collapsed && (
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<span className="flex-1">{label}</span>
|
||||
{badge && (
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{badge}
|
||||
</Badge>
|
||||
<div className={cn("flex shrink-0 items-center justify-center", isActive ? "text-foreground" : "text-muted-foreground")}>
|
||||
{icon}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{label}</span>
|
||||
{badge && (
|
||||
<Badge variant="secondary" className="ml-auto shrink-0">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -161,23 +169,46 @@ export function Sidebar() {
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ to: '/cron', icon: <Clock className="h-5 w-5" />, label: t('sidebar.cronTasks') },
|
||||
{ to: '/skills', icon: <Puzzle className="h-5 w-5" />, label: t('sidebar.skills') },
|
||||
{ to: '/channels', icon: <Radio className="h-5 w-5" />, label: t('sidebar.channels') },
|
||||
{ to: '/dashboard', icon: <Home className="h-5 w-5" />, label: t('sidebar.dashboard') },
|
||||
{ to: '/settings', icon: <Settings className="h-5 w-5" />, label: t('sidebar.settings') },
|
||||
{ to: '/models', icon: <Cpu className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.models', 'Models') },
|
||||
{ to: '/channels', icon: <Network className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.channels') },
|
||||
{ to: '/skills', icon: <Puzzle className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.skills') },
|
||||
{ to: '/cron', icon: <Clock className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.cronTasks') },
|
||||
{ to: '/dashboard', icon: <LayoutDashboard className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.dashboard') },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'flex shrink-0 flex-col border-r bg-background transition-all duration-300',
|
||||
'flex shrink-0 flex-col border-r bg-[#eae8e1]/60 dark:bg-background transition-all duration-300',
|
||||
sidebarCollapsed ? 'w-16' : 'w-64'
|
||||
)}
|
||||
>
|
||||
{/* Top Header Toggle */}
|
||||
<div className={cn("flex items-center p-2 h-12", sidebarCollapsed ? "justify-center" : "justify-between")}>
|
||||
{!sidebarCollapsed && (
|
||||
<div className="flex items-center gap-2 px-2 overflow-hidden">
|
||||
<img src={logoSvg} alt="ClawX" className="h-5 w-auto shrink-0" />
|
||||
<span className="text-sm font-semibold truncate whitespace-nowrap text-foreground/90">
|
||||
ClawX
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10"
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<PanelLeft className="h-[18px] w-[18px]" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-[18px] w-[18px]" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-hidden flex flex-col p-2 gap-1">
|
||||
{/* Chat nav item: acts as "New Chat" button, never highlighted as active */}
|
||||
<nav className="flex flex-col px-2 gap-0.5">
|
||||
<button
|
||||
onClick={() => {
|
||||
const { messages } = useChatStore.getState();
|
||||
@@ -185,13 +216,15 @@ export function Sidebar() {
|
||||
navigate('/');
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||
'hover:bg-accent hover:text-accent-foreground text-muted-foreground',
|
||||
sidebarCollapsed && 'justify-center px-2',
|
||||
'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-[14px] font-medium transition-colors mb-2',
|
||||
'bg-white dark:bg-accent shadow-sm border border-black/5 dark:border-white/10 text-foreground',
|
||||
sidebarCollapsed && 'justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<MessageSquare className="h-5 w-5 shrink-0" />
|
||||
{!sidebarCollapsed && <span className="flex-1 text-left">{t('sidebar.newChat')}</span>}
|
||||
<div className="flex shrink-0 items-center justify-center text-foreground/80">
|
||||
<Plus className="h-[18px] w-[18px]" strokeWidth={2} />
|
||||
</div>
|
||||
{!sidebarCollapsed && <span className="flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap">{t('sidebar.newChat')}</span>}
|
||||
</button>
|
||||
|
||||
{navItems.map((item) => (
|
||||
@@ -201,83 +234,93 @@ export function Sidebar() {
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Session list — below Settings, only when expanded */}
|
||||
{!sidebarCollapsed && sessions.length > 0 && (
|
||||
<div className="mt-1 overflow-y-auto max-h-72 space-y-0.5">
|
||||
{sessionBuckets.map((bucket) => (
|
||||
bucket.sessions.length > 0 ? (
|
||||
<div key={bucket.key} className="pt-1">
|
||||
<div className="px-3 py-1 text-[11px] font-medium text-muted-foreground/80">
|
||||
{bucket.label}
|
||||
</div>
|
||||
{bucket.sessions.map((s) => (
|
||||
<div key={s.key} className="group relative flex items-center">
|
||||
<button
|
||||
onClick={() => { switchSession(s.key); navigate('/'); }}
|
||||
className={cn(
|
||||
'w-full text-left rounded-md px-3 py-1.5 text-sm truncate transition-colors pr-7',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
isOnChat && currentSessionKey === s.key
|
||||
? 'bg-accent/60 text-accent-foreground font-medium'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{getSessionLabel(s.key, s.displayName, s.label)}
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete session"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSessionToDelete({
|
||||
key: s.key,
|
||||
label: getSessionLabel(s.key, s.displayName, s.label),
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'absolute right-1 flex items-center justify-center rounded p-0.5 transition-opacity',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'text-muted-foreground hover:text-destructive hover:bg-destructive/10',
|
||||
)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Session list — below Settings, only when expanded */}
|
||||
{!sidebarCollapsed && sessions.length > 0 && (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden px-2 mt-4 space-y-0.5 pb-2">
|
||||
{sessionBuckets.map((bucket) => (
|
||||
bucket.sessions.length > 0 ? (
|
||||
<div key={bucket.key} className="pt-2">
|
||||
<div className="px-2.5 pb-1 text-[11px] font-medium text-muted-foreground/60 tracking-tight">
|
||||
{bucket.label}
|
||||
</div>
|
||||
{bucket.sessions.map((s) => (
|
||||
<div key={s.key} className="group relative flex items-center">
|
||||
<button
|
||||
onClick={() => { switchSession(s.key); navigate('/'); }}
|
||||
className={cn(
|
||||
'w-full text-left rounded-lg px-2.5 py-1.5 text-[13px] truncate transition-colors pr-7',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5',
|
||||
isOnChat && currentSessionKey === s.key
|
||||
? 'bg-black/5 dark:bg-white/10 text-foreground font-medium'
|
||||
: 'text-foreground/75',
|
||||
)}
|
||||
>
|
||||
{getSessionLabel(s.key, s.displayName, s.label)}
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete session"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSessionToDelete({
|
||||
key: s.key,
|
||||
label: getSessionLabel(s.key, s.displayName, s.label),
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'absolute right-1 flex items-center justify-center rounded p-0.5 transition-opacity',
|
||||
'opacity-0 group-hover:opacity-100',
|
||||
'text-muted-foreground hover:text-destructive hover:bg-destructive/10',
|
||||
)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-2 space-y-2">
|
||||
<div className="p-2 mt-auto">
|
||||
{devModeUnlocked && !sidebarCollapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
className="w-full justify-start mb-1 hover:bg-black/5 dark:hover:bg-white/5"
|
||||
onClick={openDevConsole}
|
||||
>
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
{t('sidebar.devConsole')}
|
||||
<ExternalLink className="h-3 w-3 ml-auto" />
|
||||
<div className="flex shrink-0 items-center justify-center text-muted-foreground">
|
||||
<Terminal className="h-[18px] w-[18px] mr-2.5" strokeWidth={2} />
|
||||
</div>
|
||||
<span className="font-medium text-[14px] flex-1 text-left text-foreground/80 overflow-hidden text-ellipsis whitespace-nowrap">{t('sidebar.devConsole')}</span>
|
||||
<ExternalLink className="h-3 w-3 shrink-0 ml-auto opacity-50 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-full"
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[14px] font-medium transition-colors',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80',
|
||||
isActive && 'bg-black/5 dark:bg-white/10 text-foreground',
|
||||
sidebarCollapsed ? 'justify-center px-0' : ''
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div className={cn("flex shrink-0 items-center justify-center", isActive ? "text-foreground" : "text-muted-foreground")}>
|
||||
<SettingsIcon className="h-[18px] w-[18px]" strokeWidth={2} />
|
||||
</div>
|
||||
{!sidebarCollapsed && <span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{t('sidebar.settings')}</span>}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user