feat(ui): redesign settings page and update sidebar navigation (#383)

This commit is contained in:
DigHuang
2026-03-10 14:48:54 +08:00
committed by GitHub
Unverified
parent 19b5b2d540
commit d9ae0f3263
6 changed files with 618 additions and 906 deletions

158
pnpm-lock.yaml generated
View File

@@ -65,7 +65,7 @@ importers:
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@sliverp/qqbot':
specifier: ^1.5.4
version: 1.5.4(clawdbot@2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1595872)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3))(moltbot@0.1.0)(openclaw@2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.16.2(typescript@5.9.3)))
version: 1.5.4(clawdbot@2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1596832)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3))(moltbot@0.1.0)(openclaw@2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.16.2(typescript@5.9.3)))
'@soimy/dingtalk':
specifier: ^3.1.4
version: 3.1.4(openclaw@2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.16.2(typescript@5.9.3)))
@@ -1384,6 +1384,34 @@ packages:
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda-ext@3.15.0':
resolution: {integrity: sha512-wQwgSl7Qm8vH56oBt7IuWWDNNsDECkVMS000C92wl3PkbzjwZFiWzehwa+kF8Lr2BBMiCJNkI5nEabhYH3RN2Q==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda-ext@3.16.2':
resolution: {integrity: sha512-47d9myCJauZyzAlN7IK1eIt/4CcBMslF+yHy4q+yJotD/RV/S6qRpK2kGn+ybtdVjkPGNCoPkHKcyla9iIVjbw==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda@3.15.0':
resolution: {integrity: sha512-mDjyVulCTRYilm9Emm3lDMx7dbI1vzGqk28Pj28shartjERTUu8aUNDYOmVKNMLpUKS1akw7vy0lMF8t4qswxQ==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-cuda@3.16.2':
resolution: {integrity: sha512-LTBQFqjin7tyrLNJz0XWTB5QAHDsZV71/qiiRRjXdBKSZHVVaPLfdgxypGu7ggPeBNsv+MckRXdlH5C7yMtE4A==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@node-llama-cpp/linux-x64-vulkan@3.15.0':
resolution: {integrity: sha512-htVIthQKq/rr8v5e7NiVtcHsstqTBAAC50kUymmDMbrzAu6d/EHacCJpNbU57b1UUa1nKN5cBqr6Jr+QqEalMA==}
engines: {node: '>=20.0.0'}
@@ -1448,6 +1476,42 @@ packages:
cpu: [arm64, x64]
os: [win32]
'@node-llama-cpp/win-x64-cuda-ext@3.15.0':
resolution: {integrity: sha512-KQoNH9KsVtqGVXaRdPrnHPrg5w3KOM7CfynPmG1m16gmjmDSIspdPg/Dbg6DgHBfkdAzt+duRZEBk8Bg8KbDHw==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-cuda-ext@3.16.2':
resolution: {integrity: sha512-sdv4Kzn9bOQWNBRvw6B/zcn8dQRfZhjIHv5AfDBIOfRlSCgjebFpBeYUoU4wZPpjr3ISwcqO5MEWsw+AbUdV3Q==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-cuda@3.15.0':
resolution: {integrity: sha512-2Kyu1roDwXwFLaJgGZQISIXP9lCDZtJCx/DRcmrYRHcSUFCzo5ikOuAECyliSSQmRUAvvlRCuD+GrTcegbhojA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-cuda@3.16.2':
resolution: {integrity: sha512-jStDELHrU3rKQMOk5Hs5bWEazyjE2hzHwpNf6SblOpaGkajM/HJtxEZoL0mLHJx5qeXs4oOVkr7AzuLy0WPpNA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-vulkan@3.15.0':
resolution: {integrity: sha512-sH+K7lO49WrUvCCC3RPddCBrn2ZQwKCXKL90P/NZicMRgxTPFZEVSU2jXR/bu1K8B+4lNN+z5OEbjSYs7cKEcA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64-vulkan@3.16.2':
resolution: {integrity: sha512-9xuHFCOhCQjZgQSFrk79EuSKn9nGWt/SAq/3wujQSQLtgp8jGdtZgwcmuDUoemInf10en2dcOmEt7t8dQdC3XA==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@node-llama-cpp/win-x64@3.15.0':
resolution: {integrity: sha512-gWhtc8l3HOry5guO46YfFohLQnF0NfL4On0GAO8E27JiYYxHO9nHSCfFif4+U03+FfHquZXL0znJ1qPVOiwOPw==}
engines: {node: '>=20.0.0'}
@@ -3597,8 +3661,8 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
devtools-protocol@0.0.1595872:
resolution: {integrity: sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==}
devtools-protocol@0.0.1596832:
resolution: {integrity: sha512-IwRVIiCa4mpaKeLcZ2cmGpG0hP8ls3zj3zg87Z/JwULm2xYmhOcMrwdeHos6xaANQHGEXzSCzji+6kEuZu873A==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -4141,6 +4205,7 @@ packages:
glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@13.0.6:
@@ -4353,11 +4418,6 @@ packages:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'}
ipull@3.9.3:
resolution: {integrity: sha512-ZMkxaopfwKHwmEuGDYx7giNBdLxbHbRCWcQVA1D2eqE4crUguupfxej6s7UqbidYEwT69dkyumYkY8DPHIxF9g==}
engines: {node: '>=18.0.0'}
hasBin: true
ipull@3.9.5:
resolution: {integrity: sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==}
engines: {node: '>=18.0.0'}
@@ -6125,6 +6185,7 @@ packages:
tar@7.5.4:
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
engines: {node: '>=18'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tar@7.5.9:
resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==}
@@ -8273,6 +8334,18 @@ snapshots:
'@node-llama-cpp/linux-armv7l@3.16.2':
optional: true
'@node-llama-cpp/linux-x64-cuda-ext@3.15.0':
optional: true
'@node-llama-cpp/linux-x64-cuda-ext@3.16.2':
optional: true
'@node-llama-cpp/linux-x64-cuda@3.15.0':
optional: true
'@node-llama-cpp/linux-x64-cuda@3.16.2':
optional: true
'@node-llama-cpp/linux-x64-vulkan@3.15.0':
optional: true
@@ -8303,6 +8376,24 @@ snapshots:
'@node-llama-cpp/win-arm64@3.16.2':
optional: true
'@node-llama-cpp/win-x64-cuda-ext@3.15.0':
optional: true
'@node-llama-cpp/win-x64-cuda-ext@3.16.2':
optional: true
'@node-llama-cpp/win-x64-cuda@3.15.0':
optional: true
'@node-llama-cpp/win-x64-cuda@3.16.2':
optional: true
'@node-llama-cpp/win-x64-vulkan@3.15.0':
optional: true
'@node-llama-cpp/win-x64-vulkan@3.16.2':
optional: true
'@node-llama-cpp/win-x64@3.15.0':
optional: true
@@ -9145,9 +9236,9 @@ snapshots:
transitivePeerDependencies:
- debug
'@sliverp/qqbot@1.5.4(clawdbot@2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1595872)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3))(moltbot@0.1.0)(openclaw@2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.16.2(typescript@5.9.3)))':
'@sliverp/qqbot@1.5.4(clawdbot@2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1596832)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3))(moltbot@0.1.0)(openclaw@2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.16.2(typescript@5.9.3)))':
dependencies:
clawdbot: 2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1595872)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3)
clawdbot: 2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1596832)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3)
moltbot: 0.1.0
openclaw: 2026.3.1(@napi-rs/canvas@0.1.95)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.16.2(typescript@5.9.3))
@@ -10373,9 +10464,9 @@ snapshots:
chownr@3.0.0: {}
chromium-bidi@13.0.1(devtools-protocol@0.0.1595872):
chromium-bidi@13.0.1(devtools-protocol@0.0.1596832):
dependencies:
devtools-protocol: 0.0.1595872
devtools-protocol: 0.0.1596832
mitt: 3.0.1
zod: 3.25.76
@@ -10389,7 +10480,7 @@ snapshots:
dependencies:
clsx: 2.1.1
clawdbot@2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1595872)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3):
clawdbot@2026.1.24-3(@discordjs/opus@0.10.0(encoding@0.1.13))(@types/express@5.0.6)(devtools-protocol@0.0.1596832)(encoding@0.1.13)(opusscript@0.1.1)(typescript@5.9.3):
dependencies:
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
'@aws-sdk/client-bedrock': 3.1000.0
@@ -10413,7 +10504,7 @@ snapshots:
body-parser: 2.2.2
chalk: 5.6.2
chokidar: 5.0.0
chromium-bidi: 13.0.1(devtools-protocol@0.0.1595872)
chromium-bidi: 13.0.1(devtools-protocol@0.0.1596832)
cli-highlight: 2.1.11
commander: 14.0.3
croner: 9.1.0
@@ -10751,7 +10842,7 @@ snapshots:
dependencies:
dequal: 2.0.3
devtools-protocol@0.0.1595872: {}
devtools-protocol@0.0.1596832: {}
didyoumean@1.2.2: {}
@@ -11710,31 +11801,6 @@ snapshots:
ipaddr.js@2.3.0: {}
ipull@3.9.3:
dependencies:
'@tinyhttp/content-disposition': 2.2.4
async-retry: 1.3.3
chalk: 5.6.2
ci-info: 4.4.0
cli-spinners: 2.9.2
commander: 10.0.1
eventemitter3: 5.0.4
filenamify: 6.0.0
fs-extra: 11.3.3
is-unicode-supported: 2.1.0
lifecycle-utils: 2.1.0
lodash.debounce: 4.0.8
lowdb: 7.0.1
pretty-bytes: 6.1.1
pretty-ms: 8.0.0
sleep-promise: 9.1.0
slice-ansi: 7.1.2
stdout-update: 4.0.1
strip-ansi: 7.1.2
optionalDependencies:
'@reflink/reflink': 0.1.19
optional: true
ipull@3.9.5:
dependencies:
'@tinyhttp/content-disposition': 2.2.4
@@ -12685,7 +12751,7 @@ snapshots:
filenamify: 6.0.0
fs-extra: 11.3.3
ignore: 7.0.5
ipull: 3.9.3
ipull: 3.9.5
is-unicode-supported: 2.1.0
lifecycle-utils: 3.1.1
log-symbols: 7.0.1
@@ -12707,11 +12773,16 @@ snapshots:
'@node-llama-cpp/linux-arm64': 3.15.0
'@node-llama-cpp/linux-armv7l': 3.15.0
'@node-llama-cpp/linux-x64': 3.15.0
'@node-llama-cpp/linux-x64-cuda': 3.15.0
'@node-llama-cpp/linux-x64-cuda-ext': 3.15.0
'@node-llama-cpp/linux-x64-vulkan': 3.15.0
'@node-llama-cpp/mac-arm64-metal': 3.15.0
'@node-llama-cpp/mac-x64': 3.15.0
'@node-llama-cpp/win-arm64': 3.15.0
'@node-llama-cpp/win-x64': 3.15.0
'@node-llama-cpp/win-x64-cuda': 3.15.0
'@node-llama-cpp/win-x64-cuda-ext': 3.15.0
'@node-llama-cpp/win-x64-vulkan': 3.15.0
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -12752,11 +12823,16 @@ snapshots:
'@node-llama-cpp/linux-arm64': 3.16.2
'@node-llama-cpp/linux-armv7l': 3.16.2
'@node-llama-cpp/linux-x64': 3.16.2
'@node-llama-cpp/linux-x64-cuda': 3.16.2
'@node-llama-cpp/linux-x64-cuda-ext': 3.16.2
'@node-llama-cpp/linux-x64-vulkan': 3.16.2
'@node-llama-cpp/mac-arm64-metal': 3.16.2
'@node-llama-cpp/mac-x64': 3.16.2
'@node-llama-cpp/win-arm64': 3.16.2
'@node-llama-cpp/win-x64': 3.16.2
'@node-llama-cpp/win-x64-cuda': 3.16.2
'@node-llama-cpp/win-x64-cuda-ext': 3.16.2
'@node-llama-cpp/win-x64-vulkan': 3.16.2
typescript: 5.9.3
transitivePeerDependencies:
- supports-color

View File

@@ -9,7 +9,6 @@ import { Toaster } from 'sonner';
import i18n from './i18n';
import { MainLayout } from './components/layout/MainLayout';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Dashboard } from './pages/Dashboard';
import { Models } from './pages/Models';
import { Chat } from './pages/Chat';
import { Channels } from './pages/Channels';
@@ -166,7 +165,6 @@ function App() {
<Route element={<MainLayout />}>
<Route path="/" element={<Chat />} />
<Route path="/models" element={<Models />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/channels" element={<Channels />} />
<Route path="/skills" element={<Skills />} />
<Route path="/cron" element={<Cron />} />

View File

@@ -17,7 +17,6 @@ import {
ExternalLink,
Trash2,
Cpu,
LayoutDashboard,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useSettingsStore } from '@/stores/settings';
@@ -105,7 +104,6 @@ const INITIAL_NOW_MS = Date.now();
export function Sidebar() {
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
const sessions = useChatStore((s) => s.sessions);
const currentSessionKey = useChatStore((s) => s.currentSessionKey);
@@ -173,7 +171,6 @@ export function Sidebar() {
{ 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 (
@@ -286,21 +283,6 @@ export function Sidebar() {
{/* Footer */}
<div className="p-2 mt-auto">
{devModeUnlocked && !sidebarCollapsed && (
<Button
variant="ghost"
size="sm"
className="w-full justify-start mb-1 hover:bg-black/5 dark:hover:bg-white/5"
onClick={openDevConsole}
>
<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>
)}
<NavLink
to="/settings"
className={({ isActive }) =>
@@ -321,6 +303,26 @@ export function Sidebar() {
</>
)}
</NavLink>
<Button
variant="ghost"
className={cn(
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 h-auto text-[14px] font-medium transition-colors w-full mt-1',
'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80',
sidebarCollapsed ? 'justify-center px-0' : 'justify-start'
)}
onClick={openDevConsole}
>
<div className="flex shrink-0 items-center justify-center text-muted-foreground">
<Terminal className="h-[18px] w-[18px]" strokeWidth={2} />
</div>
{!sidebarCollapsed && (
<>
<span className="flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap">OpenClaw Page</span>
<ExternalLink className="h-3 w-3 shrink-0 ml-auto opacity-50 text-muted-foreground" />
</>
)}
</Button>
</div>
<ConfirmDialog

View File

@@ -13,6 +13,7 @@ import { Textarea } from '@/components/ui/textarea';
import { hostApiFetch } from '@/lib/host-api';
import { invokeIpc } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { useGatewayStore } from '@/stores/gateway';
// ── Types ────────────────────────────────────────────────────────
@@ -84,6 +85,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
const [attachments, setAttachments] = useState<FileAttachment[]>([]);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isComposingRef = useRef(false);
const gatewayStatus = useGatewayStore((s) => s.status);
// Auto-resize textarea
useEffect(() => {
@@ -407,7 +409,12 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
</Button>
</div>
<div className="mt-2.5 flex items-center justify-between gap-2 text-[11px] text-muted-foreground/60 px-4">
<span>Tip: switch sessions from the sidebar to keep context clean.</span>
<div className="flex items-center gap-1.5">
<div className={cn("w-1.5 h-1.5 rounded-full", gatewayStatus.state === 'running' ? "bg-green-500/80" : "bg-red-500/80")} />
<span>
gateway {gatewayStatus.state === 'running' ? 'connected' : gatewayStatus.state} | port: {gatewayStatus.port} {gatewayStatus.pid ? `| pid: ${gatewayStatus.pid}` : ''}
</span>
</div>
{hasFailedAttachments && (
<Button
variant="link"

View File

@@ -1,324 +0,0 @@
/**
* Dashboard Page
* Main overview page showing system status and quick actions
*/
import { useEffect, useState } from 'react';
import {
Activity,
MessageSquare,
Radio,
Puzzle,
Clock,
Settings,
Plus,
Terminal,
Wrench,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useGatewayStore } from '@/stores/gateway';
import { useChannelsStore } from '@/stores/channels';
import { useSkillsStore } from '@/stores/skills';
import { useSettingsStore } from '@/stores/settings';
import { StatusBadge } from '@/components/common/StatusBadge';
import { FeedbackState } from '@/components/common/FeedbackState';
import { hostApiFetch } from '@/lib/host-api';
import { trackUiEvent } from '@/lib/telemetry';
import { useTranslation } from 'react-i18next';
export function Dashboard() {
const { t } = useTranslation('dashboard');
const gatewayStatus = useGatewayStore((state) => state.status);
const { channels, fetchChannels } = useChannelsStore();
const { skills, fetchSkills } = useSkillsStore();
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
const isGatewayRunning = gatewayStatus.state === 'running';
const [uptime, setUptime] = useState(0);
// Track page view on mount only.
useEffect(() => {
trackUiEvent('dashboard.page_viewed');
}, []);
// Fetch data only when gateway is running.
useEffect(() => {
if (isGatewayRunning) {
fetchChannels();
fetchSkills();
}
}, [fetchChannels, fetchSkills, isGatewayRunning]);
// Calculate statistics safely
const connectedChannels = Array.isArray(channels) ? channels.filter((c) => c.status === 'connected').length : 0;
const enabledSkills = Array.isArray(skills) ? skills.filter((s) => s.enabled).length : 0;
// Update uptime periodically
useEffect(() => {
const updateUptime = () => {
if (gatewayStatus.connectedAt) {
setUptime(Math.floor((Date.now() - gatewayStatus.connectedAt) / 1000));
} else {
setUptime(0);
}
};
// Update immediately
updateUptime();
// Update every second
const interval = setInterval(updateUptime, 1000);
return () => clearInterval(interval);
}, [gatewayStatus.connectedAt]);
const openDevConsole = async () => {
try {
const result = await hostApiFetch<{
success: boolean;
url?: string;
error?: string;
}>('/api/gateway/control-ui');
if (result.success && result.url) {
trackUiEvent('dashboard.quick_action', { action: 'dev_console' });
window.electron.openExternal(result.url);
} else {
console.error('Failed to get Dev Console URL:', result.error);
}
} catch (err) {
console.error('Error opening Dev Console:', err);
}
};
return (
<div className="space-y-6">
{/* Status Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* Gateway Status */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('gateway')}</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<StatusBadge status={gatewayStatus.state} />
</div>
{gatewayStatus.state === 'running' && (
<p className="mt-1 text-xs text-muted-foreground">
{t('port', { port: gatewayStatus.port })} | {t('pid', { pid: gatewayStatus.pid || 'N/A' })}
</p>
)}
</CardContent>
</Card>
{/* Channels */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('channels')}</CardTitle>
<Radio className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{connectedChannels}</div>
<p className="text-xs text-muted-foreground">
{t('connectedOf', { connected: connectedChannels, total: channels.length })}
</p>
</CardContent>
</Card>
{/* Skills */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('skills')}</CardTitle>
<Puzzle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{enabledSkills}</div>
<p className="text-xs text-muted-foreground">
{t('enabledOf', { enabled: enabledSkills, total: skills.length })}
</p>
</CardContent>
</Card>
{/* Uptime */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{t('uptime')}</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{uptime > 0 ? formatUptime(uptime) : '—'}
</div>
<p className="text-xs text-muted-foreground">
{gatewayStatus.state === 'running' ? t('sinceRestart') : t('gatewayNotRunning')}
</p>
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>{t('quickActions.title')}</CardTitle>
<CardDescription>{t('quickActions.description')}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
<Button variant="outline" className="h-auto flex-col gap-2 py-4" asChild>
<Link to="/settings" onClick={() => trackUiEvent('dashboard.quick_action', { action: 'add_provider' })}>
<Wrench className="h-5 w-5" />
<span>{t('quickActions.addProvider')}</span>
</Link>
</Button>
<Button variant="outline" className="h-auto flex-col gap-2 py-4" asChild>
<Link to="/channels" onClick={() => trackUiEvent('dashboard.quick_action', { action: 'add_channel' })}>
<Plus className="h-5 w-5" />
<span>{t('quickActions.addChannel')}</span>
</Link>
</Button>
<Button variant="outline" className="h-auto flex-col gap-2 py-4" asChild>
<Link to="/cron" onClick={() => trackUiEvent('dashboard.quick_action', { action: 'create_cron' })}>
<Clock className="h-5 w-5" />
<span>{t('quickActions.createCron')}</span>
</Link>
</Button>
<Button variant="outline" className="h-auto flex-col gap-2 py-4" asChild>
<Link to="/skills" onClick={() => trackUiEvent('dashboard.quick_action', { action: 'install_skill' })}>
<Puzzle className="h-5 w-5" />
<span>{t('quickActions.installSkill')}</span>
</Link>
</Button>
<Button variant="outline" className="h-auto flex-col gap-2 py-4" asChild>
<Link to="/" onClick={() => trackUiEvent('dashboard.quick_action', { action: 'open_chat' })}>
<MessageSquare className="h-5 w-5" />
<span>{t('quickActions.openChat')}</span>
</Link>
</Button>
<Button variant="outline" className="h-auto flex-col gap-2 py-4" asChild>
<Link to="/settings" onClick={() => trackUiEvent('dashboard.quick_action', { action: 'open_settings' })}>
<Settings className="h-5 w-5" />
<span>{t('quickActions.settings')}</span>
</Link>
</Button>
{devModeUnlocked && (
<Button
variant="outline"
className="h-auto flex-col gap-2 py-4"
onClick={openDevConsole}
>
<Terminal className="h-5 w-5" />
<span>{t('quickActions.devConsole')}</span>
</Button>
)}
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* Connected Channels */}
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('connectedChannels')}</CardTitle>
</CardHeader>
<CardContent>
{(!Array.isArray(channels) || channels.length === 0) ? (
<FeedbackState
state="empty"
title={t('noChannels')}
action={(
<Button variant="link" asChild className="mt-2">
<Link to="/channels">{t('addFirst')}</Link>
</Button>
)}
/>
) : (
<div className="space-y-3">
{channels.slice(0, 5).map((channel) => (
<div
key={channel.id}
className="flex items-center justify-between rounded-lg border p-3"
>
<div className="flex items-center gap-3">
<span className="text-lg">
{channel.type === 'whatsapp' && '📱'}
{channel.type === 'telegram' && '✈️'}
{channel.type === 'discord' && '🎮'}
</span>
<div>
<p className="font-medium">{channel.name}</p>
<p className="text-xs text-muted-foreground capitalize">
{channel.type}
</p>
</div>
</div>
<StatusBadge status={channel.status} />
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Enabled Skills */}
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('activeSkills')}</CardTitle>
</CardHeader>
<CardContent>
{(!Array.isArray(skills) || skills.filter((s) => s.enabled).length === 0) ? (
<FeedbackState
state="empty"
title={t('noSkills')}
action={(
<Button variant="link" asChild className="mt-2">
<Link to="/skills">{t('enableSome')}</Link>
</Button>
)}
/>
) : (
<div className="flex flex-wrap gap-2">
{skills
.filter((s) => s.enabled)
.slice(0, 12)
.map((skill) => (
<Badge key={skill.id} variant="secondary">
{skill.icon && <span className="mr-1">{skill.icon}</span>}
{skill.name}
</Badge>
))}
{skills.filter((s) => s.enabled).length > 12 && (
<Badge variant="outline">
{t('more', { count: skills.filter((s) => s.enabled).length - 12 })}
</Badge>
)}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
/**
* Format uptime in human-readable format
*/
function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h`;
} else if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
}
export default Dashboard;

File diff suppressed because it is too large Load Diff