Compare commits

...

5 Commits

Author SHA1 Message Date
argenis de la rosa
8144938ac5 feat(web): electric blue dashboard restyle with animations and logo
Restyle the entire web dashboard with an electric blue theme featuring
glassmorphism cards, smooth animations, and the ZeroClaw logo. Remove
duplicate Vite dev server infrastructure to ensure a single gateway.

- Add electric blue color palette and glassmorphism styling system
- Add 10+ keyframe animations (fade, slide, pulse-glow, shimmer, float)
- Restyle all 10 pages with glass cards and electric components
- Add ZeroClaw logo to sidebar, pairing screen, and favicon
- Remove Vite dev/preview scripts and proxy config (build-only now)
- Update pairing dialog with ambient glow and animated elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:06:54 -04:00
argenis de la rosa
7e0570abd6 Merge remote-tracking branch 'origin/master' 2026-03-13 09:38:00 -04:00
argenis de la rosa
0931b140cf Merge origin/master into master
Made-with: Cursor
2026-03-13 09:08:46 -04:00
argenis de la rosa
e46a334f6d Merge remote-tracking branch 'refs/remotes/origin/master' 2026-03-11 17:05:46 -04:00
argenis de la rosa
76a6ab5b12 chore(github): update review ownership routing
Remove JordanTheJet from CODEOWNERS review routing and align the workflow review-policy docs with the current approver fallback.

This keeps protected paths owned by theonlyhennygod and SimianAstronaut7 without pulling in unrelated README edits.
2026-03-11 15:35:15 -04:00
19 changed files with 769 additions and 619 deletions

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" /> <meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/_app/logo.png" />
<title>ZeroClaw</title> <title>ZeroClaw</title>
</head> </head>
<body> <body>

View File

@ -5,9 +5,7 @@
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "build": "tsc -b && vite build"
"build": "tsc -b && vite build",
"preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",

BIN
web/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -47,11 +47,23 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
}; };
return ( return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center" style={{ background: 'radial-gradient(ellipse at center, #0a0a20 0%, #050510 70%)' }}>
<div className="bg-gray-900 rounded-xl p-8 w-full max-w-md border border-gray-800"> {/* Ambient glow */}
<div className="text-center mb-6"> <div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] rounded-full opacity-20 pointer-events-none" style={{ background: 'radial-gradient(circle, #0080ff 0%, transparent 70%)' }} />
<h1 className="text-2xl font-bold text-white mb-2">ZeroClaw</h1>
<p className="text-gray-400">Enter the pairing code from your terminal</p> <div className="relative glass-card p-8 w-full max-w-md animate-fade-in-scale">
{/* Top glow accent */}
<div className="absolute -top-px left-1/4 right-1/4 h-px" style={{ background: 'linear-gradient(90deg, transparent, #0080ff, transparent)' }} />
<div className="text-center mb-8">
<img
src="/_app/logo.png"
alt="ZeroClaw"
className="h-20 w-20 rounded-2xl object-cover mx-auto mb-4 animate-float"
style={{ boxShadow: '0 0 30px rgba(0,128,255,0.3)' }}
/>
<h1 className="text-2xl font-bold text-gradient-blue mb-2">ZeroClaw</h1>
<p className="text-[#556080] text-sm">Enter the pairing code from your terminal</p>
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<input <input
@ -59,19 +71,24 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
value={code} value={code}
onChange={(e) => setCode(e.target.value)} onChange={(e) => setCode(e.target.value)}
placeholder="6-digit code" placeholder="6-digit code"
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-center text-2xl tracking-widest focus:outline-none focus:border-blue-500 mb-4" className="input-electric w-full px-4 py-4 text-center text-2xl tracking-[0.3em] font-medium mb-4"
maxLength={6} maxLength={6}
autoFocus autoFocus
/> />
{error && ( {error && (
<p className="text-red-400 text-sm mb-4 text-center">{error}</p> <p className="text-[#ff4466] text-sm mb-4 text-center animate-fade-in">{error}</p>
)} )}
<button <button
type="submit" type="submit"
disabled={loading || code.length < 6} disabled={loading || code.length < 6}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded-lg font-medium transition-colors" className="btn-electric w-full py-3.5 text-sm font-semibold tracking-wide"
> >
{loading ? 'Pairing...' : 'Pair'} {loading ? (
<span className="flex items-center justify-center gap-2">
<span className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Pairing...
</span>
) : 'Pair'}
</button> </button>
</form> </form>
</div> </div>
@ -99,8 +116,11 @@ function AppContent() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center" style={{ background: 'radial-gradient(ellipse at center, #0a0a20 0%, #050510 70%)' }}>
<p className="text-gray-400">Connecting...</p> <div className="flex flex-col items-center gap-4 animate-fade-in">
<div className="h-10 w-10 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
<p className="text-[#556080] text-sm">Connecting...</p>
</div>
</div> </div>
); );
} }

View File

