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 name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/_app/logo.png" />
<title>ZeroClaw</title>
</head>
<body>

View File

@ -5,9 +5,7 @@
"license": "(MIT OR Apache-2.0)",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
"build": "tsc -b && vite build"
},
"dependencies": {
"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 (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="bg-gray-900 rounded-xl p-8 w-full max-w-md border border-gray-800">
<div className="text-center mb-6">
<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="min-h-screen flex items-center justify-center" style={{ background: 'radial-gradient(ellipse at center, #0a0a20 0%, #050510 70%)' }}>
{/* Ambient glow */}
<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%)' }} />
<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>
<form onSubmit={handleSubmit}>
<input
@ -59,19 +71,24 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
value={code}
onChange={(e) => setCode(e.target.value)}
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}
autoFocus
/>
{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
type="submit"
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>
</form>
</div>
@ -99,8 +116,11 @@ function AppContent() {
if (loading) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<p className="text-gray-400">Connecting...</p>
<div className="min-h-screen flex items-center justify-center" style={{ background: 'radial-gradient(ellipse at center, #0a0a20 0%, #050510 70%)' }}>
<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>
);
}

View File

@ -30,17 +30,17 @@ export default function Header() {
};
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 */}
<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 */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
{/* Language switcher */}
<button
type="button"
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'}
</button>
@ -49,9 +49,9 @@ export default function Header() {
<button
type="button"
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>
</button>
</div>

View File

@ -4,7 +4,7 @@ import Header from '@/components/layout/Header';
export default function Layout() {
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 */}
<Sidebar />

View File

@ -28,38 +28,59 @@ const navItems = [
export default function Sidebar() {
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 */}
<div className="flex items-center gap-2 px-5 py-5 border-b border-gray-800">
<div className="h-8 w-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold text-sm">
ZC
</div>
<span className="text-lg font-semibold text-white tracking-wide">
<div className="flex items-center gap-3 px-4 py-4 border-b border-[#1a1a3e]/50">
<img
src="/_app/logo.png"
alt="ZeroClaw"
className="h-10 w-10 rounded-xl object-cover animate-pulse-glow"
/>
<span className="text-lg font-bold text-gradient-blue tracking-wide">
ZeroClaw
</span>
</div>
{/* Navigation */}
<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
key={to}
to={to}
end={to === '/'}
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
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-800 hover:text-white',
? 'text-white shadow-[0_0_15px_rgba(0,128,255,0.2)]'
: 'text-[#556080] hover:text-white hover:bg-[#0080ff08]',
].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" />
<span>{t(labelKey)}</span>
{({ isActive }) => (
<>
<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>
))}
</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>
);
}

View File

