Compare commits
5 Commits
master
...
web-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8144938ac5 | ||
|
|
7e0570abd6 | ||
|
|
0931b140cf | ||
|
|
e46a334f6d | ||
|
|
76a6ab5b12 |
@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="color-scheme" content="dark" />
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<link rel="icon" type="image/png" href="/_app/logo.png" />
|
||||||
<title>ZeroClaw</title>
|
<title>ZeroClaw</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -5,9 +5,7 @@
|
|||||||
"license": "(MIT OR Apache-2.0)",
|
"license": "(MIT OR Apache-2.0)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"build": "tsc -b && vite build"
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
|
|||||||
BIN
web/public/logo.png
Normal file
BIN
web/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@ -47,11 +47,23 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center" style={{ background: 'radial-gradient(ellipse at center, #0a0a20 0%, #050510 70%)' }}>
|
||||||
<div className="bg-gray-900 rounded-xl p-8 w-full max-w-md border border-gray-800">
|
{/* Ambient glow */}
|
||||||
<div className="text-center mb-6">
|
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] rounded-full opacity-20 pointer-events-none" style={{ background: 'radial-gradient(circle, #0080ff 0%, transparent 70%)' }} />
|
||||||
<h1 className="text-2xl font-bold text-white mb-2">ZeroClaw</h1>
|
|
||||||
<p className="text-gray-400">Enter the pairing code from your terminal</p>
|
<div className="relative glass-card p-8 w-full max-w-md animate-fade-in-scale">
|
||||||
|
{/* Top glow accent */}
|
||||||
|
<div className="absolute -top-px left-1/4 right-1/4 h-px" style={{ background: 'linear-gradient(90deg, transparent, #0080ff, transparent)' }} />
|
||||||
|
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<img
|
||||||
|
src="/_app/logo.png"
|
||||||
|
alt="ZeroClaw"
|
||||||
|
className="h-20 w-20 rounded-2xl object-cover mx-auto mb-4 animate-float"
|
||||||
|
style={{ boxShadow: '0 0 30px rgba(0,128,255,0.3)' }}
|
||||||
|
/>
|
||||||
|
<h1 className="text-2xl font-bold text-gradient-blue mb-2">ZeroClaw</h1>
|
||||||
|
<p className="text-[#556080] text-sm">Enter the pairing code from your terminal</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<input
|
<input
|
||||||
@ -59,19 +71,24 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise<void> })
|
|||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
placeholder="6-digit code"
|
placeholder="6-digit code"
|
||||||
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-center text-2xl tracking-widest focus:outline-none focus:border-blue-500 mb-4"
|
className="input-electric w-full px-4 py-4 text-center text-2xl tracking-[0.3em] font-medium mb-4"
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-400 text-sm mb-4 text-center">{error}</p>
|
<p className="text-[#ff4466] text-sm mb-4 text-center animate-fade-in">{error}</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || code.length < 6}
|
disabled={loading || code.length < 6}
|
||||||
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded-lg font-medium transition-colors"
|
className="btn-electric w-full py-3.5 text-sm font-semibold tracking-wide"
|
||||||
>
|
>
|
||||||
{loading ? 'Pairing...' : 'Pair'}
|
{loading ? (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
Pairing...
|
||||||
|
</span>
|
||||||
|
) : 'Pair'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -99,8 +116,11 @@ function AppContent() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center" style={{ background: 'radial-gradient(ellipse at center, #0a0a20 0%, #050510 70%)' }}>
|
||||||
<p className="text-gray-400">Connecting...</p>
|
<div className="flex flex-col items-center gap-4 animate-fade-in">
|
||||||
|
<div className="h-10 w-10 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
|
<p className="text-[#556080] text-sm">Connecting...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,17 +30,17 @@ export default function Header() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="h-14 bg-gray-800 border-b border-gray-700 flex items-center justify-between px-6">
|
<header className="h-14 flex items-center justify-between px-6 border-b border-[#1a1a3e]/40 animate-fade-in" style={{ background: 'linear-gradient(90deg, rgba(8,8,24,0.9), rgba(5,5,16,0.9))', backdropFilter: 'blur(12px)' }}>
|
||||||
{/* Page title */}
|
{/* Page title */}
|
||||||
<h1 className="text-lg font-semibold text-white">{pageTitle}</h1>
|
<h1 className="text-lg font-semibold text-white tracking-tight">{pageTitle}</h1>
|
||||||
|
|
||||||
{/* Right-side controls */}
|
{/* Right-side controls */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
{/* Language switcher */}
|
{/* Language switcher */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleLanguage}
|
onClick={toggleLanguage}
|
||||||
className="px-3 py-1 rounded-md text-sm font-medium border border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
|
className="px-3 py-1 rounded-lg text-xs font-semibold border border-[#1a1a3e] text-[#8892a8] hover:text-white hover:border-[#0080ff40] hover:bg-[#0080ff10] transition-all duration-300"
|
||||||
>
|
>
|
||||||
{locale === 'en' ? 'EN' : 'TR'}
|
{locale === 'en' ? 'EN' : 'TR'}
|
||||||
</button>
|
</button>
|
||||||
@ -49,9 +49,9 @@ export default function Header() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs text-[#8892a8] hover:text-[#ff4466] hover:bg-[#ff446610] transition-all duration-300"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-3.5 w-3.5" />
|
||||||
<span>{t('auth.logout')}</span>
|
<span>{t('auth.logout')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Header from '@/components/layout/Header';
|
|||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 text-white">
|
<div className="min-h-screen text-white" style={{ background: 'linear-gradient(135deg, #050510 0%, #080818 50%, #050510 100%)' }}>
|
||||||
{/* Fixed sidebar */}
|
{/* Fixed sidebar */}
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
|
|||||||
@ -28,38 +28,59 @@ const navItems = [
|
|||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<aside className="fixed top-0 left-0 h-screen w-60 bg-gray-900 flex flex-col border-r border-gray-800">
|
<aside className="fixed top-0 left-0 h-screen w-60 flex flex-col" style={{ background: 'linear-gradient(180deg, #080818 0%, #050510 100%)' }}>
|
||||||
|
{/* Glow line on right edge */}
|
||||||
|
<div className="sidebar-glow-line" />
|
||||||
|
|
||||||
{/* Logo / Title */}
|
{/* Logo / Title */}
|
||||||
<div className="flex items-center gap-2 px-5 py-5 border-b border-gray-800">
|
<div className="flex items-center gap-3 px-4 py-4 border-b border-[#1a1a3e]/50">
|
||||||
<div className="h-8 w-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold text-sm">
|
<img
|
||||||
ZC
|
src="/_app/logo.png"
|
||||||
</div>
|
alt="ZeroClaw"
|
||||||
<span className="text-lg font-semibold text-white tracking-wide">
|
className="h-10 w-10 rounded-xl object-cover animate-pulse-glow"
|
||||||
|
/>
|
||||||
|
<span className="text-lg font-bold text-gradient-blue tracking-wide">
|
||||||
ZeroClaw
|
ZeroClaw
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-1">
|
||||||
{navItems.map(({ to, icon: Icon, labelKey }) => (
|
{navItems.map(({ to, icon: Icon, labelKey }, idx) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
end={to === '/'}
|
end={to === '/'}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
[
|
[
|
||||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors',
|
'flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-300 animate-slide-in-left group',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-blue-600 text-white'
|
? 'text-white shadow-[0_0_15px_rgba(0,128,255,0.2)]'
|
||||||
: 'text-gray-300 hover:bg-gray-800 hover:text-white',
|
: 'text-[#556080] hover:text-white hover:bg-[#0080ff08]',
|
||||||
].join(' ')
|
].join(' ')
|
||||||
}
|
}
|
||||||
|
style={({ isActive }) => ({
|
||||||
|
animationDelay: `${idx * 40}ms`,
|
||||||
|
...(isActive ? { background: 'linear-gradient(135deg, rgba(0,128,255,0.15), rgba(0,128,255,0.05))' } : {}),
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5 flex-shrink-0" />
|
{({ isActive }) => (
|
||||||
<span>{t(labelKey)}</span>
|
<>
|
||||||
|
<Icon className={`h-5 w-5 flex-shrink-0 transition-colors duration-300 ${isActive ? 'text-[#0080ff]' : 'group-hover:text-[#0080ff80]'}`} />
|
||||||
|
<span>{t(labelKey)}</span>
|
||||||
|
{isActive && (
|
||||||
|
<div className="ml-auto h-1.5 w-1.5 rounded-full bg-[#0080ff] glow-dot" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-5 py-4 border-t border-[#1a1a3e]/50">
|
||||||
|
<p className="text-[10px] text-[#334060] tracking-wider uppercase">ZeroClaw Runtime</p>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,37 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ZeroClaw Dark Theme
|
* ZeroClaw Electric Blue Theme
|
||||||
* Dark-mode by default with gray cards and blue/green accents.
|
* Dark-mode with electric blue accents, glassmorphism, and animations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-bg-primary: #0a0a0f;
|
--color-bg-primary: #050510;
|
||||||
--color-bg-secondary: #12121a;
|
--color-bg-secondary: #0a0a1a;
|
||||||
--color-bg-card: #1a1a2e;
|
--color-bg-card: #0d0d20;
|
||||||
--color-bg-card-hover: #22223a;
|
--color-bg-card-hover: #141430;
|
||||||
--color-bg-input: #14141f;
|
--color-bg-input: #0a0a18;
|
||||||
|
|
||||||
--color-border-default: #2a2a3e;
|
--color-border-default: #1a1a3e;
|
||||||
--color-border-subtle: #1e1e30;
|
--color-border-subtle: #12122a;
|
||||||
|
|
||||||
--color-accent-blue: #3b82f6;
|
--color-accent-blue: #0080ff;
|
||||||
--color-accent-blue-hover: #2563eb;
|
--color-accent-blue-hover: #0066cc;
|
||||||
--color-accent-green: #10b981;
|
--color-accent-cyan: #00d4ff;
|
||||||
--color-accent-green-hover: #059669;
|
--color-accent-green: #00e68a;
|
||||||
|
--color-accent-green-hover: #00cc7a;
|
||||||
|
|
||||||
--color-text-primary: #e2e8f0;
|
--color-text-primary: #e8edf5;
|
||||||
--color-text-secondary: #94a3b8;
|
--color-text-secondary: #8892a8;
|
||||||
--color-text-muted: #64748b;
|
--color-text-muted: #556080;
|
||||||
|
|
||||||
--color-status-success: #10b981;
|
--color-status-success: #00e68a;
|
||||||
--color-status-warning: #f59e0b;
|
--color-status-warning: #ffaa00;
|
||||||
--color-status-error: #ef4444;
|
--color-status-error: #ff4466;
|
||||||
--color-status-info: #3b82f6;
|
--color-status-info: #0080ff;
|
||||||
|
|
||||||
|
--color-glow-blue: #0080ff40;
|
||||||
|
--color-glow-cyan: #00d4ff30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles */
|
/* Base styles */
|
||||||
@ -54,32 +58,35 @@ body {
|
|||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--color-bg-secondary);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--color-border-default);
|
background: #1a1a3e;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--color-text-muted);
|
background: #0080ff60;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card utility */
|
/* Card utility */
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--color-bg-card);
|
background: linear-gradient(135deg, rgba(13, 13, 32, 0.8), rgba(10, 10, 26, 0.6));
|
||||||
border: 1px solid var(--color-border-default);
|
border: 1px solid rgba(0, 128, 255, 0.1);
|
||||||
border-radius: 0.75rem;
|
border-radius: 1rem;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
background-color: var(--color-bg-card-hover);
|
border-color: rgba(0, 128, 255, 0.25);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 128, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus ring utility */
|
/* Focus ring utility */
|
||||||
@ -87,3 +94,236 @@ body {
|
|||||||
outline: 2px solid var(--color-accent-blue);
|
outline: 2px solid var(--color-accent-blue);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== ANIMATIONS ========== */
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from { opacity: 0; transform: translateX(-16px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from { opacity: 0; transform: translateX(16px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 8px rgba(0, 128, 255, 0.3); }
|
||||||
|
50% { box-shadow: 0 0 20px rgba(0, 128, 255, 0.6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0px); }
|
||||||
|
50% { transform: translateY(-4px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderGlow {
|
||||||
|
0%, 100% { border-color: rgba(0, 128, 255, 0.15); }
|
||||||
|
50% { border-color: rgba(0, 128, 255, 0.35); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation utility classes */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-scale {
|
||||||
|
animation: fadeInScale 0.3s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-left {
|
||||||
|
animation: slideInLeft 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slideInRight 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-up {
|
||||||
|
animation: slideInUp 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-glow {
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-border-glow {
|
||||||
|
animation: borderGlow 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stagger delays for grid children */
|
||||||
|
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
|
||||||
|
.stagger-children > *:nth-child(2) { animation-delay: 60ms; }
|
||||||
|
.stagger-children > *:nth-child(3) { animation-delay: 120ms; }
|
||||||
|
.stagger-children > *:nth-child(4) { animation-delay: 180ms; }
|
||||||
|
.stagger-children > *:nth-child(5) { animation-delay: 240ms; }
|
||||||
|
.stagger-children > *:nth-child(6) { animation-delay: 300ms; }
|
||||||
|
.stagger-children > *:nth-child(7) { animation-delay: 360ms; }
|
||||||
|
.stagger-children > *:nth-child(8) { animation-delay: 420ms; }
|
||||||
|
.stagger-children > *:nth-child(9) { animation-delay: 480ms; }
|
||||||
|
.stagger-children > *:nth-child(10) { animation-delay: 540ms; }
|
||||||
|
|
||||||
|
/* Glass card */
|
||||||
|
.glass-card {
|
||||||
|
background: linear-gradient(135deg, rgba(13, 13, 32, 0.7), rgba(5, 5, 16, 0.5));
|
||||||
|
border: 1px solid rgba(0, 128, 255, 0.12);
|
||||||
|
border-radius: 1rem;
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card:hover {
|
||||||
|
border-color: rgba(0, 128, 255, 0.3);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 128, 255, 0.1), 0 0 0 1px rgba(0, 128, 255, 0.05);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Electric button */
|
||||||
|
.btn-electric {
|
||||||
|
background: linear-gradient(135deg, #0080ff, #0066cc);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-electric:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #0090ff, #0070dd);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 128, 255, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-electric:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-electric:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient text */
|
||||||
|
.text-gradient-blue {
|
||||||
|
background: linear-gradient(135deg, #0080ff, #00d4ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow dot */
|
||||||
|
.glow-dot {
|
||||||
|
box-shadow: 0 0 6px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Electric input */
|
||||||
|
.input-electric {
|
||||||
|
background: rgba(10, 10, 26, 0.8);
|
||||||
|
border: 1px solid rgba(0, 128, 255, 0.15);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-electric:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(0, 128, 255, 0.5);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 128, 255, 0.15), 0 0 20px rgba(0, 128, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-electric::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar animation */
|
||||||
|
.progress-bar-animated {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animated::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
|
.table-electric {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-electric thead tr {
|
||||||
|
border-bottom: 1px solid rgba(0, 128, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-electric thead th {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-electric tbody tr {
|
||||||
|
border-bottom: 1px solid rgba(26, 26, 62, 0.5);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-electric tbody tr:hover {
|
||||||
|
background: rgba(0, 128, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal backdrop */
|
||||||
|
.modal-backdrop {
|
||||||
|
background: rgba(5, 5, 16, 0.8);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar glow line */
|
||||||
|
.sidebar-glow-line {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: linear-gradient(180deg, transparent, rgba(0, 128, 255, 0.3), transparent);
|
||||||
|
}
|
||||||
|
|||||||
@ -171,7 +171,7 @@ export default function AgentChat() {
|
|||||||
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
||||||
{/* Connection status bar */}
|
{/* Connection status bar */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="px-4 py-2 bg-red-900/30 border-b border-red-700 flex items-center gap-2 text-sm text-red-300">
|
<div className="px-4 py-2 bg-[#ff446615] border-b border-[#ff446630] flex items-center gap-2 text-sm text-[#ff6680] animate-fade-in">
|
||||||
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
@ -180,45 +180,58 @@ export default function AgentChat() {
|
|||||||
{/* Messages area */}
|
{/* Messages area */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
<div className="flex flex-col items-center justify-center h-full text-[#334060] animate-fade-in">
|
||||||
<Bot className="h-12 w-12 mb-3 text-gray-600" />
|
<div className="h-16 w-16 rounded-2xl flex items-center justify-center mb-4 animate-float" style={{ background: 'linear-gradient(135deg, #0080ff15, #0080ff08)' }}>
|
||||||
<p className="text-lg font-medium">ZeroClaw Agent</p>
|
<Bot className="h-8 w-8 text-[#0080ff]" />
|
||||||
<p className="text-sm mt-1">Send a message to start the conversation</p>
|
</div>
|
||||||
|
<p className="text-lg font-semibold text-white mb-1">ZeroClaw Agent</p>
|
||||||
|
<p className="text-sm text-[#556080]">Send a message to start the conversation</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{messages.map((msg) => (
|
{messages.map((msg, idx) => (
|
||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
className={`group flex items-start gap-3 ${
|
className={`group flex items-start gap-3 ${
|
||||||
msg.role === 'user' ? 'flex-row-reverse' : ''
|
msg.role === 'user' ? 'flex-row-reverse animate-slide-in-right' : 'animate-slide-in-left'
|
||||||
}`}
|
}`}
|
||||||
|
style={{ animationDelay: `${Math.min(idx * 30, 200)}ms` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
|
className={`flex-shrink-0 w-8 h-8 rounded-xl flex items-center justify-center ${
|
||||||
msg.role === 'user'
|
msg.role === 'user'
|
||||||
? 'bg-blue-600'
|
? ''
|
||||||
: 'bg-gray-700'
|
: ''
|
||||||
}`}
|
}`}
|
||||||
|
style={{
|
||||||
|
background: msg.role === 'user'
|
||||||
|
? 'linear-gradient(135deg, #0080ff, #0060cc)'
|
||||||
|
: 'linear-gradient(135deg, #1a1a3e, #12122a)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{msg.role === 'user' ? (
|
{msg.role === 'user' ? (
|
||||||
<User className="h-4 w-4 text-white" />
|
<User className="h-4 w-4 text-white" />
|
||||||
) : (
|
) : (
|
||||||
<Bot className="h-4 w-4 text-white" />
|
<Bot className="h-4 w-4 text-[#0080ff]" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative max-w-[75%]">
|
<div className="relative max-w-[75%]">
|
||||||
<div
|
<div
|
||||||
className={`rounded-xl px-4 py-3 ${
|
className={`rounded-2xl px-4 py-3 ${
|
||||||
msg.role === 'user'
|
msg.role === 'user'
|
||||||
? 'bg-blue-600 text-white'
|
? 'text-white'
|
||||||
: 'bg-gray-800 text-gray-100 border border-gray-700'
|
: 'text-[#e8edf5] border border-[#1a1a3e]'
|
||||||
}`}
|
}`}
|
||||||
|
style={{
|
||||||
|
background: msg.role === 'user'
|
||||||
|
? 'linear-gradient(135deg, #0080ff, #0066cc)'
|
||||||
|
: 'linear-gradient(135deg, rgba(13,13,32,0.8), rgba(10,10,26,0.6))'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-sm whitespace-pre-wrap break-words">{msg.content}</p>
|
<p className="text-sm whitespace-pre-wrap break-words">{msg.content}</p>
|
||||||
<p
|
<p
|
||||||
className={`text-xs mt-1 ${
|
className={`text-[10px] mt-1.5 ${
|
||||||
msg.role === 'user' ? 'text-blue-200' : 'text-gray-500'
|
msg.role === 'user' ? 'text-white/50' : 'text-[#334060]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.timestamp.toLocaleTimeString()}
|
{msg.timestamp.toLocaleTimeString()}
|
||||||
@ -227,12 +240,12 @@ export default function AgentChat() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleCopy(msg.id, msg.content)}
|
onClick={() => handleCopy(msg.id, msg.content)}
|
||||||
aria-label="Copy message"
|
aria-label="Copy message"
|
||||||
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-400 hover:text-white"
|
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-all duration-300 p-1.5 rounded-lg bg-[#0a0a18] border border-[#1a1a3e] text-[#556080] hover:text-white hover:border-[#0080ff40]"
|
||||||
>
|
>
|
||||||
{copiedId === msg.id ? (
|
{copiedId === msg.id ? (
|
||||||
<Check className="h-3.5 w-3.5 text-green-400" />
|
<Check className="h-3 w-3 text-[#00e68a]" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3.5 w-3.5" />
|
<Copy className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -240,17 +253,16 @@ export default function AgentChat() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{typing && (
|
{typing && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3 animate-fade-in">
|
||||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center">
|
<div className="flex-shrink-0 w-8 h-8 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #1a1a3e, #12122a)' }}>
|
||||||
<Bot className="h-4 w-4 text-white" />
|
<Bot className="h-4 w-4 text-[#0080ff]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-800 border border-gray-700 rounded-xl px-4 py-3">
|
<div className="rounded-2xl px-4 py-3 border border-[#1a1a3e]" style={{ background: 'linear-gradient(135deg, rgba(13,13,32,0.8), rgba(10,10,26,0.6))' }}>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
<span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||||
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
<span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||||
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
<span className="w-1.5 h-1.5 bg-[#0080ff] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">Typing...</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -259,9 +271,9 @@ export default function AgentChat() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input area */}
|
{/* Input area */}
|
||||||
<div className="border-t border-gray-800 bg-gray-900 p-4">
|
<div className="border-t border-[#1a1a3e]/40 p-4" style={{ background: 'linear-gradient(180deg, rgba(8,8,24,0.9), rgba(5,5,16,0.95))' }}>
|
||||||
<div className="flex items-end gap-3 max-w-4xl mx-auto">
|
<div className="flex items-end gap-3 max-w-4xl mx-auto">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1">
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
rows={1}
|
rows={1}
|
||||||
@ -270,25 +282,25 @@ export default function AgentChat() {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={connected ? 'Type a message...' : 'Connecting...'}
|
placeholder={connected ? 'Type a message...' : 'Connecting...'}
|
||||||
disabled={!connected}
|
disabled={!connected}
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 resize-none overflow-y-auto"
|
className="input-electric w-full px-4 py-3 text-sm resize-none overflow-y-auto disabled:opacity-40"
|
||||||
style={{ minHeight: '44px', maxHeight: '200px' }}
|
style={{ minHeight: '44px', maxHeight: '200px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!connected || !input.trim()}
|
disabled={!connected || !input.trim()}
|
||||||
className="flex-shrink-0 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded-xl p-3 transition-colors"
|
className="btn-electric flex-shrink-0 p-3 rounded-xl"
|
||||||
>
|
>
|
||||||
<Send className="h-5 w-5" />
|
<Send className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center mt-2 gap-2">
|
<div className="flex items-center justify-center mt-2 gap-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-2 w-2 rounded-full ${
|
className={`inline-block h-1.5 w-1.5 rounded-full glow-dot ${
|
||||||
connected ? 'bg-green-500' : 'bg-red-500'
|
connected ? 'text-[#00e68a] bg-[#00e68a]' : 'text-[#ff4466] bg-[#ff4466]'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-[10px] text-[#334060]">
|
||||||
{connected ? 'Connected' : 'Disconnected'}
|
{connected ? 'Connected' : 'Disconnected'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export default function Config() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getConfig()
|
getConfig()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
// The API may return either a raw string or a JSON string
|
|
||||||
setConfig(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
setConfig(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
||||||
})
|
})
|
||||||
.catch((err) => setError(err.message))
|
.catch((err) => setError(err.message))
|
||||||
@ -49,23 +48,23 @@ export default function Config() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="h-5 w-5 text-blue-400" />
|
<Settings className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">Configuration</h2>
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Configuration</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors disabled:opacity-50"
|
className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
|
||||||
>
|
>
|
||||||
<Save className="h-4 w-4" />
|
<Save className="h-4 w-4" />
|
||||||
{saving ? 'Saving...' : 'Save'}
|
{saving ? 'Saving...' : 'Save'}
|
||||||
@ -73,13 +72,13 @@ export default function Config() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sensitive fields note */}
|
{/* Sensitive fields note */}
|
||||||
<div className="flex items-start gap-3 bg-yellow-900/20 border border-yellow-700/40 rounded-lg p-4">
|
<div className="flex items-start gap-3 rounded-xl p-4 border border-[#ffaa0020]" style={{ background: 'rgba(255,170,0,0.05)' }}>
|
||||||
<ShieldAlert className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
<ShieldAlert className="h-5 w-5 text-[#ffaa00] flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-yellow-300 font-medium">
|
<p className="text-sm text-[#ffaa00] font-medium">
|
||||||
Sensitive fields are masked
|
Sensitive fields are masked
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-yellow-400/70 mt-0.5">
|
<p className="text-sm text-[#ffaa0080] mt-0.5">
|
||||||
API keys, tokens, and passwords are hidden for security. To update a
|
API keys, tokens, and passwords are hidden for security. To update a
|
||||||
masked field, replace the entire masked value with your new value.
|
masked field, replace the entire masked value with your new value.
|
||||||
</p>
|
</p>
|
||||||
@ -88,27 +87,27 @@ export default function Config() {
|
|||||||
|
|
||||||
{/* Success message */}
|
{/* Success message */}
|
||||||
{success && (
|
{success && (
|
||||||
<div className="flex items-center gap-2 bg-green-900/30 border border-green-700 rounded-lg p-3">
|
<div className="flex items-center gap-2 rounded-xl p-3 border border-[#00e68a30] animate-fade-in" style={{ background: 'rgba(0,230,138,0.06)' }}>
|
||||||
<CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" />
|
<CheckCircle className="h-4 w-4 text-[#00e68a] flex-shrink-0" />
|
||||||
<span className="text-sm text-green-300">{success}</span>
|
<span className="text-sm text-[#00e68a]">{success}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-center gap-2 bg-red-900/30 border border-red-700 rounded-lg p-3">
|
<div className="flex items-center gap-2 rounded-xl p-3 border border-[#ff446630] animate-fade-in" style={{ background: 'rgba(255,68,102,0.06)' }}>
|
||||||
<AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0" />
|
<AlertTriangle className="h-4 w-4 text-[#ff4466] flex-shrink-0" />
|
||||||
<span className="text-sm text-red-300">{error}</span>
|
<span className="text-sm text-[#ff6680]">{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Config Editor */}
|
{/* Config Editor */}
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
<div className="glass-card overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-800/50">
|
<div className="flex items-center justify-between px-4 py-2.5 border-b border-[#1a1a3e]" style={{ background: 'rgba(0,128,255,0.03)' }}>
|
||||||
<span className="text-xs text-gray-400 font-medium uppercase tracking-wider">
|
<span className="text-[10px] text-[#334060] font-semibold uppercase tracking-wider">
|
||||||
TOML Configuration
|
TOML Configuration
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-[10px] text-[#334060]">
|
||||||
{config.split('\n').length} lines
|
{config.split('\n').length} lines
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -116,8 +115,8 @@ export default function Config() {
|
|||||||
value={config}
|
value={config}
|
||||||
onChange={(e) => setConfig(e.target.value)}
|
onChange={(e) => setConfig(e.target.value)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="w-full min-h-[500px] bg-gray-950 text-gray-200 font-mono text-sm p-4 resize-y focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset"
|
className="w-full min-h-[500px] text-[#8892a8] font-mono text-sm p-4 resize-y focus:outline-none focus:ring-2 focus:ring-[#0080ff40] focus:ring-inset"
|
||||||
style={{ tabSize: 4 }}
|
style={{ background: 'rgba(5,5,16,0.8)', tabSize: 4 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -26,8 +26,8 @@ export default function Cost() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 animate-fade-in">
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
|
||||||
Failed to load cost data: {error}
|
Failed to load cost data: {error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,7 +37,7 @@ export default function Cost() {
|
|||||||
if (loading || !cost) {
|
if (loading || !cost) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -45,120 +45,67 @@ export default function Cost() {
|
|||||||
const models = Object.values(cost.by_model);
|
const models = Object.values(cost.by_model);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
{[
|
||||||
<div className="flex items-center gap-3 mb-3">
|
{ icon: DollarSign, color: '#0080ff', bg: '#0080ff15', label: 'Session Cost', value: formatUSD(cost.session_cost_usd) },
|
||||||
<div className="p-2 bg-blue-600/20 rounded-lg">
|
{ icon: TrendingUp, color: '#00e68a', bg: '#00e68a15', label: 'Daily Cost', value: formatUSD(cost.daily_cost_usd) },
|
||||||
<DollarSign className="h-5 w-5 text-blue-400" />
|
{ icon: Layers, color: '#a855f7', bg: '#a855f715', label: 'Monthly Cost', value: formatUSD(cost.monthly_cost_usd) },
|
||||||
|
{ icon: Hash, color: '#ff8800', bg: '#ff880015', label: 'Total Requests', value: cost.request_count.toLocaleString() },
|
||||||
|
].map(({ icon: Icon, color, bg, label, value }) => (
|
||||||
|
<div key={label} className="glass-card p-5 animate-slide-in-up">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<div className="p-2 rounded-xl" style={{ background: bg }}>
|
||||||
|
<Icon className="h-5 w-5" style={{ color }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[#556080] uppercase tracking-wider font-medium">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-400">Session Cost</span>
|
<p className="text-2xl font-bold text-white font-mono">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-white">
|
))}
|
||||||
{formatUSD(cost.session_cost_usd)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="p-2 bg-green-600/20 rounded-lg">
|
|
||||||
<TrendingUp className="h-5 w-5 text-green-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Daily Cost</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-white">
|
|
||||||
{formatUSD(cost.daily_cost_usd)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="p-2 bg-purple-600/20 rounded-lg">
|
|
||||||
<Layers className="h-5 w-5 text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Monthly Cost</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-white">
|
|
||||||
{formatUSD(cost.monthly_cost_usd)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="p-2 bg-orange-600/20 rounded-lg">
|
|
||||||
<Hash className="h-5 w-5 text-orange-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Total Requests</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-white">
|
|
||||||
{cost.request_count.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Token Statistics */}
|
{/* Token Statistics */}
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-5">
|
<div className="glass-card p-5 animate-slide-in-up" style={{ animationDelay: '200ms' }}>
|
||||||
<h3 className="text-base font-semibold text-white mb-4">
|
<h3 className="text-sm font-semibold text-white mb-4 uppercase tracking-wider">
|
||||||
Token Statistics
|
Token Statistics
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
{[
|
||||||
<p className="text-sm text-gray-400">Total Tokens</p>
|
{ label: 'Total Tokens', value: cost.total_tokens.toLocaleString() },
|
||||||
<p className="text-xl font-bold text-white mt-1">
|
{ label: 'Avg Tokens / Request', value: cost.request_count > 0 ? Math.round(cost.total_tokens / cost.request_count).toLocaleString() : '0' },
|
||||||
{cost.total_tokens.toLocaleString()}
|
{ label: 'Cost per 1K Tokens', value: cost.total_tokens > 0 ? formatUSD((cost.monthly_cost_usd / cost.total_tokens) * 1000) : '$0.0000' },
|
||||||
</p>
|
].map(({ label, value }) => (
|
||||||
</div>
|
<div key={label} className="rounded-xl p-4" style={{ background: 'rgba(0,128,255,0.04)', border: '1px solid rgba(0,128,255,0.08)' }}>
|
||||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
<p className="text-xs text-[#556080] uppercase tracking-wider">{label}</p>
|
||||||
<p className="text-sm text-gray-400">Avg Tokens / Request</p>
|
<p className="text-xl font-bold text-white mt-1 font-mono">{value}</p>
|
||||||
<p className="text-xl font-bold text-white mt-1">
|
</div>
|
||||||
{cost.request_count > 0
|
))}
|
||||||
? Math.round(cost.total_tokens / cost.request_count).toLocaleString()
|
|
||||||
: '0'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-gray-400">Cost per 1K Tokens</p>
|
|
||||||
<p className="text-xl font-bold text-white mt-1">
|
|
||||||
{cost.total_tokens > 0
|
|
||||||
? formatUSD((cost.monthly_cost_usd / cost.total_tokens) * 1000)
|
|
||||||
: '$0.0000'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Model Breakdown Table */}
|
{/* Model Breakdown Table */}
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
<div className="glass-card overflow-hidden animate-slide-in-up" style={{ animationDelay: '300ms' }}>
|
||||||
<div className="px-5 py-4 border-b border-gray-800">
|
<div className="px-5 py-4 border-b border-[#1a1a3e]">
|
||||||
<h3 className="text-base font-semibold text-white">
|
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
Model Breakdown
|
Model Breakdown
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{models.length === 0 ? (
|
{models.length === 0 ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-[#334060]">
|
||||||
No model data available.
|
No model data available.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="table-electric">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-800">
|
<tr>
|
||||||
<th className="text-left px-5 py-3 text-gray-400 font-medium">
|
<th className="text-left">Model</th>
|
||||||
Model
|
<th className="text-right">Cost</th>
|
||||||
</th>
|
<th className="text-right">Tokens</th>
|
||||||
<th className="text-right px-5 py-3 text-gray-400 font-medium">
|
<th className="text-right">Requests</th>
|
||||||
Cost
|
<th className="text-left">Share</th>
|
||||||
</th>
|
|
||||||
<th className="text-right px-5 py-3 text-gray-400 font-medium">
|
|
||||||
Tokens
|
|
||||||
</th>
|
|
||||||
<th className="text-right px-5 py-3 text-gray-400 font-medium">
|
|
||||||
Requests
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-5 py-3 text-gray-400 font-medium">
|
|
||||||
Share
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -170,31 +117,28 @@ export default function Cost() {
|
|||||||
? (m.cost_usd / cost.monthly_cost_usd) * 100
|
? (m.cost_usd / cost.monthly_cost_usd) * 100
|
||||||
: 0;
|
: 0;
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr key={m.model}>
|
||||||
key={m.model}
|
<td className="px-5 py-3 text-white font-medium text-sm">
|
||||||
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-5 py-3 text-white font-medium">
|
|
||||||
{m.model}
|
{m.model}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3 text-gray-300 text-right font-mono">
|
<td className="px-5 py-3 text-[#8892a8] text-right font-mono text-sm">
|
||||||
{formatUSD(m.cost_usd)}
|
{formatUSD(m.cost_usd)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3 text-gray-300 text-right">
|
<td className="px-5 py-3 text-[#8892a8] text-right text-sm">
|
||||||
{m.total_tokens.toLocaleString()}
|
{m.total_tokens.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3 text-gray-300 text-right">
|
<td className="px-5 py-3 text-[#8892a8] text-right text-sm">
|
||||||
{m.request_count.toLocaleString()}
|
{m.request_count.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3">
|
<td className="px-5 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-20 h-2 bg-gray-800 rounded-full overflow-hidden">
|
<div className="w-20 h-1.5 bg-[#0a0a18] rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-blue-500 rounded-full"
|
className="h-full rounded-full progress-bar-animated transition-all duration-700"
|
||||||
style={{ width: `${Math.max(share, 2)}%` }}
|
style={{ width: `${Math.max(share, 2)}%`, background: '#0080ff' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-400 w-10 text-right">
|
<span className="text-xs text-[#556080] w-10 text-right font-mono">
|
||||||
{share.toFixed(1)}%
|
{share.toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -84,19 +84,19 @@ export default function Cron() {
|
|||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case 'ok':
|
case 'ok':
|
||||||
case 'success':
|
case 'success':
|
||||||
return <CheckCircle className="h-4 w-4 text-green-400" />;
|
return <CheckCircle className="h-4 w-4 text-[#00e68a]" />;
|
||||||
case 'error':
|
case 'error':
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return <XCircle className="h-4 w-4 text-red-400" />;
|
return <XCircle className="h-4 w-4 text-[#ff4466]" />;
|
||||||
default:
|
default:
|
||||||
return <AlertCircle className="h-4 w-4 text-yellow-400" />;
|
return <AlertCircle className="h-4 w-4 text-[#ffaa00]" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 animate-fade-in">
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
|
||||||
Failed to load cron jobs: {error}
|
Failed to load cron jobs: {error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -106,24 +106,24 @@ export default function Cron() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-5 w-5 text-blue-400" />
|
<Clock className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
Scheduled Tasks ({jobs.length})
|
Scheduled Tasks ({jobs.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForm(true)}
|
onClick={() => setShowForm(true)}
|
||||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
|
className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Job
|
Add Job
|
||||||
@ -132,8 +132,8 @@ export default function Cron() {
|
|||||||
|
|
||||||
{/* Add Job Form Modal */}
|
{/* Add Job Form Modal */}
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
<div className="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md mx-4">
|
<div className="glass-card p-6 w-full max-w-md mx-4 animate-fade-in-scale">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-white">Add Cron Job</h3>
|
<h3 className="text-lg font-semibold text-white">Add Cron Job</h3>
|
||||||
<button
|
<button
|
||||||
@ -141,21 +141,21 @@ export default function Cron() {
|
|||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-[#556080] hover:text-white transition-colors duration-300"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formError && (
|
{formError && (
|
||||||
<div className="mb-4 rounded-lg bg-red-900/30 border border-red-700 p-3 text-sm text-red-300">
|
<div className="mb-4 rounded-xl bg-[#ff446615] border border-[#ff446630] p-3 text-sm text-[#ff6680] animate-fade-in">
|
||||||
{formError}
|
{formError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
|
||||||
Name (optional)
|
Name (optional)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -163,31 +163,31 @@ export default function Cron() {
|
|||||||
value={formName}
|
value={formName}
|
||||||
onChange={(e) => setFormName(e.target.value)}
|
onChange={(e) => setFormName(e.target.value)}
|
||||||
placeholder="e.g. Daily cleanup"
|
placeholder="e.g. Daily cleanup"
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="input-electric w-full px-3 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
|
||||||
Schedule <span className="text-red-400">*</span>
|
Schedule <span className="text-[#ff4466]">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formSchedule}
|
value={formSchedule}
|
||||||
onChange={(e) => setFormSchedule(e.target.value)}
|
onChange={(e) => setFormSchedule(e.target.value)}
|
||||||
placeholder="e.g. 0 0 * * * (cron expression)"
|
placeholder="e.g. 0 0 * * * (cron expression)"
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="input-electric w-full px-3 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
|
||||||
Command <span className="text-red-400">*</span>
|
Command <span className="text-[#ff4466]">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formCommand}
|
value={formCommand}
|
||||||
onChange={(e) => setFormCommand(e.target.value)}
|
onChange={(e) => setFormCommand(e.target.value)}
|
||||||
placeholder="e.g. cleanup --older-than 7d"
|
placeholder="e.g. cleanup --older-than 7d"
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="input-electric w-full px-3 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -198,14 +198,14 @@ export default function Cron() {
|
|||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors"
|
className="px-4 py-2 text-sm font-medium text-[#8892a8] hover:text-white border border-[#1a1a3e] rounded-xl hover:bg-[#0080ff08] transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50"
|
className="btn-electric px-4 py-2 text-sm font-medium"
|
||||||
>
|
>
|
||||||
{submitting ? 'Adding...' : 'Add Job'}
|
{submitting ? 'Adding...' : 'Add Job'}
|
||||||
</button>
|
</button>
|
||||||
@ -216,88 +216,72 @@ export default function Cron() {
|
|||||||
|
|
||||||
{/* Jobs Table */}
|
{/* Jobs Table */}
|
||||||
{jobs.length === 0 ? (
|
{jobs.length === 0 ? (
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
|
<div className="glass-card p-8 text-center">
|
||||||
<Clock className="h-10 w-10 text-gray-600 mx-auto mb-3" />
|
<Clock className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
|
||||||
<p className="text-gray-400">No scheduled tasks configured.</p>
|
<p className="text-[#556080]">No scheduled tasks configured.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto">
|
<div className="glass-card overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="table-electric">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-800">
|
<tr>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-left">ID</th>
|
||||||
ID
|
<th className="text-left">Name</th>
|
||||||
</th>
|
<th className="text-left">Command</th>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-left">Next Run</th>
|
||||||
Name
|
<th className="text-left">Last Status</th>
|
||||||
</th>
|
<th className="text-left">Enabled</th>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-right">Actions</th>
|
||||||
Command
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Next Run
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Last Status
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Enabled
|
|
||||||
</th>
|
|
||||||
<th className="text-right px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{jobs.map((job) => (
|
{jobs.map((job) => (
|
||||||
<tr
|
<tr key={job.id}>
|
||||||
key={job.id}
|
<td className="px-4 py-3 text-[#556080] font-mono text-xs">
|
||||||
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 text-gray-400 font-mono text-xs">
|
|
||||||
{job.id.slice(0, 8)}
|
{job.id.slice(0, 8)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-white font-medium">
|
<td className="px-4 py-3 text-white font-medium text-sm">
|
||||||
{job.name ?? '-'}
|
{job.name ?? '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-300 font-mono text-xs max-w-[200px] truncate">
|
<td className="px-4 py-3 text-[#8892a8] font-mono text-xs max-w-[200px] truncate">
|
||||||
{job.command}
|
{job.command}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-400 text-xs">
|
<td className="px-4 py-3 text-[#556080] text-xs">
|
||||||
{formatDate(job.next_run)}
|
{formatDate(job.next_run)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{statusIcon(job.last_status)}
|
{statusIcon(job.last_status)}
|
||||||
<span className="text-gray-300 text-xs capitalize">
|
<span className="text-[#8892a8] text-xs capitalize">
|
||||||
{job.last_status ?? '-'}
|
{job.last_status ?? '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border ${
|
||||||
job.enabled
|
job.enabled
|
||||||
? 'bg-green-900/40 text-green-400 border border-green-700/50'
|
? 'text-[#00e68a] border-[#00e68a30]'
|
||||||
: 'bg-gray-800 text-gray-500 border border-gray-700'
|
: 'text-[#334060] border-[#1a1a3e]'
|
||||||
}`}
|
}`}
|
||||||
|
style={{ background: job.enabled ? 'rgba(0,230,138,0.06)' : 'rgba(26,26,62,0.3)' }}
|
||||||
>
|
>
|
||||||
{job.enabled ? 'Enabled' : 'Disabled'}
|
{job.enabled ? 'Enabled' : 'Disabled'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
{confirmDelete === job.id ? (
|
{confirmDelete === job.id ? (
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2 animate-fade-in">
|
||||||
<span className="text-xs text-red-400">Delete?</span>
|
<span className="text-xs text-[#ff4466]">Delete?</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(job.id)}
|
onClick={() => handleDelete(job.id)}
|
||||||
className="text-red-400 hover:text-red-300 text-xs font-medium"
|
className="text-[#ff4466] hover:text-[#ff6680] text-xs font-medium"
|
||||||
>
|
>
|
||||||
Yes
|
Yes
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmDelete(null)}
|
onClick={() => setConfirmDelete(null)}
|
||||||
className="text-gray-400 hover:text-white text-xs font-medium"
|
className="text-[#556080] hover:text-white text-xs font-medium"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
@ -305,7 +289,7 @@ export default function Cron() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmDelete(job.id)}
|
onClick={() => setConfirmDelete(job.id)}
|
||||||
className="text-gray-400 hover:text-red-400 transition-colors"
|
className="text-[#334060] hover:text-[#ff4466] transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -28,13 +28,13 @@ function healthColor(status: string): string {
|
|||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case 'ok':
|
case 'ok':
|
||||||
case 'healthy':
|
case 'healthy':
|
||||||
return 'bg-green-500';
|
return 'bg-[#00e68a]';
|
||||||
case 'warn':
|
case 'warn':
|
||||||
case 'warning':
|
case 'warning':
|
||||||
case 'degraded':
|
case 'degraded':
|
||||||
return 'bg-yellow-500';
|
return 'bg-[#ffaa00]';
|
||||||
default:
|
default:
|
||||||
return 'bg-red-500';
|
return 'bg-[#ff4466]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,13 +42,13 @@ function healthBorder(status: string): string {
|
|||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case 'ok':
|
case 'ok':
|
||||||
case 'healthy':
|
case 'healthy':
|
||||||
return 'border-green-500/30';
|
return 'border-[#00e68a30]';
|
||||||
case 'warn':
|
case 'warn':
|
||||||
case 'warning':
|
case 'warning':
|
||||||
case 'degraded':
|
case 'degraded':
|
||||||
return 'border-yellow-500/30';
|
return 'border-[#ffaa0030]';
|
||||||
default:
|
default:
|
||||||
return 'border-red-500/30';
|
return 'border-[#ff446630]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 animate-fade-in">
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
|
||||||
Failed to load dashboard: {error}
|
Failed to load dashboard: {error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +79,7 @@ export default function Dashboard() {
|
|||||||
if (!status || !cost) {
|
if (!status || !cost) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -87,124 +87,89 @@ export default function Dashboard() {
|
|||||||
const maxCost = Math.max(cost.session_cost_usd, cost.daily_cost_usd, cost.monthly_cost_usd, 0.001);
|
const maxCost = Math.max(cost.session_cost_usd, cost.daily_cost_usd, cost.monthly_cost_usd, 0.001);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Status Cards Grid */}
|
{/* Status Cards Grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 stagger-children">
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
{[
|
||||||
<div className="flex items-center gap-3 mb-3">
|
{ icon: Cpu, color: '#0080ff', bg: '#0080ff15', label: 'Provider / Model', value: status.provider ?? 'Unknown', sub: status.model },
|
||||||
<div className="p-2 bg-blue-600/20 rounded-lg">
|
{ icon: Clock, color: '#00e68a', bg: '#00e68a15', label: 'Uptime', value: formatUptime(status.uptime_seconds), sub: 'Since last restart' },
|
||||||
<Cpu className="h-5 w-5 text-blue-400" />
|
{ icon: Globe, color: '#a855f7', bg: '#a855f715', label: 'Gateway Port', value: `:${status.gateway_port}`, sub: `Locale: ${status.locale}` },
|
||||||
|
{ icon: Database, color: '#ff8800', bg: '#ff880015', label: 'Memory Backend', value: status.memory_backend, sub: `Paired: ${status.paired ? 'Yes' : 'No'}` },
|
||||||
|
].map(({ icon: Icon, color, bg, label, value, sub }) => (
|
||||||
|
<div key={label} className="glass-card p-5 animate-slide-in-up">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<div className="p-2 rounded-xl" style={{ background: bg }}>
|
||||||
|
<Icon className="h-5 w-5" style={{ color }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[#556080] uppercase tracking-wider font-medium">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-400">Provider / Model</span>
|
<p className="text-lg font-semibold text-white truncate capitalize">{value}</p>
|
||||||
|
<p className="text-sm text-[#556080] truncate">{sub}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-semibold text-white truncate">
|
))}
|
||||||
{status.provider ?? 'Unknown'}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400 truncate">{status.model}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="p-2 bg-green-600/20 rounded-lg">
|
|
||||||
<Clock className="h-5 w-5 text-green-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Uptime</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-semibold text-white">
|
|
||||||
{formatUptime(status.uptime_seconds)}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">Since last restart</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="p-2 bg-purple-600/20 rounded-lg">
|
|
||||||
<Globe className="h-5 w-5 text-purple-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Gateway Port</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-semibold text-white">
|
|
||||||
:{status.gateway_port}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">Locale: {status.locale}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="p-2 bg-orange-600/20 rounded-lg">
|
|
||||||
<Database className="h-5 w-5 text-orange-400" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Memory Backend</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-semibold text-white capitalize">
|
|
||||||
{status.memory_backend}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
Paired: {status.paired ? 'Yes' : 'No'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 stagger-children">
|
||||||
{/* Cost Widget */}
|
{/* Cost Widget */}
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
<div className="glass-card p-5 animate-slide-in-up">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
<DollarSign className="h-5 w-5 text-blue-400" />
|
<DollarSign className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">Cost Overview</h2>
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Cost Overview</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{ label: 'Session', value: cost.session_cost_usd, color: 'bg-blue-500' },
|
{ label: 'Session', value: cost.session_cost_usd, color: '#0080ff' },
|
||||||
{ label: 'Daily', value: cost.daily_cost_usd, color: 'bg-green-500' },
|
{ label: 'Daily', value: cost.daily_cost_usd, color: '#00e68a' },
|
||||||
{ label: 'Monthly', value: cost.monthly_cost_usd, color: 'bg-purple-500' },
|
{ label: 'Monthly', value: cost.monthly_cost_usd, color: '#a855f7' },
|
||||||
].map(({ label, value, color }) => (
|
].map(({ label, value, color }) => (
|
||||||
<div key={label}>
|
<div key={label}>
|
||||||
<div className="flex justify-between text-sm mb-1">
|
<div className="flex justify-between text-sm mb-1.5">
|
||||||
<span className="text-gray-400">{label}</span>
|
<span className="text-[#556080]">{label}</span>
|
||||||
<span className="text-white font-medium">{formatUSD(value)}</span>
|
<span className="text-white font-medium font-mono">{formatUSD(value)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-2 bg-gray-800 rounded-full overflow-hidden">
|
<div className="w-full h-1.5 bg-[#0a0a18] rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full ${color}`}
|
className="h-full rounded-full progress-bar-animated transition-all duration-700 ease-out"
|
||||||
style={{ width: `${Math.max((value / maxCost) * 100, 2)}%` }}
|
style={{ width: `${Math.max((value / maxCost) * 100, 2)}%`, background: color }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 pt-3 border-t border-gray-800 flex justify-between text-sm">
|
<div className="mt-5 pt-4 border-t border-[#1a1a3e]/50 flex justify-between text-sm">
|
||||||
<span className="text-gray-400">Total Tokens</span>
|
<span className="text-[#556080]">Total Tokens</span>
|
||||||
<span className="text-white">{cost.total_tokens.toLocaleString()}</span>
|
<span className="text-white font-mono">{cost.total_tokens.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm mt-1">
|
<div className="flex justify-between text-sm mt-1">
|
||||||
<span className="text-gray-400">Requests</span>
|
<span className="text-[#556080]">Requests</span>
|
||||||
<span className="text-white">{cost.request_count.toLocaleString()}</span>
|
<span className="text-white font-mono">{cost.request_count.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active Channels */}
|
{/* Active Channels */}
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
<div className="glass-card p-5 animate-slide-in-up">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
<Radio className="h-5 w-5 text-blue-400" />
|
<Radio className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">Active Channels</h2>
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Active Channels</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(status.channels).length === 0 ? (
|
{Object.entries(status.channels).length === 0 ? (
|
||||||
<p className="text-sm text-gray-500">No channels configured</p>
|
<p className="text-sm text-[#334060]">No channels configured</p>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(status.channels).map(([name, active]) => (
|
Object.entries(status.channels).map(([name, active]) => (
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={name}
|
||||||
className="flex items-center justify-between py-2 px-3 rounded-lg bg-gray-800/50"
|
className="flex items-center justify-between py-2.5 px-3 rounded-xl transition-all duration-300 hover:bg-[#0080ff08]"
|
||||||
|
style={{ background: 'rgba(10, 10, 26, 0.5)' }}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-white capitalize">{name}</span>
|
<span className="text-sm text-white capitalize font-medium">{name}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-2.5 w-2.5 rounded-full ${
|
className={`inline-block h-2 w-2 rounded-full glow-dot ${
|
||||||
active ? 'bg-green-500' : 'bg-gray-500'
|
active ? 'text-[#00e68a] bg-[#00e68a]' : 'text-[#334060] bg-[#334060]'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-[#556080]">
|
||||||
{active ? 'Active' : 'Inactive'}
|
{active ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -215,29 +180,30 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Health Grid */}
|
{/* Health Grid */}
|
||||||
<div className="bg-gray-900 rounded-xl p-5 border border-gray-800">
|
<div className="glass-card p-5 animate-slide-in-up">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-5">
|
||||||
<Activity className="h-5 w-5 text-blue-400" />
|
<Activity className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">Component Health</h2>
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Component Health</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{Object.entries(status.health.components).length === 0 ? (
|
{Object.entries(status.health.components).length === 0 ? (
|
||||||
<p className="text-sm text-gray-500 col-span-2">No components reporting</p>
|
<p className="text-sm text-[#334060] col-span-2">No components reporting</p>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(status.health.components).map(([name, comp]) => (
|
Object.entries(status.health.components).map(([name, comp]) => (
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={name}
|
||||||
className={`rounded-lg p-3 border ${healthBorder(comp.status)} bg-gray-800/50`}
|
className={`rounded-xl p-3 border ${healthBorder(comp.status)} transition-all duration-300 hover:scale-[1.02]`}
|
||||||
|
style={{ background: 'rgba(10, 10, 26, 0.5)' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className={`inline-block h-2 w-2 rounded-full ${healthColor(comp.status)}`} />
|
<span className={`inline-block h-2 w-2 rounded-full ${healthColor(comp.status)} glow-dot`} />
|
||||||
<span className="text-sm font-medium text-white capitalize truncate">
|
<span className="text-sm font-medium text-white capitalize truncate">
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 capitalize">{comp.status}</p>
|
<p className="text-xs text-[#556080] capitalize">{comp.status}</p>
|
||||||
{comp.restart_count > 0 && (
|
{comp.restart_count > 0 && (
|
||||||
<p className="text-xs text-yellow-400 mt-1">
|
<p className="text-xs text-[#ffaa00] mt-1">
|
||||||
Restarts: {comp.restart_count}
|
Restarts: {comp.restart_count}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -13,33 +13,33 @@ import { runDoctor } from '@/lib/api';
|
|||||||
function severityIcon(severity: DiagResult['severity']) {
|
function severityIcon(severity: DiagResult['severity']) {
|
||||||
switch (severity) {
|
switch (severity) {
|
||||||
case 'ok':
|
case 'ok':
|
||||||
return <CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" />;
|
return <CheckCircle className="h-4 w-4 text-[#00e68a] flex-shrink-0" />;
|
||||||
case 'warn':
|
case 'warn':
|
||||||
return <AlertTriangle className="h-4 w-4 text-yellow-400 flex-shrink-0" />;
|
return <AlertTriangle className="h-4 w-4 text-[#ffaa00] flex-shrink-0" />;
|
||||||
case 'error':
|
case 'error':
|
||||||
return <XCircle className="h-4 w-4 text-red-400 flex-shrink-0" />;
|
return <XCircle className="h-4 w-4 text-[#ff4466] flex-shrink-0" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function severityBorder(severity: DiagResult['severity']): string {
|
function severityBorder(severity: DiagResult['severity']): string {
|
||||||
switch (severity) {
|
switch (severity) {
|
||||||
case 'ok':
|
case 'ok':
|
||||||
return 'border-green-700/40';
|
return 'border-[#00e68a20]';
|
||||||
case 'warn':
|
case 'warn':
|
||||||
return 'border-yellow-700/40';
|
return 'border-[#ffaa0020]';
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'border-red-700/40';
|
return 'border-[#ff446620]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function severityBg(severity: DiagResult['severity']): string {
|
function severityBg(severity: DiagResult['severity']): string {
|
||||||
switch (severity) {
|
switch (severity) {
|
||||||
case 'ok':
|
case 'ok':
|
||||||
return 'bg-green-900/10';
|
return 'rgba(0,230,138,0.04)';
|
||||||
case 'warn':
|
case 'warn':
|
||||||
return 'bg-yellow-900/10';
|
return 'rgba(255,170,0,0.04)';
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'bg-red-900/10';
|
return 'rgba(255,68,102,0.04)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,12 +62,10 @@ export default function Doctor() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute summary counts
|
|
||||||
const okCount = results?.filter((r) => r.severity === 'ok').length ?? 0;
|
const okCount = results?.filter((r) => r.severity === 'ok').length ?? 0;
|
||||||
const warnCount = results?.filter((r) => r.severity === 'warn').length ?? 0;
|
const warnCount = results?.filter((r) => r.severity === 'warn').length ?? 0;
|
||||||
const errorCount = results?.filter((r) => r.severity === 'error').length ?? 0;
|
const errorCount = results?.filter((r) => r.severity === 'error').length ?? 0;
|
||||||
|
|
||||||
// Group by category
|
|
||||||
const grouped =
|
const grouped =
|
||||||
results?.reduce<Record<string, DiagResult[]>>((acc, item) => {
|
results?.reduce<Record<string, DiagResult[]>>((acc, item) => {
|
||||||
const key = item.category;
|
const key = item.category;
|
||||||
@ -77,17 +75,17 @@ export default function Doctor() {
|
|||||||
}, {}) ?? {};
|
}, {}) ?? {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Stethoscope className="h-5 w-5 text-blue-400" />
|
<Stethoscope className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">Diagnostics</h2>
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Diagnostics</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors disabled:opacity-50"
|
className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
@ -105,17 +103,17 @@ export default function Doctor() {
|
|||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680] animate-fade-in">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Loading spinner */}
|
{/* Loading spinner */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex flex-col items-center justify-center py-16">
|
<div className="flex flex-col items-center justify-center py-16 animate-fade-in">
|
||||||
<Loader2 className="h-10 w-10 text-blue-500 animate-spin mb-4" />
|
<div className="h-12 w-12 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin mb-4" />
|
||||||
<p className="text-gray-400">Running diagnostics...</p>
|
<p className="text-[#8892a8]">Running diagnostics...</p>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-[#334060] mt-1">
|
||||||
This may take a few seconds.
|
This may take a few seconds.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -125,29 +123,29 @@ export default function Doctor() {
|
|||||||
{results && !loading && (
|
{results && !loading && (
|
||||||
<>
|
<>
|
||||||
{/* Summary Bar */}
|
{/* Summary Bar */}
|
||||||
<div className="flex items-center gap-4 bg-gray-900 rounded-xl border border-gray-800 p-4">
|
<div className="glass-card flex items-center gap-4 p-4 animate-slide-in-up">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
<CheckCircle className="h-5 w-5 text-[#00e68a]" />
|
||||||
<span className="text-sm text-white font-medium">
|
<span className="text-sm text-white font-medium">
|
||||||
{okCount} <span className="text-gray-400 font-normal">ok</span>
|
{okCount} <span className="text-[#556080] font-normal">ok</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-px h-5 bg-gray-700" />
|
<div className="w-px h-5 bg-[#1a1a3e]" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangle className="h-5 w-5 text-yellow-400" />
|
<AlertTriangle className="h-5 w-5 text-[#ffaa00]" />
|
||||||
<span className="text-sm text-white font-medium">
|
<span className="text-sm text-white font-medium">
|
||||||
{warnCount}{' '}
|
{warnCount}{' '}
|
||||||
<span className="text-gray-400 font-normal">
|
<span className="text-[#556080] font-normal">
|
||||||
warning{warnCount !== 1 ? 's' : ''}
|
warning{warnCount !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-px h-5 bg-gray-700" />
|
<div className="w-px h-5 bg-[#1a1a3e]" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<XCircle className="h-5 w-5 text-red-400" />
|
<XCircle className="h-5 w-5 text-[#ff4466]" />
|
||||||
<span className="text-sm text-white font-medium">
|
<span className="text-sm text-white font-medium">
|
||||||
{errorCount}{' '}
|
{errorCount}{' '}
|
||||||
<span className="text-gray-400 font-normal">
|
<span className="text-[#556080] font-normal">
|
||||||
error{errorCount !== 1 ? 's' : ''}
|
error{errorCount !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -156,15 +154,15 @@ export default function Doctor() {
|
|||||||
{/* Overall indicator */}
|
{/* Overall indicator */}
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
{errorCount > 0 ? (
|
{errorCount > 0 ? (
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-red-900/40 text-red-400 border border-red-700/50">
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-semibold border text-[#ff4466] border-[#ff446630]" style={{ background: 'rgba(255,68,102,0.06)' }}>
|
||||||
Issues Found
|
Issues Found
|
||||||
</span>
|
</span>
|
||||||
) : warnCount > 0 ? (
|
) : warnCount > 0 ? (
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-yellow-900/40 text-yellow-400 border border-yellow-700/50">
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-semibold border text-[#ffaa00] border-[#ffaa0030]" style={{ background: 'rgba(255,170,0,0.06)' }}>
|
||||||
Warnings
|
Warnings
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-green-900/40 text-green-400 border border-green-700/50">
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-semibold border text-[#00e68a] border-[#00e68a30]" style={{ background: 'rgba(0,230,138,0.06)' }}>
|
||||||
All Clear
|
All Clear
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -174,23 +172,22 @@ export default function Doctor() {
|
|||||||
{/* Grouped Results */}
|
{/* Grouped Results */}
|
||||||
{Object.entries(grouped)
|
{Object.entries(grouped)
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.map(([category, items]) => (
|
.map(([category, items], catIdx) => (
|
||||||
<div key={category}>
|
<div key={category} className="animate-slide-in-up" style={{ animationDelay: `${(catIdx + 1) * 100}ms` }}>
|
||||||
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 capitalize">
|
<h3 className="text-[10px] font-semibold text-[#334060] uppercase tracking-wider mb-3 capitalize">
|
||||||
{category}
|
{category}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 stagger-children">
|
||||||
{items.map((result, idx) => (
|
{items.map((result, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`${category}-${idx}`}
|
key={`${category}-${idx}`}
|
||||||
className={`flex items-start gap-3 rounded-lg border p-3 ${severityBorder(
|
className={`flex items-start gap-3 rounded-xl border p-3 transition-all duration-300 hover:translate-x-1 ${severityBorder(result.severity)} animate-slide-in-left`}
|
||||||
result.severity,
|
style={{ background: severityBg(result.severity) }}
|
||||||
)} ${severityBg(result.severity)}`}
|
|
||||||
>
|
>
|
||||||
{severityIcon(result.severity)}
|
{severityIcon(result.severity)}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm text-white">{result.message}</p>
|
<p className="text-sm text-white">{result.message}</p>
|
||||||
<p className="text-xs text-gray-500 mt-0.5 capitalize">
|
<p className="text-[10px] text-[#334060] mt-0.5 capitalize uppercase tracking-wider">
|
||||||
{result.severity}
|
{result.severity}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -204,10 +201,12 @@ export default function Doctor() {
|
|||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
{!results && !loading && !error && (
|
{!results && !loading && !error && (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-gray-500">
|
<div className="flex flex-col items-center justify-center py-16 text-[#334060] animate-fade-in">
|
||||||
<Stethoscope className="h-12 w-12 text-gray-600 mb-4" />
|
<div className="h-16 w-16 rounded-2xl flex items-center justify-center mb-4 animate-float" style={{ background: 'linear-gradient(135deg, #0080ff15, #0080ff08)' }}>
|
||||||
<p className="text-lg font-medium">System Diagnostics</p>
|
<Stethoscope className="h-8 w-8 text-[#0080ff]" />
|
||||||
<p className="text-sm mt-1">
|
</div>
|
||||||
|
<p className="text-lg font-semibold text-white mb-1">System Diagnostics</p>
|
||||||
|
<p className="text-sm text-[#556080]">
|
||||||
Click "Run Diagnostics" to check your ZeroClaw installation.
|
Click "Run Diagnostics" to check your ZeroClaw installation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,19 +9,22 @@ function statusBadge(status: Integration['status']) {
|
|||||||
return {
|
return {
|
||||||
icon: Check,
|
icon: Check,
|
||||||
label: 'Active',
|
label: 'Active',
|
||||||
classes: 'bg-green-900/40 text-green-400 border-green-700/50',
|
classes: 'text-[#00e68a] border-[#00e68a30]',
|
||||||
|
bg: 'rgba(0,230,138,0.06)',
|
||||||
};
|
};
|
||||||
case 'Available':
|
case 'Available':
|
||||||
return {
|
return {
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
label: 'Available',
|
label: 'Available',
|
||||||
classes: 'bg-blue-900/40 text-blue-400 border-blue-700/50',
|
classes: 'text-[#0080ff] border-[#0080ff30]',
|
||||||
|
bg: 'rgba(0,128,255,0.06)',
|
||||||
};
|
};
|
||||||
case 'ComingSoon':
|
case 'ComingSoon':
|
||||||
return {
|
return {
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
label: 'Coming Soon',
|
label: 'Coming Soon',
|
||||||
classes: 'bg-gray-800 text-gray-400 border-gray-700',
|
classes: 'text-[#556080] border-[#1a1a3e]',
|
||||||
|
bg: 'rgba(26,26,62,0.3)',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,8 +62,8 @@ export default function Integrations() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 animate-fade-in">
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
|
||||||
Failed to load integrations: {error}
|
Failed to load integrations: {error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,17 +73,17 @@ export default function Integrations() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Puzzle className="h-5 w-5 text-blue-400" />
|
<Puzzle className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
Integrations ({integrations.length})
|
Integrations ({integrations.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -91,11 +94,12 @@ export default function Integrations() {
|
|||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setActiveCategory(cat)}
|
onClick={() => setActiveCategory(cat)}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors capitalize ${
|
className={`px-3.5 py-1.5 rounded-xl text-xs font-semibold transition-all duration-300 capitalize ${
|
||||||
activeCategory === cat
|
activeCategory === cat
|
||||||
? 'bg-blue-600 text-white'
|
? 'text-white shadow-[0_0_15px_rgba(0,128,255,0.2)]'
|
||||||
: 'bg-gray-900 text-gray-400 border border-gray-700 hover:bg-gray-800 hover:text-white'
|
: 'text-[#556080] border border-[#1a1a3e] hover:text-white hover:border-[#0080ff40]'
|
||||||
}`}
|
}`}
|
||||||
|
style={activeCategory === cat ? { background: 'linear-gradient(135deg, #0080ff, #0066cc)' } : {}}
|
||||||
>
|
>
|
||||||
{cat}
|
{cat}
|
||||||
</button>
|
</button>
|
||||||
@ -104,38 +108,39 @@ export default function Integrations() {
|
|||||||
|
|
||||||
{/* Grouped Integration Cards */}
|
{/* Grouped Integration Cards */}
|
||||||
{Object.keys(grouped).length === 0 ? (
|
{Object.keys(grouped).length === 0 ? (
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
|
<div className="glass-card p-8 text-center">
|
||||||
<Puzzle className="h-10 w-10 text-gray-600 mx-auto mb-3" />
|
<Puzzle className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
|
||||||
<p className="text-gray-400">No integrations found.</p>
|
<p className="text-[#556080]">No integrations found.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(grouped)
|
Object.entries(grouped)
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.map(([category, items]) => (
|
.map(([category, items]) => (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3 capitalize">
|
<h3 className="text-[10px] font-semibold text-[#334060] uppercase tracking-wider mb-3 capitalize">
|
||||||
{category}
|
{category}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 stagger-children">
|
||||||
{items.map((integration) => {
|
{items.map((integration) => {
|
||||||
const badge = statusBadge(integration.status);
|
const badge = statusBadge(integration.status);
|
||||||
const BadgeIcon = badge.icon;
|
const BadgeIcon = badge.icon;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={integration.name}
|
key={integration.name}
|
||||||
className="bg-gray-900 rounded-xl border border-gray-800 p-5 hover:border-gray-700 transition-colors"
|
className="glass-card p-5 animate-slide-in-up"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h4 className="text-sm font-semibold text-white truncate">
|
<h4 className="text-sm font-semibold text-white truncate">
|
||||||
{integration.name}
|
{integration.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-400 mt-1 line-clamp-2">
|
<p className="text-sm text-[#556080] mt-1 line-clamp-2">
|
||||||
{integration.description}
|
{integration.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`flex-shrink-0 inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium border ${badge.classes}`}
|
className={`flex-shrink-0 inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[10px] font-semibold border ${badge.classes}`}
|
||||||
|
style={{ background: badge.bg }}
|
||||||
>
|
>
|
||||||
<BadgeIcon className="h-3 w-3" />
|
<BadgeIcon className="h-3 w-3" />
|
||||||
{badge.label}
|
{badge.label}
|
||||||
|
|||||||
@ -14,24 +14,24 @@ function formatTimestamp(ts?: string): string {
|
|||||||
return new Date(ts).toLocaleTimeString();
|
return new Date(ts).toLocaleTimeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventTypeBadgeColor(type: string): string {
|
function eventTypeBadgeColor(type: string): { classes: string; bg: string } {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'bg-red-900/50 text-red-400 border-red-700/50';
|
return { classes: 'text-[#ff4466] border-[#ff446630]', bg: 'rgba(255,68,102,0.06)' };
|
||||||
case 'warn':
|
case 'warn':
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return 'bg-yellow-900/50 text-yellow-400 border-yellow-700/50';
|
return { classes: 'text-[#ffaa00] border-[#ffaa0030]', bg: 'rgba(255,170,0,0.06)' };
|
||||||
case 'tool_call':
|
case 'tool_call':
|
||||||
case 'tool_result':
|
case 'tool_result':
|
||||||
return 'bg-purple-900/50 text-purple-400 border-purple-700/50';
|
return { classes: 'text-[#a855f7] border-[#a855f730]', bg: 'rgba(168,85,247,0.06)' };
|
||||||
case 'message':
|
case 'message':
|
||||||
case 'chat':
|
case 'chat':
|
||||||
return 'bg-blue-900/50 text-blue-400 border-blue-700/50';
|
return { classes: 'text-[#0080ff] border-[#0080ff30]', bg: 'rgba(0,128,255,0.06)' };
|
||||||
case 'health':
|
case 'health':
|
||||||
case 'status':
|
case 'status':
|
||||||
return 'bg-green-900/50 text-green-400 border-green-700/50';
|
return { classes: 'text-[#00e68a] border-[#00e68a30]', bg: 'rgba(0,230,138,0.06)' };
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-800 text-gray-400 border-gray-700';
|
return { classes: 'text-[#556080] border-[#1a1a3e]', bg: 'rgba(26,26,62,0.3)' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,6 @@ export default function Logs() {
|
|||||||
event,
|
event,
|
||||||
};
|
};
|
||||||
setEntries((prev) => {
|
setEntries((prev) => {
|
||||||
// Cap at 500 entries for performance
|
|
||||||
const next = [...prev, entry];
|
const next = [...prev, entry];
|
||||||
return next.length > 500 ? next.slice(-500) : next;
|
return next.length > 500 ? next.slice(-500) : next;
|
||||||
});
|
});
|
||||||
@ -112,7 +111,6 @@ export default function Logs() {
|
|||||||
setAutoScroll(true);
|
setAutoScroll(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collect all event types for filter checkboxes
|
|
||||||
const allTypes = Array.from(new Set(entries.map((e) => e.event.type))).sort();
|
const allTypes = Array.from(new Set(entries.map((e) => e.event.type))).sort();
|
||||||
|
|
||||||
const toggleTypeFilter = (type: string) => {
|
const toggleTypeFilter = (type: string) => {
|
||||||
@ -135,21 +133,21 @@ export default function Logs() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center justify-between px-6 py-3 border-b border-gray-800 bg-gray-900">
|
<div className="flex items-center justify-between px-6 py-3 border-b border-[#1a1a3e]/40 animate-fade-in" style={{ background: 'linear-gradient(90deg, rgba(8,8,24,0.9), rgba(5,5,16,0.9))' }}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Activity className="h-5 w-5 text-blue-400" />
|
<Activity className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">Live Logs</h2>
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Live Logs</h2>
|
||||||
<div className="flex items-center gap-2 ml-2">
|
<div className="flex items-center gap-2 ml-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-2 w-2 rounded-full ${
|
className={`inline-block h-1.5 w-1.5 rounded-full glow-dot ${
|
||||||
connected ? 'bg-green-500' : 'bg-red-500'
|
connected ? 'text-[#00e68a] bg-[#00e68a]' : 'text-[#ff4466] bg-[#ff4466]'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-[10px] text-[#334060]">
|
||||||
{connected ? 'Connected' : 'Disconnected'}
|
{connected ? 'Connected' : 'Disconnected'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
<span className="text-[10px] text-[#334060] ml-2 font-mono">
|
||||||
{filteredEntries.length} events
|
{filteredEntries.length} events
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -158,11 +156,16 @@ export default function Logs() {
|
|||||||
{/* Pause/Resume */}
|
{/* Pause/Resume */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaused(!paused)}
|
onClick={() => setPaused(!paused)}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all duration-300 ${
|
||||||
paused
|
paused
|
||||||
? 'bg-green-600 hover:bg-green-700 text-white'
|
? 'text-white shadow-[0_0_15px_rgba(0,230,138,0.2)]'
|
||||||
: 'bg-yellow-600 hover:bg-yellow-700 text-white'
|
: 'text-white shadow-[0_0_15px_rgba(255,170,0,0.2)]'
|
||||||
}`}
|
}`}
|
||||||
|
style={{
|
||||||
|
background: paused
|
||||||
|
? 'linear-gradient(135deg, #00e68a, #00cc7a)'
|
||||||
|
: 'linear-gradient(135deg, #ffaa00, #ee9900)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{paused ? (
|
{paused ? (
|
||||||
<>
|
<>
|
||||||
@ -179,7 +182,7 @@ export default function Logs() {
|
|||||||
{!autoScroll && (
|
{!autoScroll && (
|
||||||
<button
|
<button
|
||||||
onClick={jumpToBottom}
|
onClick={jumpToBottom}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors"
|
className="btn-electric flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold"
|
||||||
>
|
>
|
||||||
<ArrowDown className="h-3.5 w-3.5" />
|
<ArrowDown className="h-3.5 w-3.5" />
|
||||||
Jump to bottom
|
Jump to bottom
|
||||||
@ -190,9 +193,9 @@ export default function Logs() {
|
|||||||
|
|
||||||
{/* Event type filters */}
|
{/* Event type filters */}
|
||||||
{allTypes.length > 0 && (
|
{allTypes.length > 0 && (
|
||||||
<div className="flex items-center gap-2 px-6 py-2 border-b border-gray-800 bg-gray-900/80 overflow-x-auto">
|
<div className="flex items-center gap-2 px-6 py-2 border-b border-[#1a1a3e]/30 overflow-x-auto" style={{ background: 'rgba(5,5,16,0.6)' }}>
|
||||||
<Filter className="h-4 w-4 text-gray-500 flex-shrink-0" />
|
<Filter className="h-3.5 w-3.5 text-[#334060] flex-shrink-0" />
|
||||||
<span className="text-xs text-gray-500 flex-shrink-0">Filter:</span>
|
<span className="text-[10px] text-[#334060] flex-shrink-0 uppercase tracking-wider">Filter:</span>
|
||||||
{allTypes.map((type) => (
|
{allTypes.map((type) => (
|
||||||
<label
|
<label
|
||||||
key={type}
|
key={type}
|
||||||
@ -202,15 +205,15 @@ export default function Logs() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={typeFilters.has(type)}
|
checked={typeFilters.has(type)}
|
||||||
onChange={() => toggleTypeFilter(type)}
|
onChange={() => toggleTypeFilter(type)}
|
||||||
className="rounded bg-gray-800 border-gray-600 text-blue-500 focus:ring-blue-500 focus:ring-offset-0 h-3.5 w-3.5"
|
className="rounded bg-[#0a0a18] border-[#1a1a3e] text-[#0080ff] focus:ring-[#0080ff] focus:ring-offset-0 h-3 w-3"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-400 capitalize">{type}</span>
|
<span className="text-[10px] text-[#556080] capitalize">{type}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
{typeFilters.size > 0 && (
|
{typeFilters.size > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setTypeFilters(new Set())}
|
onClick={() => setTypeFilters(new Set())}
|
||||||
className="text-xs text-blue-400 hover:text-blue-300 flex-shrink-0 ml-1"
|
className="text-[10px] text-[#0080ff] hover:text-[#00d4ff] flex-shrink-0 ml-1 transition-colors"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
@ -222,11 +225,11 @@ export default function Logs() {
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="flex-1 overflow-y-auto p-4 space-y-2"
|
className="flex-1 overflow-y-auto p-4 space-y-1.5"
|
||||||
>
|
>
|
||||||
{filteredEntries.length === 0 ? (
|
{filteredEntries.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
<div className="flex flex-col items-center justify-center h-full text-[#334060] animate-fade-in">
|
||||||
<Activity className="h-10 w-10 text-gray-600 mb-3" />
|
<Activity className="h-10 w-10 text-[#1a1a3e] mb-3" />
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{paused
|
{paused
|
||||||
? 'Log streaming is paused.'
|
? 'Log streaming is paused.'
|
||||||
@ -236,6 +239,7 @@ export default function Logs() {
|
|||||||
) : (
|
) : (
|
||||||
filteredEntries.map((entry) => {
|
filteredEntries.map((entry) => {
|
||||||
const { event } = entry;
|
const { event } = entry;
|
||||||
|
const badge = eventTypeBadgeColor(event.type);
|
||||||
const detail =
|
const detail =
|
||||||
event.message ??
|
event.message ??
|
||||||
event.content ??
|
event.content ??
|
||||||
@ -251,20 +255,19 @@ export default function Logs() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
className="bg-gray-900 border border-gray-800 rounded-lg p-3 hover:border-gray-700 transition-colors"
|
className="glass-card rounded-lg p-3 hover:border-[#0080ff20] transition-all duration-200"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<span className="text-xs text-gray-500 font-mono whitespace-nowrap mt-0.5">
|
<span className="text-[10px] text-[#334060] font-mono whitespace-nowrap mt-0.5">
|
||||||
{formatTimestamp(event.timestamp)}
|
{formatTimestamp(event.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border capitalize flex-shrink-0 ${eventTypeBadgeColor(
|
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold border capitalize flex-shrink-0 ${badge.classes}`}
|
||||||
event.type,
|
style={{ background: badge.bg }}
|
||||||
)}`}
|
|
||||||
>
|
>
|
||||||
{event.type}
|
{event.type}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-sm text-gray-300 break-all min-w-0">
|
<p className="text-sm text-[#8892a8] break-all min-w-0">
|
||||||
{typeof detail === 'string' ? detail : JSON.stringify(detail)}
|
{typeof detail === 'string' ? detail : JSON.stringify(detail)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -96,8 +96,8 @@ export default function Memory() {
|
|||||||
|
|
||||||
if (error && entries.length === 0) {
|
if (error && entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 animate-fade-in">
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
|
||||||
Failed to load memory: {error}
|
Failed to load memory: {error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,18 +105,18 @@ export default function Memory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Brain className="h-5 w-5 text-blue-400" />
|
<Brain className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
Memory ({entries.length})
|
Memory ({entries.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForm(true)}
|
onClick={() => setShowForm(true)}
|
||||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
|
className="btn-electric flex items-center gap-2 text-sm px-4 py-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Memory
|
Add Memory
|
||||||
@ -126,22 +126,22 @@ export default function Memory() {
|
|||||||
{/* Search and Filter */}
|
{/* Search and Filter */}
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#334060]" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Search memory entries..."
|
placeholder="Search memory entries..."
|
||||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="input-electric w-full pl-10 pr-4 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#334060]" />
|
||||||
<select
|
<select
|
||||||
value={categoryFilter}
|
value={categoryFilter}
|
||||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||||
className="bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-8 py-2.5 text-sm text-white appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
className="input-electric pl-10 pr-8 py-2.5 text-sm appearance-none cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="">All Categories</option>
|
<option value="">All Categories</option>
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
@ -153,7 +153,7 @@ export default function Memory() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
className="px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
className="btn-electric px-4 py-2.5 text-sm"
|
||||||
>
|
>
|
||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
@ -161,15 +161,15 @@ export default function Memory() {
|
|||||||
|
|
||||||
{/* Error banner (non-fatal) */}
|
{/* Error banner (non-fatal) */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-3 text-sm text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-3 text-sm text-[#ff6680] animate-fade-in">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add Memory Form Modal */}
|
{/* Add Memory Form Modal */}
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
<div className="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md mx-4">
|
<div className="glass-card p-6 w-full max-w-md mx-4 animate-fade-in-scale">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-white">Add Memory</h3>
|
<h3 className="text-lg font-semibold text-white">Add Memory</h3>
|
||||||
<button
|
<button
|
||||||
@ -177,45 +177,45 @@ export default function Memory() {
|
|||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-[#556080] hover:text-white transition-colors duration-300"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formError && (
|
{formError && (
|
||||||
<div className="mb-4 rounded-lg bg-red-900/30 border border-red-700 p-3 text-sm text-red-300">
|
<div className="mb-4 rounded-xl bg-[#ff446615] border border-[#ff446630] p-3 text-sm text-[#ff6680] animate-fade-in">
|
||||||
{formError}
|
{formError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
|
||||||
Key <span className="text-red-400">*</span>
|
Key <span className="text-[#ff4466]">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formKey}
|
value={formKey}
|
||||||
onChange={(e) => setFormKey(e.target.value)}
|
onChange={(e) => setFormKey(e.target.value)}
|
||||||
placeholder="e.g. user_preferences"
|
placeholder="e.g. user_preferences"
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="input-electric w-full px-3 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
|
||||||
Content <span className="text-red-400">*</span>
|
Content <span className="text-[#ff4466]">*</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={formContent}
|
value={formContent}
|
||||||
onChange={(e) => setFormContent(e.target.value)}
|
onChange={(e) => setFormContent(e.target.value)}
|
||||||
placeholder="Memory content..."
|
placeholder="Memory content..."
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
className="input-electric w-full px-3 py-2.5 text-sm resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
<label className="block text-xs font-semibold text-[#8892a8] mb-1.5 uppercase tracking-wider">
|
||||||
Category (optional)
|
Category (optional)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -223,7 +223,7 @@ export default function Memory() {
|
|||||||
value={formCategory}
|
value={formCategory}
|
||||||
onChange={(e) => setFormCategory(e.target.value)}
|
onChange={(e) => setFormCategory(e.target.value)}
|
||||||
placeholder="e.g. preferences, context, facts"
|
placeholder="e.g. preferences, context, facts"
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="input-electric w-full px-3 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -234,14 +234,14 @@ export default function Memory() {
|
|||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 rounded-lg hover:bg-gray-800 transition-colors"
|
className="px-4 py-2 text-sm font-medium text-[#8892a8] hover:text-white border border-[#1a1a3e] rounded-xl hover:bg-[#0080ff08] transition-all duration-300"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50"
|
className="btn-electric px-4 py-2 text-sm font-medium"
|
||||||
>
|
>
|
||||||
{submitting ? 'Saving...' : 'Save'}
|
{submitting ? 'Saving...' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
@ -253,70 +253,57 @@ export default function Memory() {
|
|||||||
{/* Memory Table */}
|
{/* Memory Table */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center h-32">
|
<div className="flex items-center justify-center h-32">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : entries.length === 0 ? (
|
) : entries.length === 0 ? (
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-8 text-center">
|
<div className="glass-card p-8 text-center">
|
||||||
<Brain className="h-10 w-10 text-gray-600 mx-auto mb-3" />
|
<Brain className="h-10 w-10 text-[#1a1a3e] mx-auto mb-3" />
|
||||||
<p className="text-gray-400">No memory entries found.</p>
|
<p className="text-[#556080]">No memory entries found.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-x-auto">
|
<div className="glass-card overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="table-electric">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-800">
|
<tr>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-left">Key</th>
|
||||||
Key
|
<th className="text-left">Content</th>
|
||||||
</th>
|
<th className="text-left">Category</th>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-left">Timestamp</th>
|
||||||
Content
|
<th className="text-right">Actions</th>
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Category
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Timestamp
|
|
||||||
</th>
|
|
||||||
<th className="text-right px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<tr
|
<tr key={entry.id}>
|
||||||
key={entry.id}
|
|
||||||
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 text-white font-medium font-mono text-xs">
|
<td className="px-4 py-3 text-white font-medium font-mono text-xs">
|
||||||
{entry.key}
|
{entry.key}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-300 max-w-[300px]">
|
<td className="px-4 py-3 text-[#8892a8] max-w-[300px] text-sm">
|
||||||
<span title={entry.content}>
|
<span title={entry.content}>
|
||||||
{truncate(entry.content, 80)}
|
{truncate(entry.content, 80)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-800 text-gray-300 capitalize">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold capitalize border border-[#1a1a3e] text-[#8892a8]" style={{ background: 'rgba(0,128,255,0.06)' }}>
|
||||||
{entry.category}
|
{entry.category}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-400 text-xs whitespace-nowrap">
|
<td className="px-4 py-3 text-[#556080] text-xs whitespace-nowrap">
|
||||||
{formatDate(entry.timestamp)}
|
{formatDate(entry.timestamp)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
{confirmDelete === entry.key ? (
|
{confirmDelete === entry.key ? (
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2 animate-fade-in">
|
||||||
<span className="text-xs text-red-400">Delete?</span>
|
<span className="text-xs text-[#ff4466]">Delete?</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(entry.key)}
|
onClick={() => handleDelete(entry.key)}
|
||||||
className="text-red-400 hover:text-red-300 text-xs font-medium"
|
className="text-[#ff4466] hover:text-[#ff6680] text-xs font-medium"
|
||||||
>
|
>
|
||||||
Yes
|
Yes
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmDelete(null)}
|
onClick={() => setConfirmDelete(null)}
|
||||||
className="text-gray-400 hover:text-white text-xs font-medium"
|
className="text-[#556080] hover:text-white text-xs font-medium"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
@ -324,7 +311,7 @@ export default function Memory() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmDelete(entry.key)}
|
onClick={() => setConfirmDelete(entry.key)}
|
||||||
className="text-gray-400 hover:text-red-400 transition-colors"
|
className="text-[#334060] hover:text-[#ff4466] transition-all duration-300"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -42,8 +42,8 @@ export default function Tools() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 animate-fade-in">
|
||||||
<div className="rounded-lg bg-red-900/30 border border-red-700 p-4 text-red-300">
|
<div className="rounded-xl bg-[#ff446615] border border-[#ff446630] p-4 text-[#ff6680]">
|
||||||
Failed to load tools: {error}
|
Failed to load tools: {error}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,75 +53,75 @@ export default function Tools() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
|
<div className="h-8 w-8 border-2 border-[#0080ff30] border-t-[#0080ff] rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6 animate-fade-in">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative max-w-md">
|
<div className="relative max-w-md">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#334060]" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="Search tools..."
|
placeholder="Search tools..."
|
||||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="input-electric w-full pl-10 pr-4 py-2.5 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent Tools Grid */}
|
{/* Agent Tools Grid */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Wrench className="h-5 w-5 text-blue-400" />
|
<Wrench className="h-5 w-5 text-[#0080ff]" />
|
||||||
<h2 className="text-base font-semibold text-white">
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
Agent Tools ({filtered.length})
|
Agent Tools ({filtered.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<p className="text-sm text-gray-500">No tools match your search.</p>
|
<p className="text-sm text-[#334060]">No tools match your search.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 stagger-children">
|
||||||
{filtered.map((tool) => {
|
{filtered.map((tool) => {
|
||||||
const isExpanded = expandedTool === tool.name;
|
const isExpanded = expandedTool === tool.name;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tool.name}
|
key={tool.name}
|
||||||
className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"
|
className="glass-card overflow-hidden animate-slide-in-up"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setExpandedTool(isExpanded ? null : tool.name)
|
setExpandedTool(isExpanded ? null : tool.name)
|
||||||
}
|
}
|
||||||
className="w-full text-left p-4 hover:bg-gray-800/50 transition-colors"
|
className="w-full text-left p-4 hover:bg-[#0080ff08] transition-all duration-300"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<Package className="h-4 w-4 text-blue-400 flex-shrink-0 mt-0.5" />
|
<Package className="h-4 w-4 text-[#0080ff] flex-shrink-0 mt-0.5" />
|
||||||
<h3 className="text-sm font-semibold text-white truncate">
|
<h3 className="text-sm font-semibold text-white truncate">
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
<ChevronDown className="h-4 w-4 text-[#0080ff] flex-shrink-0 transition-transform" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
<ChevronRight className="h-4 w-4 text-[#334060] flex-shrink-0 transition-transform" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400 mt-2 line-clamp-2">
|
<p className="text-sm text-[#556080] mt-2 line-clamp-2">
|
||||||
{tool.description}
|
{tool.description}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isExpanded && tool.parameters && (
|
{isExpanded && tool.parameters && (
|
||||||
<div className="border-t border-gray-800 p-4">
|
<div className="border-t border-[#1a1a3e] p-4 animate-fade-in">
|
||||||
<p className="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wider">
|
<p className="text-[10px] text-[#334060] mb-2 font-semibold uppercase tracking-wider">
|
||||||
Parameter Schema
|
Parameter Schema
|
||||||
</p>
|
</p>
|
||||||
<pre className="text-xs text-gray-300 bg-gray-950 rounded-lg p-3 overflow-x-auto max-h-64 overflow-y-auto">
|
<pre className="text-xs text-[#8892a8] rounded-xl p-3 overflow-x-auto max-h-64 overflow-y-auto" style={{ background: 'rgba(5,5,16,0.8)' }}>
|
||||||
{JSON.stringify(tool.parameters, null, 2)}
|
{JSON.stringify(tool.parameters, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -135,49 +135,38 @@ export default function Tools() {
|
|||||||
|
|
||||||
{/* CLI Tools Section */}
|
{/* CLI Tools Section */}
|
||||||
{filteredCli.length > 0 && (
|
{filteredCli.length > 0 && (
|
||||||
<div>
|
<div className="animate-slide-in-up" style={{ animationDelay: '200ms' }}>
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Terminal className="h-5 w-5 text-green-400" />
|
<Terminal className="h-5 w-5 text-[#00e68a]" />
|
||||||
<h2 className="text-base font-semibold text-white">
|
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
CLI Tools ({filteredCli.length})
|
CLI Tools ({filteredCli.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
<div className="glass-card overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="table-electric">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-800">
|
<tr>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-left">Name</th>
|
||||||
Name
|
<th className="text-left">Path</th>
|
||||||
</th>
|
<th className="text-left">Version</th>
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
<th className="text-left">Category</th>
|
||||||
Path
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Version
|
|
||||||
</th>
|
|
||||||
<th className="text-left px-4 py-3 text-gray-400 font-medium">
|
|
||||||
Category
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredCli.map((tool) => (
|
{filteredCli.map((tool) => (
|
||||||
<tr
|
<tr key={tool.name}>
|
||||||
key={tool.name}
|
<td className="px-4 py-3 text-white font-medium text-sm">
|
||||||
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 text-white font-medium">
|
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-400 font-mono text-xs truncate max-w-[200px]">
|
<td className="px-4 py-3 text-[#556080] font-mono text-xs truncate max-w-[200px]">
|
||||||
{tool.path}
|
{tool.path}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-400">
|
<td className="px-4 py-3 text-[#556080] text-sm">
|
||||||
{tool.version ?? '-'}
|
{tool.version ?? '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-800 text-gray-300 capitalize">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold capitalize border border-[#1a1a3e] text-[#8892a8]" style={{ background: 'rgba(0,128,255,0.06)' }}>
|
||||||
{tool.category}
|
{tool.category}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import react from "@vitejs/plugin-react";
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
// Build-only config. The web dashboard is served by the Rust gateway
|
||||||
|
// via rust-embed. Run `npm run build` then `cargo build` to update.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/_app/",
|
base: "/_app/",
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
@ -14,24 +16,4 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
},
|
},
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
"/health": {
|
|
||||||
target: "http://localhost:5555",
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
"/pair": {
|
|
||||||
target: "http://localhost:5555",
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
"/api": {
|
|
||||||
target: "http://localhost:5555",
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
"/ws": {
|
|
||||||
target: "ws://localhost:5555",
|
|
||||||
ws: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user