@ -30,17 +30,17 @@ export default function Header() {
}; };
return ( return (
<header className="h-14 bg-gray-800 border-b border-gray-700 flex items-center justify-between px-6"> <header className="h-14 flex items-center justify-between px-6 border-b border-[#1a1a3e]/40 animate-fade-in" style={{ background: 'linear-gradient(90deg, rgba(8,8,24,0.9), rgba(5,5,16,0.9))', backdropFilter: 'blur(12px)' }}>
{/* Page title */} {/* Page title */}
<h1 className="text-lg font-semibold text-white">{pageTitle}</h1> <h1 className="text-lg font-semibold text-white tracking-tight">{pageTitle}</h1>
{/* Right-side controls */} {/* Right-side controls */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
{/* Language switcher */} {/* Language switcher */}
<button <button
type="button" type="button"
onClick={toggleLanguage} onClick={toggleLanguage}
className="px-3 py-1 rounded-md text-sm font-medium border border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors" className="px-3 py-1 rounded-lg text-xs font-semibold border border-[#1a1a3e] text-[#8892a8] hover:text-white hover:border-[#0080ff40] hover:bg-[#0080ff10] transition-all duration-300"
> >
{locale === 'en' ? 'EN' : 'TR'} {locale === 'en' ? 'EN' : 'TR'}
</button> </button>
@ -49,9 +49,9 @@ export default function Header() {
<button <button
type="button" type="button"
onClick={logout} onClick={logout}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm text-gray-300 hover:bg-gray-700 hover:text-white transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs text-[#8892a8] hover:text-[#ff4466] hover:bg-[#ff446610] transition-all duration-300"
> >
<LogOut className="h-4 w-4" /> <LogOut className="h-3.5 w-3.5" />
<span>{t('auth.logout')}</span> <span>{t('auth.logout')}</span>
</button> </button>
</div> </div>

View File

@ -4,7 +4,7 @@ import Header from '@/components/layout/Header';
export default function Layout() { export default function Layout() {
return ( return (
<div className="min-h-screen bg-gray-950 text-white"> <div className="min-h-screen text-white" style={{ background: 'linear-gradient(135deg, #050510 0%, #080818 50%, #050510 100%)' }}>
{/* Fixed sidebar */} {/* Fixed sidebar */}
<Sidebar /> <Sidebar />

View File

@ -28,38 +28,59 @@ const navItems = [
export default function Sidebar() { export default function Sidebar() {
return ( return (
<aside className="fixed top-0 left-0 h-screen w-60 bg-gray-900 flex flex-col border-r border-gray-800"> <aside className="fixed top-0 left-0 h-screen w-60 flex flex-col" style={{ background: 'linear-gradient(180deg, #080818 0%, #050510 100%)' }}>
{/* Glow line on right edge */}
<div className="sidebar-glow-line" />
{/* Logo / Title */} {/* Logo / Title */}
<div className="flex items-center gap-2 px-5 py-5 border-b border-gray-800"> <div className="flex items-center gap-3 px-4 py-4 border-b border-[#1a1a3e]/50">
<div className="h-8 w-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold text-sm"> <img
ZC src="/_app/logo.png"
</div> alt="ZeroClaw"
<span className="text-lg font-semibold text-white tracking-wide"> className="h-10 w-10 rounded-xl object-cover animate-pulse-glow"
/>
<span className="text-lg font-bold text-gradient-blue tracking-wide">
ZeroClaw ZeroClaw
</span> </span>
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-1"> <nav className="flex-1 overflow-y-auto py-4 px-3 space-y-1">
{navItems.map(({ to, icon: Icon, labelKey }) => ( {navItems.map(({ to, icon: Icon, labelKey }, idx) => (
<NavLink <NavLink
key={to} key={to}
to={to} to={to}
end={to === '/'} end={to === '/'}
className={({ isActive }) => className={({ isActive }) =>
[ [
'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors', 'flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-300 animate-slide-in-left group',
isActive isActive
? 'bg-blue-600 text-white' ? 'text-white shadow-[0_0_15px_rgba(0,128,255,0.2)]'
: 'text-gray-300 hover:bg-gray-800 hover:text-white', : 'text-[#556080] hover:text-white hover:bg-[#0080ff08]',
].join(' ') ].join(' ')
} }
style={({ isActive }) => ({
animationDelay: `${idx * 40}ms`,
...(isActive ? { background: 'linear-gradient(135deg, rgba(0,128,255,0.15), rgba(0,128,255,0.05))' } : {}),
})}
> >
<Icon className="h-5 w-5 flex-shrink-0" /> {({ isActive }) => (
<span>{t(labelKey)}</span> <>
<Icon className={`h-5 w-5 flex-shrink-0 transition-colors duration-300 ${isActive ? 'text-[#0080ff]' : 'group-hover:text-[#0080ff80]'}`} />
<span>{t(labelKey)}</span>
{isActive && (
<div className="ml-auto h-1.5 w-1.5 rounded-full bg-[#0080ff] glow-dot" />
)}
</>
)}
</NavLink> </NavLink>
))} ))}
</nav> </nav>
{/* Footer */}
<div className="px-5 py-4 border-t border-[#1a1a3e]/50">
<p className="text-[10px] text-[#334060] tracking-wider uppercase">ZeroClaw Runtime</p>
</div>
</aside> </aside>
); );
} }

View File

@ -1,33 +1,37 @@
@import "tailwindcss"; @import "tailwindcss";
/* /*
* ZeroClaw Dark Theme * ZeroClaw Electric Blue Theme
* Dark-mode by default with gray cards and blue/green accents. * Dark-mode with electric blue accents, glassmorphism, and animations.
*/ */
@theme { @theme {
--color-bg-primary: #0a0a0f; --color-bg-primary: #050510;
--color-bg-secondary: #12121a; --color-bg-secondary: #0a0a1a;
--color-bg-card: #1a1a2e; --color-bg-card: #0d0d20;
--color-bg-card-hover: #22223a; --color-bg-card-hover: #141430;
--color-bg-input: #14141f; --color-bg-input: #0a0a18;
--color-border-default: #2a2a3e; --color-border-default: #1a1a3e;
--color-border-subtle: #1e1e30; --color-border-subtle: #12122a;
--color-accent-blue: #3b82f6; --color-accent-blue: #0080ff;
--color-accent-blue-hover: #2563eb; --color-accent-blue-hover: #0066cc;
--color-accent-green: #10b981; --color-accent-cyan: #00d4ff;
--color-accent-green-hover: #059669; --color-accent-green: #00e68a;
--color-accent-green-hover: #00cc7a;
--color-text-primary: #e2e8f0; --color-text-primary: #e8edf5;
--color-text-secondary: #94a3b8; --color-text-secondary: #8892a8;
--color-text-muted: #64748b; --color-text-muted: #556080;
--color-status-success: #10b981; --color-status-success: #00e68a;
--color-status-warning: #f59e0b; --color-status-warning: #ffaa00;
--color-status-error: #ef4444; --color-status-error: #ff4466;
--color-status-info: #3b82f6; --color-status-info: #0080ff;
--color-glow-blue: #0080ff40;
--color-glow-cyan: #00d4ff30;
} }
/* Base styles */ /* Base styles */
@ -54,32 +58,35 @@ body {
/* Scrollbar styling */ /* Scrollbar styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 6px;
height: 8px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--color-bg-secondary); background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--color-border-default); background: #1a1a3e;
border-radius: 4px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted); background: #0080ff60;
} }
/* Card utility */ /* Card utility */
.card { .card {
background-color: var(--color-bg-card); background: linear-gradient(135deg, rgba(13, 13, 32, 0.8), rgba(10, 10, 26, 0.6));
border: 1px solid var(--color-border-default); border: 1px solid rgba(0, 128, 255, 0.1);
border-radius: 0.75rem; border-radius: 1rem;
backdrop-filter: blur(12px);
transition: all 0.3s ease;
} }
.card:hover { .card:hover {
background-color: var(--color-bg-card-hover); border-color: rgba(0, 128, 255, 0.25);
box-shadow: 0 0 20px rgba(0, 128, 255, 0.08);
} }
/* Focus ring utility */ /* Focus ring utility */
@ -87,3 +94,236 @@ body {
outline: 2px solid var(--color-accent-blue); outline: 2px solid var(--color-accent-blue);
outline-offset: 2px; outline-offset: 2px;
} }
/* ========== ANIMATIONS ========== */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInScale {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-16px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInRight {
from { opacity: 0; transform: translateX(16px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 8px rgba(0, 128, 255, 0.3); }
50% { box-shadow: 0 0 20px rgba(0, 128, 255, 0.6); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-4px); }
}
@keyframes borderGlow {
0%, 100% { border-color: rgba(0, 128, 255, 0.15); }
50% { border-color: rgba(0, 128, 255, 0.35); }
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Animation utility classes */
.animate-fade-in {
animation: fadeIn 0.4s ease-out both;
}
.animate-fade-in-scale {
animation: fadeInScale 0.3s ease-out both;
}
.animate-slide-in-left {
animation: slideInLeft 0.4s ease-out both;
}
.animate-slide-in-right {
animation: slideInRight 0.4s ease-out both;
}
.animate-slide-in-up {
animation: slideInUp 0.4s ease-out both;
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
.animate-border-glow {
animation: borderGlow 3s ease-in-out infinite;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
/* Stagger delays for grid children */
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
.stagger-children > *:nth-child(2) { animation-delay: 60ms; }
.stagger-children > *:nth-child(3) { animation-delay: 120ms; }
.stagger-children > *:nth-child(4) { animation-delay: 180ms; }
.stagger-children > *:nth-child(5) { animation-delay: 240ms; }
.stagger-children > *:nth-child(6) { animation-delay: 300ms; }
.stagger-children > *:nth-child(7) { animation-delay: 360ms; }
.stagger-children > *:nth-child(8) { animation-delay: 420ms; }
.stagger-children > *:nth-child(9) { animation-delay: 480ms; }
.stagger-children > *:nth-child(10) { animation-delay: 540ms; }
/* Glass card */
.glass-card {
background: linear-gradient(135deg, rgba(13, 13, 32, 0.7), rgba(5, 5, 16, 0.5));
border: 1px solid rgba(0, 128, 255, 0.12);
border-radius: 1rem;
backdrop-filter: blur(16px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
border-color: rgba(0, 128, 255, 0.3);
box-shadow: 0 4px 30px rgba(0, 128, 255, 0.1), 0 0 0 1px rgba(0, 128, 255, 0.05);
transform: translateY(-1px);
}
/* Electric button */
.btn-electric {
background: linear-gradient(135deg, #0080ff, #0066cc);
color: white;
border: none;
border-radius: 0.75rem;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-electric:hover:not(:disabled) {
background: linear-gradient(135deg, #0090ff, #0070dd);
box-shadow: 0 0 20px rgba(0, 128, 255, 0.4);
transform: translateY(-1px);
}
.btn-electric:active:not(:disabled) {
transform: translateY(0);
}
.btn-electric:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Gradient text */
.text-gradient-blue {
background: linear-gradient(135deg, #0080ff, #00d4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Glow dot */
.glow-dot {
box-shadow: 0 0 6px currentColor;
}
/* Electric input */
.input-electric {
background: rgba(10, 10, 26, 0.8);
border: 1px solid rgba(0, 128, 255, 0.15);
border-radius: 0.75rem;
color: var(--color-text-primary);
transition: all 0.3s ease;
}
.input-electric:focus {
outline: none;
border-color: rgba(0, 128, 255, 0.5);
box-shadow: 0 0 0 3px rgba(0, 128, 255, 0.15), 0 0 20px rgba(0, 128, 255, 0.1);
}
.input-electric::placeholder {
color: var(--color-text-muted);
}
/* Progress bar animation */
.progress-bar-animated {
position: relative;
overflow: hidden;
}
.progress-bar-animated::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
/* Table styling */
.table-electric {
width: 100%;
}
.table-electric thead tr {
border-bottom: 1px solid rgba(0, 128, 255, 0.1);
}
.table-electric thead th {
color: var(--color-text-muted);
font-weight: 500;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.75rem 1rem;
}
.table-electric tbody tr {
border-bottom: 1px solid rgba(26, 26, 62, 0.5);
transition: all 0.2s ease;
}
.table-electric tbody tr:hover {
background: rgba(0, 128, 255, 0.04);
}
/* Modal backdrop */
.modal-backdrop {
background: rgba(5, 5, 16, 0.8);
backdrop-filter: blur(8px);
}
/* Sidebar glow line */
.sidebar-glow-line {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 1px;
background: linear-gradient(180deg, transparent, rgba(0, 128, 255, 0.3), transparent);
}

View File

@ -171,7 +171,7 @@ export default function AgentChat() {
<div className="flex flex-col h-[calc(100vh-3.5rem)]"> <div className="flex flex-col h-[calc(100vh-3.5rem)]">
{/* Connection status bar */} {/* Connection status bar */}
{error && ( {error && (
<div className="px-4 py-2 bg-red-900/30 border-b border-red-700 flex items-center gap-2 text-sm text-red-300"> <div className="px-4 py-2 bg-[#ff446615] border-b border-[#ff446630] flex items-center gap-2 text-sm text-[#ff6680] animate-fade-in">
<AlertCircle className="h-4 w-4 flex-shrink-0" /> <AlertCircle className="h-4 w-4 flex-shrink-0" />
{error} {error}
</div> </div>
@ -180,45 +180,58 @@ export default function AgentChat() {
{/* Messages area */} {/* Messages area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4"> <div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && ( {messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-gray-500"> <div className="flex flex-col items-center justify-center h-full text-[#334060] animate-fade-in">
<Bot className="h-12 w-12 mb-3 text-gray-600" /> <div className="h-16 w-16 rounded-2xl flex items-center justify-center mb-4 animate-float" style={{ background: 'linear-gradient(135deg, #0080ff15, #0080ff08)' }}>
<p className="text-lg font-medium">ZeroClaw Agent</p> <Bot className="h-8 w-8 text-[#0080ff]" />
<p className="text-sm mt-1">Send a message to start the conversation</p> </div>
<p className="text-lg font-semibold text-white mb-1">ZeroClaw Agent</p>
<p className="text-sm text-[#556080]">Send a message to start the conversation</p>
</div> </div>
)} )}
{messages.map((msg) => ( {messages.map((msg, idx) => (
<div <div
key={msg.id} key={msg.id}
className={`group flex items-start gap-3 ${ className={`group flex items-start gap-3 ${
msg.role === 'user' ? 'flex-row-reverse' : '' msg.role === 'user' ? 'flex-row-reverse animate-slide-in-right' : 'animate-slide-in-left'
}`} }`}
style={{ animationDelay: `${Math.min(idx * 30, 200)}ms` }}
> >
<div <div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ className={`flex-shrink-0 w-8 h-8 rounded-xl flex items-center justify-center ${
msg.role === 'user' msg.role === 'user'
? 'bg-blue-600' ? ''
: 'bg-gray-700' : ''
}`} }`}
style={{
background: msg.role === 'user'
? 'linear-gradient(135deg, #0080ff, #0060cc)'
: 'linear-gradient(135deg, #1a1a3e, #12122a)'
}}
> >
{msg.role === 'user' ? ( {msg.role === 'user' ? (
<User className="h-4 w-4 text-white" /> <User className="h-4 w-4 text-white" />
) : ( ) : (
<Bot className="h-4 w-4 text-white" /> <Bot className="h-4 w-4 text-[#0080ff]" />
)} )}
</div> </div>
<div className="relative max-w-[75%]"> <div className="relative max-w-[75%]">
<div <div
className={`rounded-xl px-4 py-3 ${ className={`rounded-2xl px-4 py-3 ${
msg.role === 'user' msg.role === 'user'
? 'bg-blue-600 text-white' ? 'text-white'
: 'bg-gray-800 text-gray-100 border border-gray-700' : 'text-[#e8edf5] border border-[#1a1a3e]'
}`} }`}
style={{
background: msg.role === 'user'
? 'linear-gradient(135deg, #0080ff, #0066cc)'
: 'linear-gradient(135deg, rgba(13,13,32,0.8), rgba(10,10,26,0.6))'
}}
> >
<p className="text-sm whitespace-pre-wrap break-words">{msg.content}</p> <p className="text-sm whitespace-pre-wrap break-words">{msg.content}</p>
<p <p
className={`text-xs mt-1 ${ className={`text-[10px] mt-1.5 ${
msg.role === 'user' ? 'text-blue-200' : 'text-gray-500' msg.role === 'user' ? 'text-white/50' : 'text-[#334060]'
}`} }`}
> >
{msg.timestamp.toLocaleTimeString()} {msg.timestamp.toLocaleTimeString()}
@ -227,12 +240,12 @@ export default function AgentChat() {
<button <button
onClick={() => handleCopy(msg.id, msg.content)} onClick={() => handleCopy(msg.id, msg.content)}
aria-label="Copy message" aria-label="Copy message"
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-400 hover:text-white" className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-all duration-300 p-1.5 rounded-lg bg-[#0a0a18] border border-[#1a1a3e] text-[#556080] hover:text-white hover:border-[#0080ff40]"
> >
{copiedId === msg.id ? ( {copiedId === msg.id ? (
<Check className="h-3.5 w-3.5 text-green-400" /> <Check className="h-3 w-3 text-[#00e68a]" />
) : ( ) : (
<Copy className="h-3.5 w-3.5" /> <Copy className="h-3 w-3" />
)} )}
</button> </button>
</div> </div>
@ -240,17 +253,16 @@ export default function AgentChat() {
))} ))}
{typing && ( {typing && (
<div className="flex items-start gap-3"> <div className="flex items-start gap-3 animate-fade-in">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center"> <div className="flex-shrink-0 w-8 h-8 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #1a1a3e, #12122a)' }}>
<Bot className="h-4 w-4 text-white" /> <Bot className="h-4 w-4 text-[#0080ff]" />
</div> </div>
<div className="bg-gray-800 border border-gray-700 rounded-xl px-4 py-3"> <div className="rounded-2xl px-4 py-3 border border-[#1a1a3e]" style={{ background: 'linear-gradient(135deg, rgba(13,13,32,0.8), rgba(10,10,26,0.6))' }}>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5">
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} /> <span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} /> <span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} /> <span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div> </div>
<p className="text-xs text-gray-500 mt-1">Typing...</p>
</div> </div>
</div> </div>
)} )}
@ -259,9 +271,9 @@ export default function AgentChat() {
</div> </div>
{/* Input area */} {/* Input area */}
<div className="border-t border-gray-800 bg-gray-900 p-4"> <div className="border-t border-[#1a1a3e]/40 p-4" style={{ background: 'linear-gradient(180deg, rgba(8,8,24,0.9), rgba(5,5,16,0.95))' }}>
<div className="flex items-end gap-3 max-w-4xl mx-auto"> <div className="flex items-end gap-3 max-w-4xl mx-auto">
<div className="flex-1 relative"> <div className="flex-1">
<textarea <textarea
ref={inputRef} ref={inputRef}
rows={1} rows={1}
@ -270,25 +282,25 @@ export default function AgentChat() {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={connected ? 'Type a message...' : 'Connecting...'} placeholder={connected ? 'Type a message...' : 'Connecting...'}
disabled={!connected} disabled={!connected}
className="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 resize-none overflow-y-auto" className="input-electric w-full px-4 py-3 text-sm resize-none overflow-y-auto disabled:opacity-40"
style={{ minHeight: '44px', maxHeight: '200px' }} style={{ minHeight: '44px', maxHeight: '200px' }}
/> />
</div> </div>
<button <button
onClick={handleSend} onClick={handleSend}
disabled={!connected || !input.trim()} disabled={!connected || !input.trim()}
className="flex-shrink-0 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded-xl p-3 transition-colors" className="btn-electric flex-shrink-0 p-3 rounded-xl"
> >
<Send className="h-5 w-5" /> <Send className="h-5 w-5" />
</button> </button>
</div> </div>
<div className="flex items-center justify-center mt-2 gap-2"> <div className="flex items-center justify-center mt-2 gap-2">
<span <span
className={`inline-block h-2 w-2 rounded-full ${ className={`inline-block h-1.5 w-1.5 rounded-full glow-dot ${
connected ? 'bg-green-500' : 'bg-red-500' connected ? 'text-[#00e68a] bg-[#00e68a]' : 'text-[#ff4466] bg-[#ff4466]'
}`} }`}
/> />
<span className="text-xs text-gray-500"> <span className="text-[10px] text-[#334060]">
{connected ? 'Connected' : 'Disconnected'} {connected ? 'Connected' : 'Disconnected'}
</span> </span>
</div> </div>

View File

@ -18,7 +18,6 @@ export default function Config() {
useEffect(() => { useEffect(() => {
getConfig() getConfig()
.then((data) => { .then((data) => {
// The API may return either a raw string or a JSON string
setConfig(typeof data === 'string' ? data : JSON.stringify(data, null, 2)); setConfig(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
}) })
.catch((err) => setError(err.message)) .catch((err) => setError(err.message))
@ -49,23 +48,23 @@ export default function Config() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-blue-400" /> <Settings className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white">Configuration</h2> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Configuration</h2>
</div> </div>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors disabled:opacity-50" className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
> >
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
{saving ? 'Saving...' : 'Save'} {saving ? 'Saving...' : 'Save'}
@ -73,13 +72,13 @@ export default function Config() {
</div> </div>
{/* Sensitive fields note */} {/* Sensitive fields note */}
<div className="flex items-start gap-3 bg-yellow-900/20 border border-yellow-700/40 rounded-lg p-4"> <div className="flex items-start gap-3 rounded-xl p-4 border border-[#ffaa0020]" style={{ background: 'rgba(255,170,0,0.05)' }}>
<ShieldAlert className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" /> <ShieldAlert className="h-5 w-5 text-[#ffaa00] flex-shrink-0 mt-0.5" />
<div> <div>
<p className="text-sm text-yellow-300 font-medium"> <p className="text-sm text-[#ffaa00] font-medium">
Sensitive fields are masked Sensitive fields are masked
</p> </p>
<p className="text-sm text-yellow-400/70 mt-0.5"> <p className="text-sm text-[#ffaa0080] mt-0.5">
API keys, tokens, and passwords are hidden for security. To update a API keys, tokens, and passwords are hidden for security. To update a
masked field, replace the entire masked value with your new value. masked field, replace the entire masked value with your new value.
</p> </p>
@ -88,27 +87,27 @@ export default function Config() {
{/* Success message */} {/* Success message */}
{success && ( {success && (
<div className="flex items-center gap-2 bg-green-900/30 border border-green-700 rounded-lg p-3"> <div className="flex items-center gap-2 rounded-xl p-3 border border-[#00e68a30] animate-fade-in" style={{ background: 'rgba(0,230,138,0.06)' }}>
<CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" /> <CheckCircle className="h-4 w-4 text-[#00e68a] flex-shrink-0" />
<span className="text-sm text-green-300">{success}</span> <span className="text-sm text-[#00e68a]">{success}</span>
</div> </div>
)} )}
{/* Error message */} {/* Error message */}
{error && ( {error && (
<div className="flex items-center gap-2 bg-red-900/30 border border-red-700 rounded-lg p-3"> <div className="flex items-center gap-2 rounded-xl p-3 border border-[#ff446630] animate-fade-in" style={{ background: 'rgba(255,68,102,0.06)' }}>
<AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0" /> <AlertTriangle className="h-4 w-4 text-[#ff4466] flex-shrink-0" />
<span className="text-sm text-red-300">{error}</span> <span className="text-sm text-[#ff6680]">{error}</span>
</div> </div>
)} )}
{/* Config Editor */} {/* Config Editor */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"> <div className="glass-card overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-800/50"> <div className="flex items-center justify-between px-4 py-2.5 border-b border-[#1a1a3e]" style={{ background: 'rgba(0,128,255,0.03)' }}>
<span className="text-xs text-gray-400 font-medium uppercase tracking-wider"> <span className="text-[10px] text-[#334060] font-semibold uppercase tracking-wider">
TOML Configuration TOML Configuration
</span> </span>
<span className="text-xs text-gray-500"> <span className="text-[10px] text-[#334060]">
{config.split('\n').length} lines {config.split('\n').length} lines
</span> </span>
</div> </div>
@ -116,8 +115,8 @@ export default function Config() {
value={config} value={config}
onChange={(e) => setConfig(e.target.value)} onChange={(e) => setConfig(e.target.value)}
spellCheck={false} spellCheck={false}
className="w-full min-h-[500px] bg-gray-950 text-gray-200 font-mono text-sm p-4 resize-y focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset" className="w-full min-h-[500px] text-[#8892a8] font-mono text-sm p-4 resize-y focus:outline-none focus:ring-2 focus:ring-[#0080ff40] focus:ring-inset"
style={{ tabSize: 4 }} style={{ background: 'rgba(5,5,16,0.8)', tabSize: 4 }}
/> />
</div> </div>
</div> </div>

View File

@ -26,8 +26,8 @@ export default function Cost() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6 animate-fade-in">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load cost data: {error} Failed to load cost data: {error}
</div> </div>
</div> </div>
@ -37,7 +37,7 @@ export default function Cost() {
if (loading || !cost) { if (loading || !cost) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
); );
} }
@ -45,120 +45,67 @@ export default function Cost() {
const models = Object.values(cost.by_model); const models = Object.values(cost.by_model);
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Summary Cards */} {/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800"> {[
<div className="flex items-center gap-3 mb-3"> { icon: DollarSign, color: '#0080ff', bg: '#0080ff15', label: 'Session Cost', value: formatUSD(cost.session_cost_usd) },
<div className="p-2 bg-blue-600/20 rounded-lg"> { icon: TrendingUp, color: '#00e68a', bg: '#00e68a15', label: 'Daily Cost', value: formatUSD(cost.daily_cost_usd) },
<DollarSign className="h-5 w-5 text-blue-400" /> { icon: Layers, color: '#a855f7', bg: '#a855f715', label: 'Monthly Cost', value: formatUSD(cost.monthly_cost_usd) },
{ icon: Hash, color: '#ff8800', bg: '#ff880015', label: 'Total Requests', value: cost.request_count.toLocaleString() },
].map(({ icon: Icon, color, bg, label, value }) => (
<div key={label} className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-xl" style={{ background: bg }}>
<Icon className="h-5 w-5" style={{ color }} />
</div>
<span className="text-xs text-[#556080] uppercase tracking-wider font-medium">{label}</span>
</div> </div>
<span className="text-sm text-gray-400">Session Cost</span> <p className="text-2xl font-bold text-white font-mono">{value}</p>
</div> </div>
<p className="text-2xl font-bold text-white"> ))}
{formatUSD(cost.session_cost_usd)}
</p>
</div>
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-green-600/20 rounded-lg">
<TrendingUp className="h-5 w-5 text-green-400" />
</div>
<span className="text-sm text-gray-400">Daily Cost</span>
</div>
<p className="text-2xl font-bold text-white">
{formatUSD(cost.daily_cost_usd)}
</p>
</div>
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-purple-600/20 rounded-lg">
<Layers className="h-5 w-5 text-purple-400" />
</div>
<span className="text-sm text-gray-400">Monthly Cost</span>
</div>
<p className="text-2xl font-bold text-white">
{formatUSD(cost.monthly_cost_usd)}
</p>
</div>
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-orange-600/20 rounded-lg">
<Hash className="h-5 w-5 text-orange-400" />
</div>
<span className="text-sm text-gray-400">Total Requests</span>
</div>
<p className="text-2xl font-bold text-white">
{cost.request_count.toLocaleString()}
</p>
</div>
</div> </div>
{/* Token Statistics */} {/* Token Statistics */}
<div className="bg-gray-900 rounded-xl border border-gray-800 p-5"> <div className="glass-card p-5 animate-slide-in-up" style={{ animationDelay: '200ms' }}>
<h3 className="text-base font-semibold text-white mb-4"> <h3 className="text-sm font-semibold text-white mb-4 uppercase tracking-wider">
Token Statistics Token Statistics
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-gray-800/50 rounded-lg p-4"> {[
<p className="text-sm text-gray-400">Total Tokens</p> { label: 'Total Tokens', value: cost.total_tokens.toLocaleString() },
<p className="text-xl font-bold text-white mt-1"> { label: 'Avg Tokens / Request', value: cost.request_count > 0 ? Math.round(cost.total_tokens / cost.request_count).toLocaleString() : '0' },
{cost.total_tokens.toLocaleString()} { label: 'Cost per 1K Tokens', value: cost.total_tokens > 0 ? formatUSD((cost.monthly_cost_usd / cost.total_tokens) * 1000) : '$0.0000' },
</p> ].map(({ label, value }) => (
</div> <div key={label} className="rounded-xl p-4" style={{ background: 'rgba(0,128,255,0.04)', border: '1px solid rgba(0,128,255,0.08)' }}>
<div className="bg-gray-800/50 rounded-lg p-4"> <p className="text-xs text-[#556080] uppercase tracking-wider">{label}</p>
<p className="text-sm text-gray-400">Avg Tokens / Request</p> <p className="text-xl font-bold text-white mt-1 font-mono">{value}</p>
<p className="text-xl font-bold text-white mt-1"> </div>
{cost.request_count > 0 ))}
? Math.round(cost.total_tokens / cost.request_count).toLocaleString()
: '0'}
</p>
</div>
<div className="bg-gray-800/50 rounded-lg p-4">
<p className="text-sm text-gray-400">Cost per 1K Tokens</p>
<p className="text-xl font-bold text-white mt-1">
{cost.total_tokens > 0
? formatUSD((cost.monthly_cost_usd / cost.total_tokens) * 1000)
: '$0.0000'}
</p>
</div>
</div> </div>
</div> </div>
{/* Model Breakdown Table */} {/* Model Breakdown Table */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"> <div className="glass-card overflow-hidden animate-slide-in-up" style={{ animationDelay: '300ms' }}>
<div className="px-5 py-4 border-b border-gray-800"> <div className="px-5 py-4 border-b border-[#1a1a3e]">
<h3 className="text-base font-semibold text-white"> <h3 className="text-sm font-semibold text-white uppercase tracking-wider">
Model Breakdown Model Breakdown
</h3> </h3>
</div> </div>
{models.length === 0 ? ( {models.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="p-8 text-center text-[#334060]">
No model data available. No model data available.
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <table className="table-electric">
<thead> <thead>
<tr className="border-b border-gray-800"> <tr>
<th className="text-left px-5 py-3 text-gray-400 font-medium"> <th className="text-left">Model</th>
Model <th className="text-right">Cost</th>
</th> <th className="text-right">Tokens</th>
<th className="text-right px-5 py-3 text-gray-400 font-medium"> <th className="text-right">Requests</th>
Cost <th className="text-left">Share</th>
</th>
<th className="text-right px-5 py-3 text-gray-400 font-medium">
Tokens
</th>
<th className="text-right px-5 py-3 text-gray-400 font-medium">
Requests
</th>
<th className="text-left px-5 py-3 text-gray-400 font-medium">
Share
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -170,31 +117,28 @@ export default function Cost() {
? (m.cost_usd / cost.monthly_cost_usd) * 100 ? (m.cost_usd / cost.monthly_cost_usd) * 100
: 0; : 0;
return ( return (
<tr <tr key={m.model}>
key={m.model} <td className="px-5 py-3 text-white font-medium text-sm">
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="px-5 py-3 text-white font-medium">
{m.model} {m.model}
</td> </td>
<td className="px-5 py-3 text-gray-300 text-right font-mono"> <td className="px-5 py-3 text-[#8892a8] text-right font-mono text-sm">
{formatUSD(m.cost_usd)} {formatUSD(m.cost_usd)}
</td> </td>
<td className="px-5 py-3 text-gray-300 text-right"> <td className="px-5 py-3 text-[#8892a8] text-right text-sm">
{m.total_tokens.toLocaleString()} {m.total_tokens.toLocaleString()}
</td> </td>
<td className="px-5 py-3 text-gray-300 text-right"> <td className="px-5 py-3 text-[#8892a8] text-right text-sm">
{m.request_count.toLocaleString()} {m.request_count.toLocaleString()}
</td> </td>
<td className="px-5 py-3"> <td className="px-5 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-800 rounded-full overflow-hidden"> <div className="w-20 h-1.5 bg-[#0a0a18] rounded-full overflow-hidden">
<div <div
className="h-full bg-blue-500 rounded-full" className="h-full rounded-full progress-bar-animated transition-all duration-700"
style={{ width: `${Math.max(share, 2)}%` }} style={{ width: `${Math.max(share, 2)}%`, background: '#0080ff' }}
/> />
</div> </div>
<span className="text-xs text-gray-400 w-10 text-right"> <span className="text-xs text-[#556080] w-10 text-right font-mono">
{share.toFixed(1)}% {share.toFixed(1)}%
</span> </span>
</div> </div>

View File

@ -84,19 +84,19 @@ export default function Cron() {
switch (status.toLowerCase()) { switch (status.toLowerCase()) {
case 'ok': case 'ok':
case 'success': case 'success':
return <CheckCircle className="h-4 w-4 text-green-400" />; return <CheckCircle className="h-4 w-4 text-[#00e68a]" />;
case 'error': case 'error':
case 'failed': case 'failed':
return <XCircle className="h-4 w-4 text-red-400" />; return <XCircle className="h-4 w-4 text-[#ff4466]" />;
default: default:
return <AlertCircle className="h-4 w-4 text-yellow-400" />; return <AlertCircle className="h-4 w-4 text-[#ffaa00]" />;
} }
}; };
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6 animate-fade-in">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load cron jobs: {error} Failed to load cron jobs: {error}
</div> </div>
</div> </div>
@ -106,24 +106,24 @@ export default function Cron() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="h-5 w-5 text-blue-400" /> <Clock className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white"> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Scheduled Tasks ({jobs.length}) Scheduled Tasks ({jobs.length})
</h2> </h2>
</div> </div>
<button <button
onClick={() => setShowForm(true)} onClick={() => setShowForm(true)}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors" className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Add Job Add Job
@ -132,8 +132,8 @@ export default function Cron() {
{/* Add Job Form Modal */} {/* Add Job Form Modal */}
{showForm && ( {showForm && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"> <div className="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md mx-4"> <div className="glass-card p-6 w-full max-w-md mx-4 animate-fade-in-scale">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">Add Cron Job</h3> <h3 className="text-lg font-semibold text-white">Add Cron Job</h3>
<button <button
@ -141,21 +141,21 @@ export default function Cron() {
setShowForm(false); setShowForm(false);
setFormError(null); setFormError(null);
}} }}
className="text-gray-400 hover:text-white transition-colors" className="text-[#556080] hover:text-white transition-colors duration-300"
> >
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
{formError && ( {formError && (
<div className="mb-4 rounded-lg bg-red-900/30 border border-red-700 p-3 text-sm text-red-300"> <div className="mb-4 rounded-xl bg-[#ff446615] border border-[#ff446630] p-3 text-sm text-[#ff6680] animate-fade-in">
{formError} {formError}
</div> </div>
)} )}
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-1"> <label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Name (optional) Name (optional)
</label> </label>
<input <input
@ -163,31 +163,31 @@ export default function Cron() {
value={formName} value={formName}
onChange={(e) => setFormName(e.target.value)} onChange={(e) => setFormName(e.target.value)}
placeholder="e.g. Daily cleanup" placeholder="e.g. Daily cleanup"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="input-electric w-full px-3 py-2.5 text-sm"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-1"> <label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Schedule <span className="text-red-400">*</span> Schedule <span className="text-[#ff4466]">*</span>
</label> </label>
<input <input
type="text" type="text"
value={formSchedule} value={formSchedule}
onChange={(e) => setFormSchedule(e.target.value)} onChange={(e) => setFormSchedule(e.target.value)}
placeholder="e.g. 0 0 * * * (cron expression)" placeholder="e.g. 0 0 * * * (cron expression)"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="input-electric w-full px-3 py-2.5 text-sm"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-1"> <label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Command <span className="text-red-400">*</span> Command <span className="text-[#ff4466]">*</span>
</label> </label>
<input <input
type="text" type="text"
value={formCommand} value={formCommand}
onChange={(e) => setFormCommand(e.target.value)} onChange={(e) => setFormCommand(e.target.value)}
placeholder="e.g. cleanup --older-than 7d" placeholder="e.g. cleanup --older-than 7d"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="input-electric w-full px-3 py-2.5 text-sm"
/> />
</div> </div>
</div> </div>
@ -198,14 +198,14 @@ export default function Cron() {
setShowForm(false); setShowForm(false);
setFormError(null); setFormError(null);
}} }}
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors" className="px-4 py-2 text-sm font-medium text-[#8892a8] hover:text-white border border-[#1a1a3e] rounded-xl hover:bg-[#0080ff08] transition-all duration-300"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleAdd} onClick={handleAdd}
disabled={submitting} disabled={submitting}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50" className="btn-electric px-4 py-2 text-sm font-medium"
> >
{submitting ? 'Adding...' : 'Add Job'} {submitting ? 'Adding...' : 'Add Job'}
</button> </button>
@ -216,88 +216,72 @@ export default function Cron() {
{/* Jobs Table */} {/* Jobs Table */}
{jobs.length === 0 ? ( {jobs.length === 0 ? (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center"> <div className="glass-card p-8 text-center">
<Clock className="h-10 w-10 text-gray-600 mx-auto mb-3" /> <Clock className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
<p className="text-gray-400">No scheduled tasks configured.</p> <p className="text-[#556080]">No scheduled tasks configured.</p>
</div> </div>
) : ( ) : (
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto"> <div className="glass-card overflow-x-auto">
<table className="w-full text-sm"> <table className="table-electric">
<thead> <thead>
<tr className="border-b border-gray-800"> <tr>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-left">ID</th>
ID <th className="text-left">Name</th>
</th> <th className="text-left">Command</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-left">Next Run</th>
Name <th className="text-left">Last Status</th>
</th> <th className="text-left">Enabled</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-right">Actions</th>
Command
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Next Run
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Last Status
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Enabled
</th>
<th className="text-right px-4 py-3 text-gray-400 font-medium">
Actions
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{jobs.map((job) => ( {jobs.map((job) => (
<tr <tr key={job.id}>
key={job.id} <td className="px-4 py-3 text-[#556080] font-mono text-xs">
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="px-4 py-3 text-gray-400 font-mono text-xs">
{job.id.slice(0, 8)} {job.id.slice(0, 8)}
</td> </td>
<td className="px-4 py-3 text-white font-medium"> <td className="px-4 py-3 text-white font-medium text-sm">
{job.name ?? '-'} {job.name ?? '-'}
</td> </td>
<td className="px-4 py-3 text-gray-300 font-mono text-xs max-w-[200px] truncate"> <td className="px-4 py-3 text-[#8892a8] font-mono text-xs max-w-[200px] truncate">
{job.command} {job.command}
</td> </td>
<td className="px-4 py-3 text-gray-400 text-xs"> <td className="px-4 py-3 text-[#556080] text-xs">
{formatDate(job.next_run)} {formatDate(job.next_run)}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{statusIcon(job.last_status)} {statusIcon(job.last_status)}
<span className="text-gray-300 text-xs capitalize"> <span className="text-[#8892a8] text-xs capitalize">
{job.last_status ?? '-'} {job.last_status ?? '-'}
</span> </span>
</div> </div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span <span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${ className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border ${
job.enabled job.enabled
? 'bg-green-900/40 text-green-400 border border-green-700/50' ? 'text-[#00e68a] border-[#00e68a30]'
: 'bg-gray-800 text-gray-500 border border-gray-700' : 'text-[#334060] border-[#1a1a3e]'
}`} }`}
style={{ background: job.enabled ? 'rgba(0,230,138,0.06)' : 'rgba(26,26,62,0.3)' }}
> >
{job.enabled ? 'Enabled' : 'Disabled'} {job.enabled ? 'Enabled' : 'Disabled'}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
{confirmDelete === job.id ? ( {confirmDelete === job.id ? (
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2 animate-fade-in">
<span className="text-xs text-red-400">Delete?</span> <span className="text-xs text-[#ff4466]">Delete?</span>
<button <button
onClick={() => handleDelete(job.id)} onClick={() => handleDelete(job.id)}
className="text-red-400 hover:text-red-300 text-xs font-medium" className="text-[#ff4466] hover:text-[#ff6680] text-xs font-medium"
> >
Yes Yes
</button> </button>
<button <button
onClick={() => setConfirmDelete(null)} onClick={() => setConfirmDelete(null)}
className="text-gray-400 hover:text-white text-xs font-medium" className="text-[#556080] hover:text-white text-xs font-medium"
> >
No No
</button> </button>
@ -305,7 +289,7 @@ export default function Cron() {
) : ( ) : (
<button <button
onClick={() => setConfirmDelete(job.id)} onClick={() => setConfirmDelete(job.id)}
className="text-gray-400 hover:text-red-400 transition-colors" className="text-[#334060] hover:text-[#ff4466] transition-all duration-300"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</button> </button>

View File

@ -28,13 +28,13 @@ function healthColor(status: string): string {
switch (status.toLowerCase()) { switch (status.toLowerCase()) {
case 'ok': case 'ok':
case 'healthy': case 'healthy':
return 'bg-green-500'; return 'bg-[#00e68a]';
case 'warn': case 'warn':
case 'warning': case 'warning':
case 'degraded': case 'degraded':
return 'bg-yellow-500'; return 'bg-[#ffaa00]';
default: default:
return 'bg-red-500'; return 'bg-[#ff4466]';
} }
} }
@ -42,13 +42,13 @@ function healthBorder(status: string): string {
switch (status.toLowerCase()) { switch (status.toLowerCase()) {
case 'ok': case 'ok':
case 'healthy': case 'healthy':
return 'border-green-500/30'; return 'border-[#00e68a30]';
case 'warn': case 'warn':
case 'warning': case 'warning':
case 'degraded': case 'degraded':
return 'border-yellow-500/30'; return 'border-[#ffaa0030]';
default: default:
return 'border-red-500/30'; return 'border-[#ff446630]';
} }
} }
@ -68,8 +68,8 @@ export default function Dashboard() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6 animate-fade-in">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load dashboard: {error} Failed to load dashboard: {error}
</div> </div>
</div> </div>
@ -79,7 +79,7 @@ export default function Dashboard() {
if (!status || !cost) { if (!status || !cost) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
); );
} }
@ -87,124 +87,89 @@ export default function Dashboard() {
const maxCost = Math.max(cost.session_cost_usd, cost.daily_cost_usd, cost.monthly_cost_usd, 0.001); const maxCost = Math.max(cost.session_cost_usd, cost.daily_cost_usd, cost.monthly_cost_usd, 0.001);
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Status Cards Grid */} {/* Status Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800"> {[
<div className="flex items-center gap-3 mb-3"> { icon: Cpu, color: '#0080ff', bg: '#0080ff15', label: 'Provider / Model', value: status.provider ?? 'Unknown', sub: status.model },
<div className="p-2 bg-blue-600/20 rounded-lg"> { icon: Clock, color: '#00e68a', bg: '#00e68a15', label: 'Uptime', value: formatUptime(status.uptime_seconds), sub: 'Since last restart' },
<Cpu className="h-5 w-5 text-blue-400" /> { icon: Globe, color: '#a855f7', bg: '#a855f715', label: 'Gateway Port', value: `:${status.gateway_port}`, sub: `Locale: ${status.locale}` },
{ icon: Database, color: '#ff8800', bg: '#ff880015', label: 'Memory Backend', value: status.memory_backend, sub: `Paired: ${status.paired ? 'Yes' : 'No'}` },
].map(({ icon: Icon, color, bg, label, value, sub }) => (
<div key={label} className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-xl" style={{ background: bg }}>
<Icon className="h-5 w-5" style={{ color }} />
</div>
<span className="text-xs text-[#556080] uppercase tracking-wider font-medium">{label}</span>
</div> </div>
<span className="text-sm text-gray-400">Provider / Model</span> <p className="text-lg font-semibold text-white truncate capitalize">{value}</p>
<p className="text-sm text-[#556080] truncate">{sub}</p>
</div> </div>
<p className="text-lg font-semibold text-white truncate"> ))}
{status.provider ?? 'Unknown'}
</p>
<p className="text-sm text-gray-400 truncate">{status.model}</p>
</div>
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-green-600/20 rounded-lg">
<Clock className="h-5 w-5 text-green-400" />
</div>
<span className="text-sm text-gray-400">Uptime</span>
</div>
<p className="text-lg font-semibold text-white">
{formatUptime(status.uptime_seconds)}
</p>
<p className="text-sm text-gray-400">Since last restart</p>
</div>
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-purple-600/20 rounded-lg">
<Globe className="h-5 w-5 text-purple-400" />
</div>
<span className="text-sm text-gray-400">Gateway Port</span>
</div>
<p className="text-lg font-semibold text-white">
:{status.gateway_port}
</p>
<p className="text-sm text-gray-400">Locale: {status.locale}</p>
</div>
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-orange-600/20 rounded-lg">
<Database className="h-5 w-5 text-orange-400" />
</div>
<span className="text-sm text-gray-400">Memory Backend</span>
</div>
<p className="text-lg font-semibold text-white capitalize">
{status.memory_backend}
</p>
<p className="text-sm text-gray-400">
Paired: {status.paired ? 'Yes' : 'No'}
</p>
</div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 stagger-children">
{/* Cost Widget */} {/* Cost Widget */}
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800"> <div className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-5">
<DollarSign className="h-5 w-5 text-blue-400" /> <DollarSign className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white">Cost Overview</h2> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Cost Overview</h2>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{[ {[
{ label: 'Session', value: cost.session_cost_usd, color: 'bg-blue-500' }, { label: 'Session', value: cost.session_cost_usd, color: '#0080ff' },
{ label: 'Daily', value: cost.daily_cost_usd, color: 'bg-green-500' }, { label: 'Daily', value: cost.daily_cost_usd, color: '#00e68a' },
{ label: 'Monthly', value: cost.monthly_cost_usd, color: 'bg-purple-500' }, { label: 'Monthly', value: cost.monthly_cost_usd, color: '#a855f7' },
].map(({ label, value, color }) => ( ].map(({ label, value, color }) => (
<div key={label}> <div key={label}>
<div className="flex justify-between text-sm mb-1"> <div className="flex justify-between text-sm mb-1.5">
<span className="text-gray-400">{label}</span> <span className="text-[#556080]">{label}</span>
<span className="text-white font-medium">{formatUSD(value)}</span> <span className="text-white font-medium font-mono">{formatUSD(value)}</span>
</div> </div>
<div className="w-full h-2 bg-gray-800 rounded-full overflow-hidden"> <div className="w-full h-1.5 bg-[#0a0a18] rounded-full overflow-hidden">
<div <div
className={`h-full rounded-full ${color}`} className="h-full rounded-full progress-bar-animated transition-all duration-700 ease-out"
style={{ width: `${Math.max((value / maxCost) * 100, 2)}%` }} style={{ width: `${Math.max((value / maxCost) * 100, 2)}%`, background: color }}
/> />
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="mt-4 pt-3 border-t border-gray-800 flex justify-between text-sm"> <div className="mt-5 pt-4 border-t border-[#1a1a3e]/50 flex justify-between text-sm">
<span className="text-gray-400">Total Tokens</span> <span className="text-[#556080]">Total Tokens</span>
<span className="text-white">{cost.total_tokens.toLocaleString()}</span> <span className="text-white font-mono">{cost.total_tokens.toLocaleString()}</span>
</div> </div>
<div className="flex justify-between text-sm mt-1"> <div className="flex justify-between text-sm mt-1">
<span className="text-gray-400">Requests</span> <span className="text-[#556080]">Requests</span>
<span className="text-white">{cost.request_count.toLocaleString()}</span> <span className="text-white font-mono">{cost.request_count.toLocaleString()}</span>
</div> </div>
</div> </div>
{/* Active Channels */} {/* Active Channels */}
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800"> <div className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-5">
<Radio className="h-5 w-5 text-blue-400" /> <Radio className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white">Active Channels</h2> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Active Channels</h2>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(status.channels).length === 0 ? ( {Object.entries(status.channels).length === 0 ? (
<p className="text-sm text-gray-500">No channels configured</p> <p className="text-sm text-[#334060]">No channels configured</p>
) : ( ) : (
Object.entries(status.channels).map(([name, active]) => ( Object.entries(status.channels).map(([name, active]) => (
<div <div
key={name} key={name}
className="flex items-center justify-between py-2 px-3 rounded-lg bg-gray-800/50" className="flex items-center justify-between py-2.5 px-3 rounded-xl transition-all duration-300 hover:bg-[#0080ff08]"
style={{ background: 'rgba(10, 10, 26, 0.5)' }}
> >
<span className="text-sm text-white capitalize">{name}</span> <span className="text-sm text-white capitalize font-medium">{name}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className={`inline-block h-2.5 w-2.5 rounded-full ${ className={`inline-block h-2 w-2 rounded-full glow-dot ${
active ? 'bg-green-500' : 'bg-gray-500' active ? 'text-[#00e68a] bg-[#00e68a]' : 'text-[#334060] bg-[#334060]'
}`} }`}
/> />
<span className="text-xs text-gray-400"> <span className="text-xs text-[#556080]">
{active ? 'Active' : 'Inactive'} {active ? 'Active' : 'Inactive'}
</span> </span>
</div> </div>
@ -215,29 +180,30 @@ export default function Dashboard() {
</div> </div>
{/* Health Grid */} {/* Health Grid */}
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800"> <div className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-5">
<Activity className="h-5 w-5 text-blue-400" /> <Activity className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white">Component Health</h2> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Component Health</h2>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{Object.entries(status.health.components).length === 0 ? ( {Object.entries(status.health.components).length === 0 ? (
<p className="text-sm text-gray-500 col-span-2">No components reporting</p> <p className="text-sm text-[#334060] col-span-2">No components reporting</p>
) : ( ) : (
Object.entries(status.health.components).map(([name, comp]) => ( Object.entries(status.health.components).map(([name, comp]) => (
<div <div
key={name} key={name}
className={`rounded-lg p-3 border ${healthBorder(comp.status)} bg-gray-800/50`} className={`rounded-xl p-3 border ${healthBorder(comp.status)} transition-all duration-300 hover:scale-[1.02]`}
style={{ background: 'rgba(10, 10, 26, 0.5)' }}
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className={`inline-block h-2 w-2 rounded-full ${healthColor(comp.status)}`} /> <span className={`inline-block h-2 w-2 rounded-full ${healthColor(comp.status)} glow-dot`} />
<span className="text-sm font-medium text-white capitalize truncate"> <span className="text-sm font-medium text-white capitalize truncate">
{name} {name}
</span> </span>
</div> </div>
<p className="text-xs text-gray-400 capitalize">{comp.status}</p> <p className="text-xs text-[#556080] capitalize">{comp.status}</p>
{comp.restart_count > 0 && ( {comp.restart_count > 0 && (
<p className="text-xs text-yellow-400 mt-1"> <p className="text-xs text-[#ffaa00] mt-1">
Restarts: {comp.restart_count} Restarts: {comp.restart_count}
</p> </p>
)} )}

View File

@ -13,33 +13,33 @@ import { runDoctor } from '@/lib/api';
function severityIcon(severity: DiagResult['severity']) { function severityIcon(severity: DiagResult['severity']) {
switch (severity) { switch (severity) {
case 'ok': case 'ok':
return <CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" />; return <CheckCircle className="h-4 w-4 text-[#00e68a] flex-shrink-0" />;
case 'warn': case 'warn':
return <AlertTriangle className="h-4 w-4 text-yellow-400 flex-shrink-0" />; return <AlertTriangle className="h-4 w-4 text-[#ffaa00] flex-shrink-0" />;
case 'error': case 'error':
return <XCircle className="h-4 w-4 text-red-400 flex-shrink-0" />; return <XCircle className="h-4 w-4 text-[#ff4466] flex-shrink-0" />;
} }
} }
function severityBorder(severity: DiagResult['severity']): string { function severityBorder(severity: DiagResult['severity']): string {
switch (severity) { switch (severity) {
case 'ok': case 'ok':
return 'border-green-700/40'; return 'border-[#00e68a20]';
case 'warn': case 'warn':
return 'border-yellow-700/40'; return 'border-[#ffaa0020]';
case 'error': case 'error':
return 'border-red-700/40'; return 'border-[#ff446620]';
} }
} }
function severityBg(severity: DiagResult['severity']): string { function severityBg(severity: DiagResult['severity']): string {
switch (severity) { switch (severity) {
case 'ok': case 'ok':
return 'bg-green-900/10'; return 'rgba(0,230,138,0.04)';
case 'warn': case 'warn':
return 'bg-yellow-900/10'; return 'rgba(255,170,0,0.04)';
case 'error': case 'error':
return 'bg-red-900/10'; return 'rgba(255,68,102,0.04)';
} }
} }
@ -62,12 +62,10 @@ export default function Doctor() {
} }
}; };
// Compute summary counts
const okCount = results?.filter((r) => r.severity === 'ok').length ?? 0; const okCount = results?.filter((r) => r.severity === 'ok').length ?? 0;
const warnCount = results?.filter((r) => r.severity === 'warn').length ?? 0; const warnCount = results?.filter((r) => r.severity === 'warn').length ?? 0;
const errorCount = results?.filter((r) => r.severity === 'error').length ?? 0; const errorCount = results?.filter((r) => r.severity === 'error').length ?? 0;
// Group by category
const grouped = const grouped =
results?.reduce<Record<string, DiagResult[]>>((acc, item) => { results?.reduce<Record<string, DiagResult[]>>((acc, item) => {
const key = item.category; const key = item.category;
@ -77,17 +75,17 @@ export default function Doctor() {
}, {}) ?? {}; }, {}) ?? {};
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Stethoscope className="h-5 w-5 text-blue-400" /> <Stethoscope className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white">Diagnostics</h2> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Diagnostics</h2>
</div> </div>
<button <button
onClick={handleRun} onClick={handleRun}
disabled={loading} disabled={loading}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors disabled:opacity-50" className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
> >
{loading ? ( {loading ? (
<> <>
@ -105,17 +103,17 @@ export default function Doctor() {
{/* Error */} {/* Error */}
{error && ( {error && (
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680] animate-fade-in">
{error} {error}
</div> </div>
)} )}
{/* Loading spinner */} {/* Loading spinner */}
{loading && ( {loading && (
<div className="flex flex-col items-center justify-center py-16"> <div className="flex flex-col items-center justify-center py-16 animate-fade-in">
<Loader2 className="h-10 w-10 text-blue-500 animate-spin mb-4" /> <div className="h-12 w-12 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin mb-4" />
<p className="text-gray-400">Running diagnostics...</p> <p className="text-[#8892a8]">Running diagnostics...</p>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-[#334060] mt-1">
This may take a few seconds. This may take a few seconds.
</p> </p>
</div> </div>
@ -125,29 +123,29 @@ export default function Doctor() {
{results && !loading && ( {results && !loading && (
<> <>
{/* Summary Bar */} {/* Summary Bar */}
<div className="flex items-center gap-4 bg-gray-900 rounded-xl border border-gray-800 p-4"> <div className="glass-card flex items-center gap-4 p-4 animate-slide-in-up">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-400" /> <CheckCircle className="h-5 w-5 text-[#00e68a]" />
<span className="text-sm text-white font-medium"> <span className="text-sm text-white font-medium">
{okCount} <span className="text-gray-400 font-normal">ok</span> {okCount} <span className="text-[#556080] font-normal">ok</span>
</span> </span>
</div> </div>
<div className="w-px h-5 bg-gray-700" /> <div className="w-px h-5 bg-[#1a1a3e]" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-400" /> <AlertTriangle className="h-5 w-5 text-[#ffaa00]" />
<span className="text-sm text-white font-medium"> <span className="text-sm text-white font-medium">
{warnCount}{' '} {warnCount}{' '}
<span className="text-gray-400 font-normal"> <span className="text-[#556080] font-normal">
warning{warnCount !== 1 ? 's' : ''} warning{warnCount !== 1 ? 's' : ''}
</span> </span>
</span> </span>
</div> </div>
<div className="w-px h-5 bg-gray-700" /> <div className="w-px h-5 bg-[#1a1a3e]" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<XCircle className="h-5 w-5 text-red-400" /> <XCircle className="h-5 w-5 text-[#ff4466]" />
<span className="text-sm text-white font-medium"> <span className="text-sm text-white font-medium">
{errorCount}{' '} {errorCount}{' '}
<span className="text-gray-400 font-normal"> <span className="text-[#556080] font-normal">
error{errorCount !== 1 ? 's' : ''} error{errorCount !== 1 ? 's' : ''}
</span> </span>
</span> </span>
@ -156,15 +154,15 @@ export default function Doctor() {
{/* Overall indicator */} {/* Overall indicator */}
<div className="ml-auto"> <div className="ml-auto">
{errorCount > 0 ? ( {errorCount > 0 ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-red-900/40 text-red-400 border border-red-700/50"> <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-semibold border text-[#ff4466] border-[#ff446630]" style={{ background: 'rgba(255,68,102,0.06)' }}>
Issues Found Issues Found
</span> </span>
) : warnCount > 0 ? ( ) : warnCount > 0 ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-yellow-900/40 text-yellow-400 border border-yellow-700/50"> <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-semibold border text-[#ffaa00] border-[#ffaa0030]" style={{ background: 'rgba(255,170,0,0.06)' }}>
Warnings Warnings
</span> </span>
) : ( ) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-green-900/40 text-green-400 border border-green-700/50"> <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-semibold border text-[#00e68a] border-[#00e68a30]" style={{ background: 'rgba(0,230,138,0.06)' }}>
All Clear All Clear
</span> </span>
)} )}
@ -174,23 +172,22 @@ export default function Doctor() {
{/* Grouped Results */} {/* Grouped Results */}
{Object.entries(grouped) {Object.entries(grouped)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([category, items]) => ( .map(([category, items], catIdx) => (
<div key={category}> <div key={category} className="animate-slide-in-up" style={{ animationDelay: `${(catIdx + 1) * 100}ms` }}>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 capitalize"> <h3 className="text-[10px] font-semibold text-[#334060] uppercase tracking-wider mb-3 capitalize">
{category} {category}
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2 stagger-children">
{items.map((result, idx) => ( {items.map((result, idx) => (
<div <div
key={`${category}-${idx}`} key={`${category}-${idx}`}
className={`flex items-start gap-3 rounded-lg border p-3 ${severityBorder( className={`flex items-start gap-3 rounded-xl border p-3 transition-all duration-300 hover:translate-x-1 ${severityBorder(result.severity)} animate-slide-in-left`}
result.severity, style={{ background: severityBg(result.severity) }}
)} ${severityBg(result.severity)}`}
> >
{severityIcon(result.severity)} {severityIcon(result.severity)}
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-white">{result.message}</p> <p className="text-sm text-white">{result.message}</p>
<p className="text-xs text-gray-500 mt-0.5 capitalize"> <p className="text-[10px] text-[#334060] mt-0.5 capitalize uppercase tracking-wider">
{result.severity} {result.severity}
</p> </p>
</div> </div>
@ -204,10 +201,12 @@ export default function Doctor() {
{/* Empty state */} {/* Empty state */}
{!results && !loading && !error && ( {!results && !loading && !error && (
<div className="flex flex-col items-center justify-center py-16 text-gray-500"> <div className="flex flex-col items-center justify-center py-16 text-[#334060] animate-fade-in">
<Stethoscope className="h-12 w-12 text-gray-600 mb-4" /> <div className="h-16 w-16 rounded-2xl flex items-center justify-center mb-4 animate-float" style={{ background: 'linear-gradient(135deg, #0080ff15, #0080ff08)' }}>
<p className="text-lg font-medium">System Diagnostics</p> <Stethoscope className="h-8 w-8 text-[#0080ff]" />
<p className="text-sm mt-1"> </div>
<p className="text-lg font-semibold text-white mb-1">System Diagnostics</p>
<p className="text-sm text-[#556080]">
Click "Run Diagnostics" to check your ZeroClaw installation. Click "Run Diagnostics" to check your ZeroClaw installation.
</p> </p>
</div> </div>

View File

@ -9,19 +9,22 @@ function statusBadge(status: Integration['status']) {
return { return {
icon: Check, icon: Check,
label: 'Active', label: 'Active',
classes: 'bg-green-900/40 text-green-400 border-green-700/50', classes: 'text-[#00e68a] border-[#00e68a30]',
bg: 'rgba(0,230,138,0.06)',
}; };
case 'Available': case 'Available':
return { return {
icon: Zap, icon: Zap,
label: 'Available', label: 'Available',
classes: 'bg-blue-900/40 text-blue-400 border-blue-700/50', classes: 'text-[#0080ff] border-[#0080ff30]',
bg: 'rgba(0,128,255,0.06)',
}; };
case 'ComingSoon': case 'ComingSoon':
return { return {
icon: Clock, icon: Clock,
label: 'Coming Soon', label: 'Coming Soon',
classes: 'bg-gray-800 text-gray-400 border-gray-700', classes: 'text-[#556080] border-[#1a1a3e]',
bg: 'rgba(26,26,62,0.3)',
}; };
} }
} }
@ -59,8 +62,8 @@ export default function Integrations() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6 animate-fade-in">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load integrations: {error} Failed to load integrations: {error}
</div> </div>
</div> </div>
@ -70,17 +73,17 @@ export default function Integrations() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Puzzle className="h-5 w-5 text-blue-400" /> <Puzzle className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white"> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Integrations ({integrations.length}) Integrations ({integrations.length})
</h2> </h2>
</div> </div>
@ -91,11 +94,12 @@ export default function Integrations() {
<button <button
key={cat} key={cat}
onClick={() => setActiveCategory(cat)} onClick={() => setActiveCategory(cat)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors capitalize ${ className={`px-3.5 py-1.5 rounded-xl text-xs font-semibold transition-all duration-300 capitalize ${
activeCategory === cat activeCategory === cat
? 'bg-blue-600 text-white' ? 'text-white shadow-[0_0_15px_rgba(0,128,255,0.2)]'
: 'bg-gray-900 text-gray-400 border border-gray-700 hover:bg-gray-800 hover:text-white' : 'text-[#556080] border border-[#1a1a3e] hover:text-white hover:border-[#0080ff40]'
}`} }`}
style={activeCategory === cat ? { background: 'linear-gradient(135deg, #0080ff, #0066cc)' } : {}}
> >
{cat} {cat}
</button> </button>
@ -104,38 +108,39 @@ export default function Integrations() {
{/* Grouped Integration Cards */} {/* Grouped Integration Cards */}
{Object.keys(grouped).length === 0 ? ( {Object.keys(grouped).length === 0 ? (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center"> <div className="glass-card p-8 text-center">
<Puzzle className="h-10 w-10 text-gray-600 mx-auto mb-3" /> <Puzzle className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
<p className="text-gray-400">No integrations found.</p> <p className="text-[#556080]">No integrations found.</p>
</div> </div>
) : ( ) : (
Object.entries(grouped) Object.entries(grouped)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([category, items]) => ( .map(([category, items]) => (
<div key={category}> <div key={category}>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 capitalize"> <h3 className="text-[10px] font-semibold text-[#334060] uppercase tracking-wider mb-3 capitalize">
{category} {category}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 stagger-children">
{items.map((integration) => { {items.map((integration) => {
const badge = statusBadge(integration.status); const badge = statusBadge(integration.status);
const BadgeIcon = badge.icon; const BadgeIcon = badge.icon;
return ( return (
<div <div
key={integration.name} key={integration.name}
className="bg-gray-900 rounded-xl border border-gray-800 p-5 hover:border-gray-700 transition-colors" className="glass-card p-5 animate-slide-in-up"
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h4 className="text-sm font-semibold text-white truncate"> <h4 className="text-sm font-semibold text-white truncate">
{integration.name} {integration.name}
</h4> </h4>
<p className="text-sm text-gray-400 mt-1 line-clamp-2"> <p className="text-sm text-[#556080] mt-1 line-clamp-2">
{integration.description} {integration.description}
</p> </p>
</div> </div>
<span <span
className={`flex-shrink-0 inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium border ${badge.classes}`} className={`flex-shrink-0 inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[10px] font-semibold border ${badge.classes}`}
style={{ background: badge.bg }}
> >
<BadgeIcon className="h-3 w-3" /> <BadgeIcon className="h-3 w-3" />
{badge.label} {badge.label}

View File

@ -14,24 +14,24 @@ function formatTimestamp(ts?: string): string {
return new Date(ts).toLocaleTimeString(); return new Date(ts).toLocaleTimeString();
} }
function eventTypeBadgeColor(type: string): string { function eventTypeBadgeColor(type: string): { classes: string; bg: string } {
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'error': case 'error':
return 'bg-red-900/50 text-red-400 border-red-700/50'; return { classes: 'text-[#ff4466] border-[#ff446630]', bg: 'rgba(255,68,102,0.06)' };
case 'warn': case 'warn':
case 'warning': case 'warning':
return 'bg-yellow-900/50 text-yellow-400 border-yellow-700/50'; return { classes: 'text-[#ffaa00] border-[#ffaa0030]', bg: 'rgba(255,170,0,0.06)' };
case 'tool_call': case 'tool_call':
case 'tool_result': case 'tool_result':
return 'bg-purple-900/50 text-purple-400 border-purple-700/50'; return { classes: 'text-[#a855f7] border-[#a855f730]', bg: 'rgba(168,85,247,0.06)' };
case 'message': case 'message':
case 'chat': case 'chat':
return 'bg-blue-900/50 text-blue-400 border-blue-700/50'; return { classes: 'text-[#0080ff] border-[#0080ff30]', bg: 'rgba(0,128,255,0.06)' };
case 'health': case 'health':
case 'status': case 'status':
return 'bg-green-900/50 text-green-400 border-green-700/50'; return { classes: 'text-[#00e68a] border-[#00e68a30]', bg: 'rgba(0,230,138,0.06)' };
default: default:
return 'bg-gray-800 text-gray-400 border-gray-700'; return { classes: 'text-[#556080] border-[#1a1a3e]', bg: 'rgba(26,26,62,0.3)' };
} }
} }
@ -76,7 +76,6 @@ export default function Logs() {
event, event,
}; };
setEntries((prev) => { setEntries((prev) => {
// Cap at 500 entries for performance
const next = [...prev, entry]; const next = [...prev, entry];
return next.length > 500 ? next.slice(-500) : next; return next.length > 500 ? next.slice(-500) : next;
}); });
@ -112,7 +111,6 @@ export default function Logs() {
setAutoScroll(true); setAutoScroll(true);
}; };
// Collect all event types for filter checkboxes
const allTypes = Array.from(new Set(entries.map((e) => e.event.type))).sort(); const allTypes = Array.from(new Set(entries.map((e) => e.event.type))).sort();
const toggleTypeFilter = (type: string) => { const toggleTypeFilter = (type: string) => {
@ -135,21 +133,21 @@ export default function Logs() {
return ( return (
<div className="flex flex-col h-[calc(100vh-3.5rem)]"> <div className="flex flex-col h-[calc(100vh-3.5rem)]">
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center justify-between px-6 py-3 border-b border-gray-800 bg-gray-900"> <div className="flex items-center justify-between px-6 py-3 border-b border-[#1a1a3e]/40 animate-fade-in" style={{ background: 'linear-gradient(90deg, rgba(8,8,24,0.9), rgba(5,5,16,0.9))' }}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-blue-400" /> <Activity className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white">Live Logs</h2> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Live Logs</h2>
<div className="flex items-center gap-2 ml-2"> <div className="flex items-center gap-2 ml-2">
<span <span
className={`inline-block h-2 w-2 rounded-full ${ className={`inline-block h-1.5 w-1.5 rounded-full glow-dot ${
connected ? 'bg-green-500' : 'bg-red-500' connected ? 'text-[#00e68a] bg-[#00e68a]' : 'text-[#ff4466] bg-[#ff4466]'
}`} }`}
/> />
<span className="text-xs text-gray-500"> <span className="text-[10px] text-[#334060]">
{connected ? 'Connected' : 'Disconnected'} {connected ? 'Connected' : 'Disconnected'}
</span> </span>
</div> </div>
<span className="text-xs text-gray-500 ml-2"> <span className="text-[10px] text-[#334060] ml-2 font-mono">
{filteredEntries.length} events {filteredEntries.length} events
</span> </span>
</div> </div>
@ -158,11 +156,16 @@ export default function Logs() {
{/* Pause/Resume */} {/* Pause/Resume */}
<button <button
onClick={() => setPaused(!paused)} onClick={() => setPaused(!paused)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all duration-300 ${
paused paused
? 'bg-green-600 hover:bg-green-700 text-white' ? 'text-white shadow-[0_0_15px_rgba(0,230,138,0.2)]'
: 'bg-yellow-600 hover:bg-yellow-700 text-white' : 'text-white shadow-[0_0_15px_rgba(255,170,0,0.2)]'
}`} }`}
style={{
background: paused
? 'linear-gradient(135deg, #00e68a, #00cc7a)'
: 'linear-gradient(135deg, #ffaa00, #ee9900)'
}}
> >
{paused ? ( {paused ? (
<> <>
@ -179,7 +182,7 @@ export default function Logs() {
{!autoScroll && ( {!autoScroll && (
<button <button
onClick={jumpToBottom} onClick={jumpToBottom}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors" className="btn-electric flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold"
> >
<ArrowDown className="h-3.5 w-3.5" /> <ArrowDown className="h-3.5 w-3.5" />
Jump to bottom Jump to bottom
@ -190,9 +193,9 @@ export default function Logs() {
{/* Event type filters */} {/* Event type filters */}
{allTypes.length > 0 && ( {allTypes.length > 0 && (
<div className="flex items-center gap-2 px-6 py-2 border-b border-gray-800 bg-gray-900/80 overflow-x-auto"> <div className="flex items-center gap-2 px-6 py-2 border-b border-[#1a1a3e]/30 overflow-x-auto" style={{ background: 'rgba(5,5,16,0.6)' }}>
<Filter className="h-4 w-4 text-gray-500 flex-shrink-0" /> <Filter className="h-3.5 w-3.5 text-[#334060] flex-shrink-0" />
<span className="text-xs text-gray-500 flex-shrink-0">Filter:</span> <span className="text-[10px] text-[#334060] flex-shrink-0 uppercase tracking-wider">Filter:</span>
{allTypes.map((type) => ( {allTypes.map((type) => (
<label <label
key={type} key={type}
@ -202,15 +205,15 @@ export default function Logs() {
type="checkbox" type="checkbox"
checked={typeFilters.has(type)} checked={typeFilters.has(type)}
onChange={() => toggleTypeFilter(type)} onChange={() => toggleTypeFilter(type)}
className="rounded bg-gray-800 border-gray-600 text-blue-500 focus:ring-blue-500 focus:ring-offset-0 h-3.5 w-3.5" className="rounded bg-[#0a0a18] border-[#1a1a3e] text-[#0080ff] focus:ring-[#0080ff] focus:ring-offset-0 h-3 w-3"
/> />
<span className="text-xs text-gray-400 capitalize">{type}</span> <span className="text-[10px] text-[#556080] capitalize">{type}</span>
</label> </label>
))} ))}
{typeFilters.size > 0 && ( {typeFilters.size > 0 && (
<button <button
onClick={() => setTypeFilters(new Set())} onClick={() => setTypeFilters(new Set())}
className="text-xs text-blue-400 hover:text-blue-300 flex-shrink-0 ml-1" className="text-[10px] text-[#0080ff] hover:text-[#00d4ff] flex-shrink-0 ml-1 transition-colors"
> >
Clear Clear
</button> </button>
@ -222,11 +225,11 @@ export default function Logs() {
<div <div
ref={containerRef} ref={containerRef}
onScroll={handleScroll} onScroll={handleScroll}
className="flex-1 overflow-y-auto p-4 space-y-2" className="flex-1 overflow-y-auto p-4 space-y-1.5"
> >
{filteredEntries.length === 0 ? ( {filteredEntries.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500"> <div className="flex flex-col items-center justify-center h-full text-[#334060] animate-fade-in">
<Activity className="h-10 w-10 text-gray-600 mb-3" /> <Activity className="h-10 w-10 text-[#1a1a3e] mb-3" />
<p className="text-sm"> <p className="text-sm">
{paused {paused
? 'Log streaming is paused.' ? 'Log streaming is paused.'
@ -236,6 +239,7 @@ export default function Logs() {
) : ( ) : (
filteredEntries.map((entry) => { filteredEntries.map((entry) => {
const { event } = entry; const { event } = entry;
const badge = eventTypeBadgeColor(event.type);
const detail = const detail =
event.message ?? event.message ??
event.content ?? event.content ??
@ -251,20 +255,19 @@ export default function Logs() {
return ( return (
<div <div
key={entry.id} key={entry.id}
className="bg-gray-900 border border-gray-800 rounded-lg p-3 hover:border-gray-700 transition-colors" className="glass-card rounded-lg p-3 hover:border-[#0080ff20] transition-all duration-200"
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-xs text-gray-500 font-mono whitespace-nowrap mt-0.5"> <span className="text-[10px] text-[#334060] font-mono whitespace-nowrap mt-0.5">
{formatTimestamp(event.timestamp)} {formatTimestamp(event.timestamp)}
</span> </span>
<span <span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border capitalize flex-shrink-0 ${eventTypeBadgeColor( className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border capitalize flex-shrink-0 ${badge.classes}`}
event.type, style={{ background: badge.bg }}
)}`}
> >
{event.type} {event.type}
</span> </span>
<p className="text-sm text-gray-300 break-all min-w-0"> <p className="text-sm text-[#8892a8] break-all min-w-0">
{typeof detail === 'string' ? detail : JSON.stringify(detail)} {typeof detail === 'string' ? detail : JSON.stringify(detail)}
</p> </p>
</div> </div>

View File

@ -96,8 +96,8 @@ export default function Memory() {
if (error && entries.length === 0) { if (error && entries.length === 0) {
return ( return (
<div className="p-6"> <div className="p-6 animate-fade-in">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load memory: {error} Failed to load memory: {error}
</div> </div>
</div> </div>
@ -105,18 +105,18 @@ export default function Memory() {
} }
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Brain className="h-5 w-5 text-blue-400" /> <Brain className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white"> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Memory ({entries.length}) Memory ({entries.length})
</h2> </h2>
</div> </div>
<button <button
onClick={() => setShowForm(true)} onClick={() => setShowForm(true)}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors" className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Add Memory Add Memory
@ -126,22 +126,22 @@ export default function Memory() {
{/* Search and Filter */} {/* Search and Filter */}
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#334060]" />
<input <input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Search memory entries..." placeholder="Search memory entries..."
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="input-electric w-full pl-10 pr-4 py-2.5 text-sm"
/> />
</div> </div>
<div className="relative"> <div className="relative">
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" /> <Filter className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#334060]" />
<select <select
value={categoryFilter} value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)} onChange={(e) => setCategoryFilter(e.target.value)}
className="bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-8 py-2.5 text-sm text-white appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer" className="input-electric pl-10 pr-8 py-2.5 text-sm appearance-none cursor-pointer"
> >
<option value="">All Categories</option> <option value="">All Categories</option>
{categories.map((cat) => ( {categories.map((cat) => (
@ -153,7 +153,7 @@ export default function Memory() {
</div> </div>
<button <button
onClick={handleSearch} onClick={handleSearch}
className="px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors" className="btn-electric px-4 py-2.5 text-sm"
> >
Search Search
</button> </button>
@ -161,15 +161,15 @@ export default function Memory() {
{/* Error banner (non-fatal) */} {/* Error banner (non-fatal) */}
{error && ( {error && (
<div className="rounded-lg bg-red-900/30 border border-red-700 p-3 text-sm text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-3 text-sm text-[#ff6680] animate-fade-in">
{error} {error}
</div> </div>
)} )}
{/* Add Memory Form Modal */} {/* Add Memory Form Modal */}
{showForm && ( {showForm && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"> <div className="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md mx-4"> <div className="glass-card p-6 w-full max-w-md mx-4 animate-fade-in-scale">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">Add Memory</h3> <h3 className="text-lg font-semibold text-white">Add Memory</h3>
<button <button
@ -177,45 +177,45 @@ export default function Memory() {
setShowForm(false); setShowForm(false);
setFormError(null); setFormError(null);
}} }}
className="text-gray-400 hover:text-white transition-colors" className="text-[#556080] hover:text-white transition-colors duration-300"
> >
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
{formError && ( {formError && (
<div className="mb-4 rounded-lg bg-red-900/30 border border-red-700 p-3 text-sm text-red-300"> <div className="mb-4 rounded-xl bg-[#ff446615] border border-[#ff446630] p-3 text-sm text-[#ff6680] animate-fade-in">
{formError} {formError}
</div> </div>
)} )}
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-1"> <label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Key <span className="text-red-400">*</span> Key <span className="text-[#ff4466]">*</span>
</label> </label>
<input <input
type="text" type="text"
value={formKey} value={formKey}
onChange={(e) => setFormKey(e.target.value)} onChange={(e) => setFormKey(e.target.value)}
placeholder="e.g. user_preferences" placeholder="e.g. user_preferences"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="input-electric w-full px-3 py-2.5 text-sm"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-1"> <label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Content <span className="text-red-400">*</span> Content <span className="text-[#ff4466]">*</span>
</label> </label>
<textarea <textarea
value={formContent} value={formContent}
onChange={(e) => setFormContent(e.target.value)} onChange={(e) => setFormContent(e.target.value)}
placeholder="Memory content..." placeholder="Memory content..."
rows={4} rows={4}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none" className="input-electric w-full px-3 py-2.5 text-sm resize-none"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-300 mb-1"> <label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Category (optional) Category (optional)
</label> </label>
<input <input
@ -223,7 +223,7 @@ export default function Memory() {
value={formCategory} value={formCategory}
onChange={(e) => setFormCategory(e.target.value)} onChange={(e) => setFormCategory(e.target.value)}
placeholder="e.g. preferences, context, facts" placeholder="e.g. preferences, context, facts"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" className="input-electric w-full px-3 py-2.5 text-sm"
/> />
</div> </div>
</div> </div>
@ -234,14 +234,14 @@ export default function Memory() {
setShowForm(false); setShowForm(false);
setFormError(null); setFormError(null);
}} }}
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors" className="px-4 py-2 text-sm font-medium text-[#8892a8] hover:text-white border border-[#1a1a3e] rounded-xl hover:bg-[#0080ff08] transition-all duration-300"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleAdd} onClick={handleAdd}
disabled={submitting} disabled={submitting}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50" className="btn-electric px-4 py-2 text-sm font-medium"
> >
{submitting ? 'Saving...' : 'Save'} {submitting ? 'Saving...' : 'Save'}
</button> </button>
@ -253,70 +253,57 @@ export default function Memory() {
{/* Memory Table */} {/* Memory Table */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-32"> <div className="flex items-center justify-center h-32">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
) : entries.length === 0 ? ( ) : entries.length === 0 ? (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center"> <div className="glass-card p-8 text-center">
<Brain className="h-10 w-10 text-gray-600 mx-auto mb-3" /> <Brain className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
<p className="text-gray-400">No memory entries found.</p> <p className="text-[#556080]">No memory entries found.</p>
</div> </div>
) : ( ) : (
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto"> <div className="glass-card overflow-x-auto">
<table className="w-full text-sm"> <table className="table-electric">
<thead> <thead>
<tr className="border-b border-gray-800"> <tr>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-left">Key</th>
Key <th className="text-left">Content</th>
</th> <th className="text-left">Category</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-left">Timestamp</th>
Content <th className="text-right">Actions</th>
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Category
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Timestamp
</th>
<th className="text-right px-4 py-3 text-gray-400 font-medium">
Actions
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{entries.map((entry) => ( {entries.map((entry) => (
<tr <tr key={entry.id}>
key={entry.id}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="px-4 py-3 text-white font-medium font-mono text-xs"> <td className="px-4 py-3 text-white font-medium font-mono text-xs">
{entry.key} {entry.key}
</td> </td>
<td className="px-4 py-3 text-gray-300 max-w-[300px]"> <td className="px-4 py-3 text-[#8892a8] max-w-[300px] text-sm">
<span title={entry.content}> <span title={entry.content}>
{truncate(entry.content, 80)} {truncate(entry.content, 80)}
</span> </span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-800 text-gray-300 capitalize"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold capitalize border border-[#1a1a3e] text-[#8892a8]" style={{ background: 'rgba(0,128,255,0.06)' }}>
{entry.category} {entry.category}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-gray-400 text-xs whitespace-nowrap"> <td className="px-4 py-3 text-[#556080] text-xs whitespace-nowrap">
{formatDate(entry.timestamp)} {formatDate(entry.timestamp)}
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
{confirmDelete === entry.key ? ( {confirmDelete === entry.key ? (
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2 animate-fade-in">
<span className="text-xs text-red-400">Delete?</span> <span className="text-xs text-[#ff4466]">Delete?</span>
<button <button
onClick={() => handleDelete(entry.key)} onClick={() => handleDelete(entry.key)}
className="text-red-400 hover:text-red-300 text-xs font-medium" className="text-[#ff4466] hover:text-[#ff6680] text-xs font-medium"
> >
Yes Yes
</button> </button>
<button <button
onClick={() => setConfirmDelete(null)} onClick={() => setConfirmDelete(null)}
className="text-gray-400 hover:text-white text-xs font-medium" className="text-[#556080] hover:text-white text-xs font-medium"
> >
No No
</button> </button>
@ -324,7 +311,7 @@ export default function Memory() {
) : ( ) : (
<button <button
onClick={() => setConfirmDelete(entry.key)} onClick={() => setConfirmDelete(entry.key)}
className="text-gray-400 hover:text-red-400 transition-colors" className="text-[#334060] hover:text-[#ff4466] transition-all duration-300"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</button> </button>

View File

@ -42,8 +42,8 @@ export default function Tools() {
if (error) { if (error) {
return ( return (
<div className="p-6"> <div className="p-6 animate-fade-in">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300"> <div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load tools: {error} Failed to load tools: {error}
</div> </div>
</div> </div>
@ -53,75 +53,75 @@ export default function Tools() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" /> <div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6 animate-fade-in">
{/* Search */} {/* Search */}
<div className="relative max-w-md"> <div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#334060]" />
<input <input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="Search tools..." placeholder="Search tools..."
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="input-electric w-full pl-10 pr-4 py-2.5 text-sm"
/> />
</div> </div>
{/* Agent Tools Grid */} {/* Agent Tools Grid */}
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Wrench className="h-5 w-5 text-blue-400" /> <Wrench className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-base font-semibold text-white"> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Agent Tools ({filtered.length}) Agent Tools ({filtered.length})
</h2> </h2>
</div> </div>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<p className="text-sm text-gray-500">No tools match your search.</p> <p className="text-sm text-[#334060]">No tools match your search.</p>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 stagger-children">
{filtered.map((tool) => { {filtered.map((tool) => {
const isExpanded = expandedTool === tool.name; const isExpanded = expandedTool === tool.name;
return ( return (
<div <div
key={tool.name} key={tool.name}
className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden" className="glass-card overflow-hidden animate-slide-in-up"
> >
<button <button
onClick={() => onClick={() =>
setExpandedTool(isExpanded ? null : tool.name) setExpandedTool(isExpanded ? null : tool.name)
} }
className="w-full text-left p-4 hover:bg-gray-800/50 transition-colors" className="w-full text-left p-4 hover:bg-[#0080ff08] transition-all duration-300"
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<Package className="h-4 w-4 text-blue-400 flex-shrink-0 mt-0.5" /> <Package className="h-4 w-4 text-[#0080ff] flex-shrink-0 mt-0.5" />
<h3 className="text-sm font-semibold text-white truncate"> <h3 className="text-sm font-semibold text-white truncate">
{tool.name} {tool.name}
</h3> </h3>
</div> </div>
{isExpanded ? ( {isExpanded ? (
<ChevronDown className="h-4 w-4 text-gray-400 flex-shrink-0" /> <ChevronDown className="h-4 w-4 text-[#0080ff] flex-shrink-0 transition-transform" />
) : ( ) : (
<ChevronRight className="h-4 w-4 text-gray-400 flex-shrink-0" /> <ChevronRight className="h-4 w-4 text-[#334060] flex-shrink-0 transition-transform" />
)} )}
</div> </div>
<p className="text-sm text-gray-400 mt-2 line-clamp-2"> <p className="text-sm text-[#556080] mt-2 line-clamp-2">
{tool.description} {tool.description}
</p> </p>
</button> </button>
{isExpanded && tool.parameters && ( {isExpanded && tool.parameters && (
<div className="border-t border-gray-800 p-4"> <div className="border-t border-[#1a1a3e] p-4 animate-fade-in">
<p className="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wider"> <p className="text-[10px] text-[#334060] mb-2 font-semibold uppercase tracking-wider">
Parameter Schema Parameter Schema
</p> </p>
<pre className="text-xs text-gray-300 bg-gray-950 rounded-lg p-3 overflow-x-auto max-h-64 overflow-y-auto"> <pre className="text-xs text-[#8892a8] rounded-xl p-3 overflow-x-auto max-h-64 overflow-y-auto" style={{ background: 'rgba(5,5,16,0.8)' }}>
{JSON.stringify(tool.parameters, null, 2)} {JSON.stringify(tool.parameters, null, 2)}
</pre> </pre>
</div> </div>
@ -135,49 +135,38 @@ export default function Tools() {
{/* CLI Tools Section */} {/* CLI Tools Section */}
{filteredCli.length > 0 && ( {filteredCli.length > 0 && (
<div> <div className="animate-slide-in-up" style={{ animationDelay: '200ms' }}>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Terminal className="h-5 w-5 text-green-400" /> <Terminal className="h-5 w-5 text-[#00e68a]" />
<h2 className="text-base font-semibold text-white"> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">
CLI Tools ({filteredCli.length}) CLI Tools ({filteredCli.length})
</h2> </h2>
</div> </div>
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"> <div className="glass-card overflow-hidden">
<table className="w-full text-sm"> <table className="table-electric">
<thead> <thead>
<tr className="border-b border-gray-800"> <tr>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-left">Name</th>
Name <th className="text-left">Path</th>
</th> <th className="text-left">Version</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium"> <th className="text-left">Category</th>
Path
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Version
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Category
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredCli.map((tool) => ( {filteredCli.map((tool) => (
<tr <tr key={tool.name}>
key={tool.name} <td className="px-4 py-3 text-white font-medium text-sm">
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="px-4 py-3 text-white font-medium">
{tool.name} {tool.name}
</td> </td>
<td className="px-4 py-3 text-gray-400 font-mono text-xs truncate max-w-[200px]"> <td className="px-4 py-3 text-[#556080] font-mono text-xs truncate max-w-[200px]">
{tool.path} {tool.path}
</td> </td>
<td className="px-4 py-3 text-gray-400"> <td className="px-4 py-3 text-[#556080] text-sm">
{tool.version ?? '-'} {tool.version ?? '-'}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-800 text-gray-300 capitalize"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold capitalize border border-[#1a1a3e] text-[#8892a8]" style={{ background: 'rgba(0,128,255,0.06)' }}>
{tool.category} {tool.category}
</span> </span>
</td> </td>

View File

@ -3,6 +3,8 @@ import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import path from "path"; import path from "path";
// Build-only config. The web dashboard is served by the Rust gateway
// via rust-embed. Run `npm run build` then `cargo build` to update.
export default defineConfig({ export default defineConfig({
base: "/_app/", base: "/_app/",
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
@ -14,24 +16,4 @@ export default defineConfig({
build: { build: {
outDir: "dist", outDir: "dist",
}, },
server: {
proxy: {
"/health": {
target: "http://localhost:5555",
changeOrigin: true,
},
"/pair": {
target: "http://localhost:5555",
changeOrigin: true,
},
"/api": {
target: "http://localhost:5555",
changeOrigin: true,
},
"/ws": {
target: "ws://localhost:5555",
ws: true,
},
},
},
}); });