@ -1,33 +1,37 @@
@import "tailwindcss";
/*
* ZeroClaw Dark Theme
* Dark-mode by default with gray cards and blue/green accents.
* ZeroClaw Electric Blue Theme
* Dark-mode with electric blue accents, glassmorphism, and animations.
*/
@theme {
--color-bg-primary: #0a0a0f;
--color-bg-secondary: #12121a;
--color-bg-card: #1a1a2e;
--color-bg-card-hover: #22223a;
--color-bg-input: #14141f;
--color-bg-primary: #050510;
--color-bg-secondary: #0a0a1a;
--color-bg-card: #0d0d20;
--color-bg-card-hover: #141430;
--color-bg-input: #0a0a18;
--color-border-default: #2a2a3e;
--color-border-subtle: #1e1e30;
--color-border-default: #1a1a3e;
--color-border-subtle: #12122a;
--color-accent-blue: #3b82f6;
--color-accent-blue-hover: #2563eb;
--color-accent-green: #10b981;
--color-accent-green-hover: #059669;
--color-accent-blue: #0080ff;
--color-accent-blue-hover: #0066cc;
--color-accent-cyan: #00d4ff;
--color-accent-green: #00e68a;
--color-accent-green-hover: #00cc7a;
--color-text-primary: #e2e8f0;
--color-text-secondary: #94a3b8;
--color-text-muted: #64748b;
--color-text-primary: #e8edf5;
--color-text-secondary: #8892a8;
--color-text-muted: #556080;
--color-status-success: #10b981;
--color-status-warning: #f59e0b;
--color-status-error: #ef4444;
--color-status-info: #3b82f6;
--color-status-success: #00e68a;
--color-status-warning: #ffaa00;
--color-status-error: #ff4466;
--color-status-info: #0080ff;
--color-glow-blue: #0080ff40;
--color-glow-cyan: #00d4ff30;
}
/* Base styles */
@ -54,32 +58,35 @@ body {
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 4px;
background: #1a1a3e;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted);
background: #0080ff60;
}
/* Card utility */
.card {
background-color: var(--color-bg-card);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
background: linear-gradient(135deg, rgba(13, 13, 32, 0.8), rgba(10, 10, 26, 0.6));
border: 1px solid rgba(0, 128, 255, 0.1);
border-radius: 1rem;
backdrop-filter: blur(12px);
transition: all 0.3s ease;
}
.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 */
@ -87,3 +94,236 @@ body {
outline: 2px solid var(--color-accent-blue);
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)]">
{/* Connection status bar */}
{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" />
{error}
</div>
@ -180,45 +180,58 @@ export default function AgentChat() {
{/* Messages area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<Bot className="h-12 w-12 mb-3 text-gray-600" />
<p className="text-lg font-medium">ZeroClaw Agent</p>
<p className="text-sm mt-1">Send a message to start the conversation</p>
<div className="flex flex-col items-center justify-center h-full text-[#334060] animate-fade-in">
<div className="h-16 w-16 rounded-2xl flex items-center justify-center mb-4 animate-float" style={{ background: 'linear-gradient(135deg, #0080ff15, #0080ff08)' }}>
<Bot className="h-8 w-8 text-[#0080ff]" />
</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>
)}
{messages.map((msg) => (
{messages.map((msg, idx) => (
<div
key={msg.id}
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
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'
? 'bg-blue-600'
: 'bg-gray-700'
? ''
: ''
}`}
style={{
background: msg.role === 'user'
? 'linear-gradient(135deg, #0080ff, #0060cc)'
: 'linear-gradient(135deg, #1a1a3e, #12122a)'
}}
>
{msg.role === 'user' ? (
<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 className="relative max-w-[75%]">
<div
className={`rounded-xl px-4 py-3 ${
className={`rounded-2xl px-4 py-3 ${
msg.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-100 border border-gray-700'
? 'text-white'
: '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-xs mt-1 ${
msg.role === 'user' ? 'text-blue-200' : 'text-gray-500'
className={`text-[10px] mt-1.5 ${
msg.role === 'user' ? 'text-white/50' : 'text-[#334060]'
}`}
>
{msg.timestamp.toLocaleTimeString()}
@ -227,12 +240,12 @@ export default function AgentChat() {
<button
onClick={() => handleCopy(msg.id, msg.content)}
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 ? (
<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>
</div>
@ -240,17 +253,16 @@ export default function AgentChat() {
))}
{typing && (
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center">
<Bot className="h-4 w-4 text-white" />
<div className="flex items-start gap-3 animate-fade-in">
<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-[#0080ff]" />
</div>
<div className="bg-gray-800 border border-gray-700 rounded-xl px-4 py-3">
<div className="flex items-center gap-1">
<span className="w-2 h-2 bg-gray-400 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-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
<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.5">
<span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
<p className="text-xs text-gray-500 mt-1">Typing...</p>
</div>
</div>
)}
@ -259,9 +271,9 @@ export default function AgentChat() {
</div>
{/* 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-1 relative">
<div className="flex-1">
<textarea
ref={inputRef}
rows={1}
@ -270,25 +282,25 @@ export default function AgentChat() {
onKeyDown={handleKeyDown}
placeholder={connected ? 'Type a message...' : 'Connecting...'}
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' }}
/>
</div>
<button
onClick={handleSend}
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" />
</button>
</div>
<div className="flex items-center justify-center mt-2 gap-2">
<span
className={`inline-block h-2 w-2 rounded-full ${
connected ? 'bg-green-500' : 'bg-red-500'
className={`inline-block h-1.5 w-1.5 rounded-full glow-dot ${
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'}
</span>
</div>

View File

@ -18,7 +18,6 @@ export default function Config() {
useEffect(() => {
getConfig()
.then((data) => {
// The API may return either a raw string or a JSON string
setConfig(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
})
.catch((err) => setError(err.message))
@ -49,23 +48,23 @@ export default function Config() {
if (loading) {
return (
<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>
);
}
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 animate-fade-in">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Configuration</h2>
<Settings className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Configuration</h2>
</div>
<button
onClick={handleSave}
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" />
{saving ? 'Saving...' : 'Save'}
@ -73,13 +72,13 @@ export default function Config() {
</div>
{/* Sensitive fields note */}
<div className="flex items-start gap-3 bg-yellow-900/20 border border-yellow-700/40 rounded-lg p-4">
<ShieldAlert className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
<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-[#ffaa00] flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-yellow-300 font-medium">
<p className="text-sm text-[#ffaa00] font-medium">
Sensitive fields are masked
</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
masked field, replace the entire masked value with your new value.
</p>
@ -88,27 +87,27 @@ export default function Config() {
{/* Success message */}
{success && (
<div className="flex items-center gap-2 bg-green-900/30 border border-green-700 rounded-lg p-3">
<CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" />
<span className="text-sm text-green-300">{success}</span>
<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-[#00e68a] flex-shrink-0" />
<span className="text-sm text-[#00e68a]">{success}</span>
</div>
)}
{/* Error message */}
{error && (
<div className="flex items-center gap-2 bg-red-900/30 border border-red-700 rounded-lg p-3">
<AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0" />
<span className="text-sm text-red-300">{error}</span>
<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-[#ff4466] flex-shrink-0" />
<span className="text-sm text-[#ff6680]">{error}</span>
</div>
)}
{/* Config Editor */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-800/50">
<span className="text-xs text-gray-400 font-medium uppercase tracking-wider">
<div className="glass-card overflow-hidden">
<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-[10px] text-[#334060] font-semibold uppercase tracking-wider">
TOML Configuration
</span>
<span className="text-xs text-gray-500">
<span className="text-[10px] text-[#334060]">
{config.split('\n').length} lines
</span>
</div>
@ -116,8 +115,8 @@ export default function Config() {
value={config}
onChange={(e) => setConfig(e.target.value)}
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"
style={{ tabSize: 4 }}
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={{ background: 'rgba(5,5,16,0.8)', tabSize: 4 }}
/>
</div>
</div>

View File

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

View File

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

View File

@ -28,13 +28,13 @@ function healthColor(status: string): string {
switch (status.toLowerCase()) {
case 'ok':
case 'healthy':
return 'bg-green-500';
return 'bg-[#00e68a]';
case 'warn':
case 'warning':
case 'degraded':
return 'bg-yellow-500';
return 'bg-[#ffaa00]';
default:
return 'bg-red-500';
return 'bg-[#ff4466]';
}
}
@ -42,13 +42,13 @@ function healthBorder(status: string): string {
switch (status.toLowerCase()) {
case 'ok':
case 'healthy':
return 'border-green-500/30';
return 'border-[#00e68a30]';
case 'warn':
case 'warning':
case 'degraded':
return 'border-yellow-500/30';
return 'border-[#ffaa0030]';
default:
return 'border-red-500/30';
return 'border-[#ff446630]';
}
}
@ -68,8 +68,8 @@ export default function Dashboard() {
if (error) {
return (
<div className="p-6">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
<div className="p-6 animate-fade-in">
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load dashboard: {error}
</div>
</div>
@ -79,7 +79,7 @@ export default function Dashboard() {
if (!status || !cost) {
return (
<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>
);
}
@ -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);
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 animate-fade-in">
{/* Status Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<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-blue-600/20 rounded-lg">
<Cpu className="h-5 w-5 text-blue-400" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
{[
{ icon: Cpu, color: '#0080ff', bg: '#0080ff15', label: 'Provider / Model', value: status.provider ?? 'Unknown', sub: status.model },
{ icon: Clock, color: '#00e68a', bg: '#00e68a15', label: 'Uptime', value: formatUptime(status.uptime_seconds), sub: 'Since last restart' },
{ 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>
<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>
<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 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 */}
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-2 mb-4">
<DollarSign className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Cost Overview</h2>
<div className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-2 mb-5">
<DollarSign className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Cost Overview</h2>
</div>
<div className="space-y-4">
{[
{ label: 'Session', value: cost.session_cost_usd, color: 'bg-blue-500' },
{ label: 'Daily', value: cost.daily_cost_usd, color: 'bg-green-500' },
{ label: 'Monthly', value: cost.monthly_cost_usd, color: 'bg-purple-500' },
{ label: 'Session', value: cost.session_cost_usd, color: '#0080ff' },
{ label: 'Daily', value: cost.daily_cost_usd, color: '#00e68a' },
{ label: 'Monthly', value: cost.monthly_cost_usd, color: '#a855f7' },
].map(({ label, value, color }) => (
<div key={label}>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-400">{label}</span>
<span className="text-white font-medium">{formatUSD(value)}</span>
<div className="flex justify-between text-sm mb-1.5">
<span className="text-[#556080]">{label}</span>
<span className="text-white font-medium font-mono">{formatUSD(value)}</span>
</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
className={`h-full rounded-full ${color}`}
style={{ width: `${Math.max((value / maxCost) * 100, 2)}%` }}
className="h-full rounded-full progress-bar-animated transition-all duration-700 ease-out"
style={{ width: `${Math.max((value / maxCost) * 100, 2)}%`, background: color }}
/>
</div>
</div>
))}
</div>
<div className="mt-4 pt-3 border-t border-gray-800 flex justify-between text-sm">
<span className="text-gray-400">Total Tokens</span>
<span className="text-white">{cost.total_tokens.toLocaleString()}</span>
<div className="mt-5 pt-4 border-t border-[#1a1a3e]/50 flex justify-between text-sm">
<span className="text-[#556080]">Total Tokens</span>
<span className="text-white font-mono">{cost.total_tokens.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span className="text-gray-400">Requests</span>
<span className="text-white">{cost.request_count.toLocaleString()}</span>
<span className="text-[#556080]">Requests</span>
<span className="text-white font-mono">{cost.request_count.toLocaleString()}</span>
</div>
</div>
{/* Active Channels */}
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-2 mb-4">
<Radio className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Active Channels</h2>
<div className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-2 mb-5">
<Radio className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Active Channels</h2>
</div>
<div className="space-y-2">
{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]) => (
<div
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">
<span
className={`inline-block h-2.5 w-2.5 rounded-full ${
active ? 'bg-green-500' : 'bg-gray-500'
className={`inline-block h-2 w-2 rounded-full glow-dot ${
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'}
</span>
</div>
@ -215,29 +180,30 @@ export default function Dashboard() {
</div>
{/* Health Grid */}
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
<div className="flex items-center gap-2 mb-4">
<Activity className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Component Health</h2>
<div className="glass-card p-5 animate-slide-in-up">
<div className="flex items-center gap-2 mb-5">
<Activity className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Component Health</h2>
</div>
<div className="grid grid-cols-2 gap-3">
{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]) => (
<div
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">
<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">
{name}
</span>
</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 && (
<p className="text-xs text-yellow-400 mt-1">
<p className="text-xs text-[#ffaa00] mt-1">
Restarts: {comp.restart_count}
</p>
)}

View File

@ -13,33 +13,33 @@ import { runDoctor } from '@/lib/api';
function severityIcon(severity: DiagResult['severity']) {
switch (severity) {
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':
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':
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 {
switch (severity) {
case 'ok':
return 'border-green-700/40';
return 'border-[#00e68a20]';
case 'warn':
return 'border-yellow-700/40';
return 'border-[#ffaa0020]';
case 'error':
return 'border-red-700/40';
return 'border-[#ff446620]';
}
}
function severityBg(severity: DiagResult['severity']): string {
switch (severity) {
case 'ok':
return 'bg-green-900/10';
return 'rgba(0,230,138,0.04)';
case 'warn':
return 'bg-yellow-900/10';
return 'rgba(255,170,0,0.04)';
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 warnCount = results?.filter((r) => r.severity === 'warn').length ?? 0;
const errorCount = results?.filter((r) => r.severity === 'error').length ?? 0;
// Group by category
const grouped =
results?.reduce<Record<string, DiagResult[]>>((acc, item) => {
const key = item.category;
@ -77,17 +75,17 @@ export default function Doctor() {
}, {}) ?? {};
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 animate-fade-in">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Stethoscope className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Diagnostics</h2>
<Stethoscope className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Diagnostics</h2>
</div>
<button
onClick={handleRun}
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 ? (
<>
@ -105,17 +103,17 @@ export default function Doctor() {
{/* 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}
</div>
)}
{/* Loading spinner */}
{loading && (
<div className="flex flex-col items-center justify-center py-16">
<Loader2 className="h-10 w-10 text-blue-500 animate-spin mb-4" />
<p className="text-gray-400">Running diagnostics...</p>
<p className="text-sm text-gray-500 mt-1">
<div className="flex flex-col items-center justify-center py-16 animate-fade-in">
<div className="h-12 w-12 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin mb-4" />
<p className="text-[#8892a8]">Running diagnostics...</p>
<p className="text-sm text-[#334060] mt-1">
This may take a few seconds.
</p>
</div>
@ -125,29 +123,29 @@ export default function Doctor() {
{results && !loading && (
<>
{/* 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">
<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">
{okCount} <span className="text-gray-400 font-normal">ok</span>
{okCount} <span className="text-[#556080] font-normal">ok</span>
</span>
</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">
<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">
{warnCount}{' '}
<span className="text-gray-400 font-normal">
<span className="text-[#556080] font-normal">
warning{warnCount !== 1 ? 's' : ''}
</span>
</span>
</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">
<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">
{errorCount}{' '}
<span className="text-gray-400 font-normal">
<span className="text-[#556080] font-normal">
error{errorCount !== 1 ? 's' : ''}
</span>
</span>
@ -156,15 +154,15 @@ export default function Doctor() {
{/* Overall indicator */}
<div className="ml-auto">
{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
</span>
) : 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
</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
</span>
)}
@ -174,23 +172,22 @@ export default function Doctor() {
{/* Grouped Results */}
{Object.entries(grouped)
.sort(([a], [b]) => a.localeCompare(b))
.map(([category, items]) => (
<div key={category}>
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 capitalize">
.map(([category, items], catIdx) => (
<div key={category} className="animate-slide-in-up" style={{ animationDelay: `${(catIdx + 1) * 100}ms` }}>
<h3 className="text-[10px] font-semibold text-[#334060] uppercase tracking-wider mb-3 capitalize">
{category}
</h3>
<div className="space-y-2">
<div className="space-y-2 stagger-children">
{items.map((result, idx) => (
<div
key={`${category}-${idx}`}
className={`flex items-start gap-3 rounded-lg border p-3 ${severityBorder(
result.severity,
)} ${severityBg(result.severity)}`}
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`}
style={{ background: severityBg(result.severity) }}
>
{severityIcon(result.severity)}
<div className="min-w-0">
<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}
</p>
</div>
@ -204,10 +201,12 @@ export default function Doctor() {
{/* Empty state */}
{!results && !loading && !error && (
<div className="flex flex-col items-center justify-center py-16 text-gray-500">
<Stethoscope className="h-12 w-12 text-gray-600 mb-4" />
<p className="text-lg font-medium">System Diagnostics</p>
<p className="text-sm mt-1">
<div className="flex flex-col items-center justify-center py-16 text-[#334060] animate-fade-in">
<div className="h-16 w-16 rounded-2xl flex items-center justify-center mb-4 animate-float" style={{ background: 'linear-gradient(135deg, #0080ff15, #0080ff08)' }}>
<Stethoscope className="h-8 w-8 text-[#0080ff]" />
</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.
</p>
</div>

View File

@ -9,19 +9,22 @@ function statusBadge(status: Integration['status']) {
return {
icon: Check,
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':
return {
icon: Zap,
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':
return {
icon: Clock,
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) {
return (
<div className="p-6">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
<div className="p-6 animate-fade-in">
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load integrations: {error}
</div>
</div>
@ -70,17 +73,17 @@ export default function Integrations() {
if (loading) {
return (
<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>
);
}
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 animate-fade-in">
{/* Header */}
<div className="flex items-center gap-2">
<Puzzle className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">
<Puzzle className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Integrations ({integrations.length})
</h2>
</div>
@ -91,11 +94,12 @@ export default function Integrations() {
<button
key={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
? 'bg-blue-600 text-white'
: 'bg-gray-900 text-gray-400 border border-gray-700 hover:bg-gray-800 hover:text-white'
? 'text-white shadow-[0_0_15px_rgba(0,128,255,0.2)]'
: 'text-[#556080] border border-[#1a1a3e] hover:text-white hover:border-[#0080ff40]'
}`}
style={activeCategory === cat ? { background: 'linear-gradient(135deg, #0080ff, #0066cc)' } : {}}
>
{cat}
</button>
@ -104,38 +108,39 @@ export default function Integrations() {
{/* Grouped Integration Cards */}
{Object.keys(grouped).length === 0 ? (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
<Puzzle className="h-10 w-10 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No integrations found.</p>
<div className="glass-card p-8 text-center">
<Puzzle className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
<p className="text-[#556080]">No integrations found.</p>
</div>
) : (
Object.entries(grouped)
.sort(([a], [b]) => a.localeCompare(b))
.map(([category, items]) => (
<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}
</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) => {
const badge = statusBadge(integration.status);
const BadgeIcon = badge.icon;
return (
<div
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="min-w-0">
<h4 className="text-sm font-semibold text-white truncate">
{integration.name}
</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}
</p>
</div>
<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" />
{badge.label}

View File

@ -14,24 +14,24 @@ function formatTimestamp(ts?: string): string {
return new Date(ts).toLocaleTimeString();
}
function eventTypeBadgeColor(type: string): string {
function eventTypeBadgeColor(type: string): { classes: string; bg: string } {
switch (type.toLowerCase()) {
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 '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_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 '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 '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:
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,
};
setEntries((prev) => {
// Cap at 500 entries for performance
const next = [...prev, entry];
return next.length > 500 ? next.slice(-500) : next;
});
@ -112,7 +111,6 @@ export default function Logs() {
setAutoScroll(true);
};
// Collect all event types for filter checkboxes
const allTypes = Array.from(new Set(entries.map((e) => e.event.type))).sort();
const toggleTypeFilter = (type: string) => {
@ -135,21 +133,21 @@ export default function Logs() {
return (
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
{/* 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">
<Activity className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Live Logs</h2>
<Activity className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Live Logs</h2>
<div className="flex items-center gap-2 ml-2">
<span
className={`inline-block h-2 w-2 rounded-full ${
connected ? 'bg-green-500' : 'bg-red-500'
className={`inline-block h-1.5 w-1.5 rounded-full glow-dot ${
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'}
</span>
</div>
<span className="text-xs text-gray-500 ml-2">
<span className="text-[10px] text-[#334060] ml-2 font-mono">
{filteredEntries.length} events
</span>
</div>
@ -158,11 +156,16 @@ export default function Logs() {
{/* Pause/Resume */}
<button
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
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-yellow-600 hover:bg-yellow-700 text-white'
? 'text-white shadow-[0_0_15px_rgba(0,230,138,0.2)]'
: '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 ? (
<>
@ -179,7 +182,7 @@ export default function Logs() {
{!autoScroll && (
<button
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" />
Jump to bottom
@ -190,9 +193,9 @@ export default function Logs() {
{/* Event type filters */}
{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">
<Filter className="h-4 w-4 text-gray-500 flex-shrink-0" />
<span className="text-xs text-gray-500 flex-shrink-0">Filter:</span>
<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-3.5 w-3.5 text-[#334060] flex-shrink-0" />
<span className="text-[10px] text-[#334060] flex-shrink-0 uppercase tracking-wider">Filter:</span>
{allTypes.map((type) => (
<label
key={type}
@ -202,15 +205,15 @@ export default function Logs() {
type="checkbox"
checked={typeFilters.has(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>
))}
{typeFilters.size > 0 && (
<button
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
</button>
@ -222,11 +225,11 @@ export default function Logs() {
<div
ref={containerRef}
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 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<Activity className="h-10 w-10 text-gray-600 mb-3" />
<div className="flex flex-col items-center justify-center h-full text-[#334060] animate-fade-in">
<Activity className="h-10 w-10 text-[#1a1a3e] mb-3" />
<p className="text-sm">
{paused
? 'Log streaming is paused.'
@ -236,6 +239,7 @@ export default function Logs() {
) : (
filteredEntries.map((entry) => {
const { event } = entry;
const badge = eventTypeBadgeColor(event.type);
const detail =
event.message ??
event.content ??
@ -251,20 +255,19 @@ export default function Logs() {
return (
<div
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">
<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)}
</span>
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border capitalize flex-shrink-0 ${eventTypeBadgeColor(
event.type,
)}`}
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border capitalize flex-shrink-0 ${badge.classes}`}
style={{ background: badge.bg }}
>
{event.type}
</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)}
</p>
</div>

View File

@ -96,8 +96,8 @@ export default function Memory() {
if (error && entries.length === 0) {
return (
<div className="p-6">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
<div className="p-6 animate-fade-in">
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load memory: {error}
</div>
</div>
@ -105,18 +105,18 @@ export default function Memory() {
}
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 animate-fade-in">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Brain className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">
<Brain className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Memory ({entries.length})
</h2>
</div>
<button
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" />
Add Memory
@ -126,22 +126,22 @@ export default function Memory() {
{/* Search and Filter */}
<div className="flex flex-col sm:flex-row gap-3">
<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
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown}
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 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
value={categoryFilter}
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>
{categories.map((cat) => (
@ -153,7 +153,7 @@ export default function Memory() {
</div>
<button
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
</button>
@ -161,15 +161,15 @@ export default function Memory() {
{/* Error banner (non-fatal) */}
{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}
</div>
)}
{/* Add Memory Form Modal */}
{showForm && (
<div className="fixed inset-0 bg-black/60 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="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
<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">
<h3 className="text-lg font-semibold text-white">Add Memory</h3>
<button
@ -177,45 +177,45 @@ export default function Memory() {
setShowForm(false);
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" />
</button>
</div>
{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}
</div>
)}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Key <span className="text-red-400">*</span>
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Key <span className="text-[#ff4466]">*</span>
</label>
<input
type="text"
value={formKey}
onChange={(e) => setFormKey(e.target.value)}
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>
<label className="block text-sm font-medium text-gray-300 mb-1">
Content <span className="text-red-400">*</span>
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
Content <span className="text-[#ff4466]">*</span>
</label>
<textarea
value={formContent}
onChange={(e) => setFormContent(e.target.value)}
placeholder="Memory content..."
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>
<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)
</label>
<input
@ -223,7 +223,7 @@ export default function Memory() {
value={formCategory}
onChange={(e) => setFormCategory(e.target.value)}
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>
@ -234,14 +234,14 @@ export default function Memory() {
setShowForm(false);
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
</button>
<button
onClick={handleAdd}
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'}
</button>
@ -253,70 +253,57 @@ export default function Memory() {
{/* Memory Table */}
{loading ? (
<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>
) : entries.length === 0 ? (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
<Brain className="h-10 w-10 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No memory entries found.</p>
<div className="glass-card p-8 text-center">
<Brain className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
<p className="text-[#556080]">No memory entries found.</p>
</div>
) : (
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto">
<table className="w-full text-sm">
<div className="glass-card overflow-x-auto">
<table className="table-electric">
<thead>
<tr className="border-b border-gray-800">
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Key
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Content
</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>
<th className="text-left">Key</th>
<th className="text-left">Content</th>
<th className="text-left">Category</th>
<th className="text-left">Timestamp</th>
<th className="text-right">Actions</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr
key={entry.id}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<tr key={entry.id}>
<td className="px-4 py-3 text-white font-medium font-mono text-xs">
{entry.key}
</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}>
{truncate(entry.content, 80)}
</span>
</td>
<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}
</span>
</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)}
</td>
<td className="px-4 py-3 text-right">
{confirmDelete === entry.key ? (
<div className="flex items-center justify-end gap-2">
<span className="text-xs text-red-400">Delete?</span>
<div className="flex items-center justify-end gap-2 animate-fade-in">
<span className="text-xs text-[#ff4466]">Delete?</span>
<button
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
</button>
<button
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
</button>
@ -324,7 +311,7 @@ export default function Memory() {
) : (
<button
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" />
</button>

View File

@ -42,8 +42,8 @@ export default function Tools() {
if (error) {
return (
<div className="p-6">
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
<div className="p-6 animate-fade-in">
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
Failed to load tools: {error}
</div>
</div>
@ -53,75 +53,75 @@ export default function Tools() {
if (loading) {
return (
<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>
);
}
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 animate-fade-in">
{/* Search */}
<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
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
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>
{/* Agent Tools Grid */}
<div>
<div className="flex items-center gap-2 mb-4">
<Wrench className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">
<Wrench className="h-5 w-5 text-[#0080ff]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
Agent Tools ({filtered.length})
</h2>
</div>
{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) => {
const isExpanded = expandedTool === tool.name;
return (
<div
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
onClick={() =>
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-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">
{tool.name}
</h3>
</div>
{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>
<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}
</p>
</button>
{isExpanded && tool.parameters && (
<div className="border-t border-gray-800 p-4">
<p className="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wider">
<div className="border-t border-[#1a1a3e] p-4 animate-fade-in">
<p className="text-[10px] text-[#334060] mb-2 font-semibold uppercase tracking-wider">
Parameter Schema
</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)}
</pre>
</div>
@ -135,49 +135,38 @@ export default function Tools() {
{/* CLI Tools Section */}
{filteredCli.length > 0 && (
<div>
<div className="animate-slide-in-up" style={{ animationDelay: '200ms' }}>
<div className="flex items-center gap-2 mb-4">
<Terminal className="h-5 w-5 text-green-400" />
<h2 className="text-base font-semibold text-white">
<Terminal className="h-5 w-5 text-[#00e68a]" />
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
CLI Tools ({filteredCli.length})
</h2>
</div>
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<table className="w-full text-sm">
<div className="glass-card overflow-hidden">
<table className="table-electric">
<thead>
<tr className="border-b border-gray-800">
<th className="text-left px-4 py-3 text-gray-400 font-medium">
Name
</th>
<th className="text-left px-4 py-3 text-gray-400 font-medium">
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>
<th className="text-left">Name</th>
<th className="text-left">Path</th>
<th className="text-left">Version</th>
<th className="text-left">Category</th>
</tr>
</thead>
<tbody>
{filteredCli.map((tool) => (
<tr
key={tool.name}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="px-4 py-3 text-white font-medium">
<tr key={tool.name}>
<td className="px-4 py-3 text-white font-medium text-sm">
{tool.name}
</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}
</td>
<td className="px-4 py-3 text-gray-400">
<td className="px-4 py-3 text-[#556080] text-sm">
{tool.version ?? '-'}
</td>
<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}
</span>
</td>

View File

@ -3,6 +3,8 @@ import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
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({
base: "/_app/",
plugins: [react(), tailwindcss()],
@ -14,24 +16,4 @@ export default defineConfig({
build: {
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,
},
},
},
});