mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 08:30:52 +00:00
feat(frontend): cleaner UI pass — consistent layout, tokens, skeleton loading (#258)
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#161618" />
|
||||
<meta name="theme-color" content="#070b11" />
|
||||
<meta name="description" content="OpenJarvis — on-device AI assistant" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
||||
Generated
+69
@@ -23,6 +23,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.38",
|
||||
"lucide-react": "^0.576.0",
|
||||
"motion": "^12.38.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -5931,6 +5932,33 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.38.0",
|
||||
"motion-utils": "^12.36.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
@@ -8834,6 +8862,47 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/motion": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
|
||||
"integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.38.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.36.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.36.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.38",
|
||||
"lucide-react": "^0.576.0",
|
||||
"motion": "^12.38.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -63,8 +63,8 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
|
||||
<div
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl mb-3"
|
||||
style={{
|
||||
background: 'var(--color-surface, #f5f5f5)',
|
||||
border: '1px solid var(--color-border, #e0e0e0)',
|
||||
background: 'var(--color-surface)',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<audio ref={audioRef} src={src} preload="metadata" />
|
||||
@@ -73,8 +73,8 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
|
||||
onClick={toggle}
|
||||
className="flex items-center justify-center w-9 h-9 rounded-full transition-colors shrink-0"
|
||||
style={{
|
||||
background: 'var(--color-accent, #6366f1)',
|
||||
color: 'white',
|
||||
background: 'var(--color-accent)',
|
||||
color: 'var(--color-on-accent)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -94,14 +94,14 @@ export function AudioPlayer({ src }: AudioPlayerProps) {
|
||||
|
||||
<div
|
||||
className="h-1.5 rounded-full cursor-pointer"
|
||||
style={{ background: 'var(--color-bg-tertiary, #e0e0e0)' }}
|
||||
style={{ background: 'var(--color-bg-tertiary)' }}
|
||||
onClick={seek}
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
background: 'var(--color-accent, #6366f1)',
|
||||
background: 'var(--color-accent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ChatArea() {
|
||||
<button
|
||||
onClick={() => navigate('/data-sources')}
|
||||
className="px-3 py-1 rounded text-xs font-medium cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff', border: 'none' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)', border: 'none' }}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
|
||||
@@ -338,7 +338,7 @@ export function InputArea() {
|
||||
<button
|
||||
onClick={stopStreaming}
|
||||
className="p-2 rounded-xl transition-colors shrink-0 cursor-pointer"
|
||||
style={{ background: 'var(--color-error)', color: 'white' }}
|
||||
style={{ background: 'var(--color-error)', color: 'var(--color-on-accent)' }}
|
||||
title="Stop generating"
|
||||
>
|
||||
<Square size={16} />
|
||||
|
||||
@@ -319,7 +319,7 @@ export function CommandPalette() {
|
||||
</div>
|
||||
)}
|
||||
{pullSuccess && (
|
||||
<div className="px-4 py-2 text-xs flex items-center gap-1.5" style={{ color: 'var(--color-success)', background: 'rgba(34,197,94,0.05)' }}>
|
||||
<div className="px-4 py-2 text-xs flex items-center gap-1.5" style={{ color: 'var(--color-success)', background: 'color-mix(in srgb, var(--color-success) 5%, transparent)' }}>
|
||||
<Check size={12} /> Downloaded {pullSuccess} successfully
|
||||
</div>
|
||||
)}
|
||||
@@ -395,7 +395,7 @@ export function CommandPalette() {
|
||||
className="flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-medium cursor-pointer"
|
||||
style={{
|
||||
background: justInstalled ? 'var(--color-accent-subtle)' : 'var(--color-accent)',
|
||||
color: justInstalled ? 'var(--color-accent)' : '#fff',
|
||||
color: justInstalled ? 'var(--color-accent)' : 'var(--color-on-accent)',
|
||||
opacity: (isPulling || (pulling && !isPulling)) ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
@@ -420,7 +420,7 @@ export function CommandPalette() {
|
||||
<button
|
||||
onClick={handleCustomPull} disabled={!customModel.trim() || !!pulling}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff', opacity: (!customModel.trim() || pulling) ? 0.5 : 1 }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)', opacity: (!customModel.trim() || pulling) ? 0.5 : 1 }}
|
||||
>
|
||||
<Download size={12} /> Pull
|
||||
</button>
|
||||
@@ -445,7 +445,7 @@ export function CommandPalette() {
|
||||
<Cloud size={14} style={{ color: hasKey ? 'var(--color-success)' : 'var(--color-text-tertiary)' }} />
|
||||
<span className="text-xs font-medium" style={{ color: 'var(--color-text)' }}>{provider.name}</span>
|
||||
{hasKey && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(34,197,94,0.1)', color: 'var(--color-success)' }}>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'color-mix(in srgb, var(--color-success) 10%, transparent)', color: 'var(--color-success)' }}>
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -12,16 +12,13 @@ export function CostComparison() {
|
||||
|
||||
if (!savings || savings.total_tokens === 0) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
|
||||
<DollarSign size={16} style={{ color: 'var(--color-success)' }} />
|
||||
<div className="hud-panel p-6">
|
||||
<h3 className="hud-label flex items-center gap-2 mb-4">
|
||||
<DollarSign size={12} style={{ color: 'var(--color-success)' }} />
|
||||
Cost Comparison
|
||||
</h3>
|
||||
<div className="h-48 flex items-center justify-center text-sm" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
Start chatting to see local vs. cloud cost savings.
|
||||
<span className="hud-mono">awaiting first inference…</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -31,12 +28,9 @@ export function CostComparison() {
|
||||
const completionK = savings.total_completion_tokens / 1000;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
|
||||
<DollarSign size={16} style={{ color: 'var(--color-success)' }} />
|
||||
<div className="hud-panel p-6">
|
||||
<h3 className="hud-label flex items-center gap-2 mb-4">
|
||||
<DollarSign size={12} style={{ color: 'var(--color-success)' }} />
|
||||
Cost Comparison*
|
||||
</h3>
|
||||
|
||||
|
||||
@@ -47,20 +47,15 @@ function StatCard({
|
||||
unit?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg p-4"
|
||||
style={{ background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<div className="hud-panel p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon size={14} style={{ color: 'var(--color-accent)' }} />
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{label}
|
||||
</span>
|
||||
<Icon size={12} style={{ color: 'var(--color-accent)' }} />
|
||||
<span className="hud-label">{label}</span>
|
||||
</div>
|
||||
<div className="text-xl font-semibold truncate" style={{ color: 'var(--color-text)' }}>
|
||||
<div className="hud-mono text-2xl font-semibold truncate" style={{ color: 'var(--color-text)' }}>
|
||||
{value}
|
||||
{unit && (
|
||||
<span className="text-xs font-normal ml-1" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
<span className="hud-label ml-1" style={{ fontSize: '0.625rem', letterSpacing: '0.18em' }}>
|
||||
{unit}
|
||||
</span>
|
||||
)}
|
||||
@@ -118,28 +113,22 @@ export function EnergyDashboard() {
|
||||
|
||||
if (error || !energy) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
|
||||
<Zap size={16} style={{ color: 'var(--color-accent)' }} />
|
||||
<div className="hud-panel p-6">
|
||||
<h3 className="hud-label flex items-center gap-2 mb-4">
|
||||
<Zap size={12} style={{ color: 'var(--color-accent)' }} />
|
||||
Energy Monitoring
|
||||
</h3>
|
||||
<div className="h-48 flex items-center justify-center text-sm" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{error || 'Waiting for energy data from the server...'}
|
||||
<span className="hud-mono">{error || 'awaiting telemetry stream…'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
|
||||
<Zap size={16} style={{ color: 'var(--color-accent)' }} />
|
||||
<div className="hud-panel p-6">
|
||||
<h3 className="hud-label flex items-center gap-2 mb-4">
|
||||
<Zap size={12} style={{ color: 'var(--color-accent)' }} />
|
||||
Energy Monitoring
|
||||
</h3>
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ const STEP_COLORS: Record<string, string> = {
|
||||
route: 'var(--color-accent)',
|
||||
retrieve: 'var(--color-success)',
|
||||
generate: 'var(--color-warning)',
|
||||
tool_call: '#a855f7',
|
||||
respond: '#ec4899',
|
||||
tool_call: 'var(--color-accent-purple)',
|
||||
respond: 'var(--color-accent-purple)',
|
||||
};
|
||||
|
||||
function StepBadge({ type }: { type: string }) {
|
||||
@@ -143,28 +143,22 @@ export function TraceDebugger() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
|
||||
<GitBranch size={16} style={{ color: 'var(--color-accent)' }} />
|
||||
<div className="hud-panel p-6">
|
||||
<h3 className="hud-label flex items-center gap-2 mb-4">
|
||||
<GitBranch size={12} style={{ color: 'var(--color-accent)' }} />
|
||||
Trace Debugger
|
||||
</h3>
|
||||
<div className="h-48 flex items-center justify-center text-sm" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{error}
|
||||
<span className="hud-mono">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
|
||||
<GitBranch size={16} style={{ color: 'var(--color-accent)' }} />
|
||||
<div className="hud-panel p-6">
|
||||
<h3 className="hud-label flex items-center gap-2 mb-4">
|
||||
<GitBranch size={12} style={{ color: 'var(--color-accent)' }} />
|
||||
Trace Debugger
|
||||
</h3>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null })}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: 'white' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
Try again
|
||||
|
||||
@@ -24,7 +24,8 @@ export function Layout() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full overflow-hidden" style={{ paddingTop: '3px' }}>
|
||||
<div className="flex flex-col h-full w-full overflow-hidden relative" style={{ paddingTop: '3px' }}>
|
||||
<div className="hud-backdrop" aria-hidden="true" />
|
||||
<SystemPulse apiReachable={apiReachable} />
|
||||
|
||||
{/* Health check banner */}
|
||||
@@ -32,8 +33,8 @@ export function Layout() {
|
||||
<div
|
||||
className="flex items-center gap-3 px-4 py-2 text-sm shrink-0"
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.08)',
|
||||
borderBottom: '1px solid rgba(239, 68, 68, 0.15)',
|
||||
background: 'color-mix(in srgb, var(--color-error) 8%, transparent)',
|
||||
borderBottom: '1px solid color-mix(in srgb, var(--color-error) 15%, transparent)',
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
>
|
||||
@@ -52,7 +53,7 @@ export function Layout() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<div className="flex flex-1 min-h-0 relative z-10">
|
||||
<Sidebar />
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
@@ -60,8 +61,10 @@ export function Layout() {
|
||||
onClick={() => useAppStore.getState().setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<main className="flex-1 flex flex-col min-w-0 h-full" style={{ background: 'var(--color-bg)' }}>
|
||||
<Outlet />
|
||||
<main className="flex-1 flex flex-col min-w-0 h-full relative overflow-hidden" style={{ background: 'transparent' }}>
|
||||
<div className="flex-1 flex flex-col min-w-0 min-h-0 relative z-[2]">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,7 @@ export function OptInModal({ onClose }: OptInModalProps) {
|
||||
<div
|
||||
className="flex items-center justify-center w-14 h-14 rounded-2xl mb-5 mx-auto"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--color-accent), #6366f1)',
|
||||
background: 'linear-gradient(135deg, var(--color-accent), var(--color-accent-purple))',
|
||||
}}
|
||||
>
|
||||
<Trophy size={28} color="white" />
|
||||
@@ -151,7 +151,7 @@ export function OptInModal({ onClose }: OptInModalProps) {
|
||||
className="px-5 py-2.5 rounded-xl text-sm font-medium transition-colors cursor-pointer"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
color: 'white',
|
||||
color: 'var(--color-on-accent)',
|
||||
}}
|
||||
>
|
||||
Done
|
||||
@@ -240,7 +240,7 @@ export function OptInModal({ onClose }: OptInModalProps) {
|
||||
className="w-full py-2.5 rounded-xl text-sm font-medium transition-all cursor-pointer"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
color: 'white',
|
||||
color: 'var(--color-on-accent)',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.9')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
|
||||
@@ -138,9 +138,9 @@ export function SetupScreen({ onReady }: { onReady: () => void }) {
|
||||
<div
|
||||
className="flex items-start gap-3 px-4 py-3 rounded-xl text-sm"
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.1)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
color: '#ef4444',
|
||||
background: 'color-mix(in srgb, var(--color-error) 10%, transparent)',
|
||||
border: '1px solid color-mix(in srgb, var(--color-error) 20%, transparent)',
|
||||
color: 'var(--color-error)',
|
||||
}}
|
||||
>
|
||||
<XCircle size={16} className="shrink-0 mt-0.5" />
|
||||
|
||||
@@ -193,7 +193,7 @@ export function Sidebar() {
|
||||
<button
|
||||
key={item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors w-full text-left cursor-pointer"
|
||||
className="relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors w-full text-left cursor-pointer"
|
||||
style={{
|
||||
background: isActive ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
color: isActive ? 'var(--color-text)' : 'var(--color-text-secondary)',
|
||||
@@ -206,7 +206,17 @@ export function Sidebar() {
|
||||
if (!isActive) e.currentTarget.style.background = 'transparent';
|
||||
}}
|
||||
>
|
||||
<item.icon size={16} />
|
||||
{isActive && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute left-0 top-1.5 bottom-1.5 w-[2px] rounded-full"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
boxShadow: '0 0 8px var(--color-accent-glow)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<item.icon size={16} style={isActive ? { color: 'var(--color-accent)' } : undefined} />
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -6,15 +6,15 @@ type PulseState = 'idle' | 'inferencing' | 'agent-active' | 'hidden';
|
||||
|
||||
const PULSE_CONFIG: Record<Exclude<PulseState, 'hidden'>, { color: string; animation: string }> = {
|
||||
idle: {
|
||||
color: 'rgba(59, 130, 246, 0.3)',
|
||||
color: 'color-mix(in srgb, var(--color-accent) 22%, transparent)',
|
||||
animation: 'none',
|
||||
},
|
||||
inferencing: {
|
||||
color: '#3b82f6',
|
||||
color: 'var(--color-accent)',
|
||||
animation: 'pulse-glow 2s ease-in-out infinite',
|
||||
},
|
||||
'agent-active': {
|
||||
color: '#8b5cf6',
|
||||
color: 'var(--color-accent-purple)',
|
||||
animation: 'pulse-travel 3s linear infinite',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ function ProgressRow({
|
||||
) : status.state === 'paused' ? (
|
||||
<Loader2 size={14} style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
) : (
|
||||
<AlertCircle size={14} style={{ color: '#ef4444' }} />
|
||||
<AlertCircle size={14} style={{ color: 'var(--color-error)' }} />
|
||||
)}
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{status?.state === 'syncing'
|
||||
@@ -68,13 +68,13 @@ function ProgressRow({
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{
|
||||
background:
|
||||
status?.state === 'error' ? '#ef4444' : 'var(--color-accent)',
|
||||
status?.state === 'error' ? 'var(--color-error)' : 'var(--color-accent)',
|
||||
width: `${pct}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{status?.error && (
|
||||
<p className="text-xs" style={{ color: '#ef4444' }}>
|
||||
<p className="text-xs" style={{ color: 'var(--color-error)' }}>
|
||||
{status.error}
|
||||
</p>
|
||||
)}
|
||||
@@ -162,7 +162,7 @@ export function IngestDashboard({
|
||||
className="w-full py-3 px-4 rounded-xl font-semibold text-sm flex items-center justify-center gap-2 transition-all"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
color: 'white',
|
||||
color: 'var(--color-on-accent)',
|
||||
}}
|
||||
>
|
||||
{!allDone && <Loader2 size={16} className="animate-spin" />}
|
||||
|
||||
@@ -133,7 +133,7 @@ export function ReadyScreen({
|
||||
<button
|
||||
onClick={() => onStart()}
|
||||
className="px-8 py-3 rounded-xl font-semibold text-sm transition-all"
|
||||
style={{ background: 'var(--color-accent)', color: 'white' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
>
|
||||
Open Chat
|
||||
</button>
|
||||
|
||||
@@ -56,7 +56,7 @@ function SidebarItem({
|
||||
) : state === 'skipped' ? (
|
||||
<SkipForward size={16} style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
) : state === 'error' ? (
|
||||
<Circle size={16} style={{ color: '#ef4444' }} />
|
||||
<Circle size={16} style={{ color: 'var(--color-error)' }} />
|
||||
) : (
|
||||
<Circle size={16} style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
)}
|
||||
@@ -198,7 +198,7 @@ function OAuthPanel({
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium self-start transition-all"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
color: 'white',
|
||||
color: 'var(--color-on-accent)',
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
@@ -249,7 +249,7 @@ function LocalPanel({
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium self-start transition-all"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
color: 'white',
|
||||
color: 'var(--color-on-accent)',
|
||||
cursor: isConnecting ? 'not-allowed' : 'pointer',
|
||||
opacity: isConnecting ? 0.7 : 1,
|
||||
}}
|
||||
@@ -342,7 +342,7 @@ function StepByStepPanel({
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
color: '#7c3aed', fontSize: 11,
|
||||
color: 'var(--color-accent-purple)', fontSize: 11,
|
||||
fontWeight: 600, marginBottom: 4,
|
||||
}}>
|
||||
STEP {i + 1}
|
||||
@@ -359,7 +359,7 @@ function StepByStepPanel({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: '#60a5fa', fontSize: 12,
|
||||
color: 'var(--color-accent)', fontSize: 12,
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
@@ -414,8 +414,8 @@ function StepByStepPanel({
|
||||
style={{
|
||||
flex: 1, padding: 10,
|
||||
background: isConnecting || (fields.length > 0 && !allFilled)
|
||||
? '#444' : '#7c3aed',
|
||||
color: 'white', border: 'none',
|
||||
? 'var(--color-disabled-bg)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none',
|
||||
borderRadius: 6, fontSize: 13,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
@@ -531,9 +531,9 @@ export function SourceConnectFlow({
|
||||
<div
|
||||
className="mt-3 px-4 py-3 rounded-lg text-sm"
|
||||
style={{
|
||||
background: 'rgba(239,68,68,0.1)',
|
||||
border: '1px solid rgba(239,68,68,0.2)',
|
||||
color: '#ef4444',
|
||||
background: 'color-mix(in srgb, var(--color-error) 10%, transparent)',
|
||||
border: '1px solid color-mix(in srgb, var(--color-error) 20%, transparent)',
|
||||
color: 'var(--color-error)',
|
||||
}}
|
||||
>
|
||||
{activeEntry.error}
|
||||
@@ -593,7 +593,7 @@ export function SourceConnectFlow({
|
||||
<button
|
||||
onClick={onComplete}
|
||||
className="w-full py-3 px-4 rounded-xl font-semibold text-sm transition-all"
|
||||
style={{ background: 'var(--color-accent)', color: 'white' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
>
|
||||
Continue →
|
||||
</button>
|
||||
|
||||
+294
-67
@@ -23,9 +23,10 @@
|
||||
--color-text-tertiary: #a1a1aa;
|
||||
--color-text-inverse: #fafafa;
|
||||
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #2563eb;
|
||||
--color-accent-subtle: #eff6ff;
|
||||
--color-accent: #0891b2;
|
||||
--color-accent-hover: #0e7490;
|
||||
--color-accent-subtle: rgba(8, 145, 178, 0.08);
|
||||
--color-accent-glow: rgba(8, 145, 178, 0.25);
|
||||
|
||||
--color-accent-amber: #f59e0b;
|
||||
--color-accent-amber-hover: #d97706;
|
||||
@@ -33,7 +34,7 @@
|
||||
--color-accent-purple: #8b5cf6;
|
||||
--color-accent-purple-hover: #7c3aed;
|
||||
--color-accent-purple-subtle: rgba(139, 92, 246, 0.1);
|
||||
--color-accent-blue-subtle: rgba(59, 130, 246, 0.1);
|
||||
--color-accent-blue-subtle: rgba(8, 145, 178, 0.08);
|
||||
|
||||
--color-success: #16a34a;
|
||||
--color-warning: #ca8a04;
|
||||
@@ -43,8 +44,10 @@
|
||||
--color-input-bg: #ffffff;
|
||||
--color-input-border: #d4d4d8;
|
||||
|
||||
--color-user-bubble: #3b82f6;
|
||||
--color-user-bubble: #0891b2;
|
||||
--color-user-bubble-text: #ffffff;
|
||||
--color-on-accent: #ffffff;
|
||||
--color-disabled-bg: #e4e4e7;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
@@ -59,94 +62,103 @@
|
||||
--sidebar-width: 260px;
|
||||
--chat-max-width: 720px;
|
||||
|
||||
--font-display: 'Chakra Petch', 'Space Grotesk', system-ui, sans-serif;
|
||||
--font-hud: 'IBM Plex Mono', 'SF Mono', 'JetBrains Mono', ui-monospace, monospace;
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-bg: #161618;
|
||||
--color-bg-secondary: #1e1e21;
|
||||
--color-bg-tertiary: #2a2a2d;
|
||||
--color-surface: #1e1e21;
|
||||
--color-sidebar: rgba(30, 30, 33, 0.85);
|
||||
--color-border: rgba(255, 255, 255, 0.08);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.04);
|
||||
--color-bg: #0a0a0b;
|
||||
--color-bg-secondary: #121214;
|
||||
--color-bg-tertiary: #18181b;
|
||||
--color-surface: #121214;
|
||||
--color-sidebar: rgba(12, 12, 14, 0.72);
|
||||
--color-border: rgba(255, 255, 255, 0.09);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.05);
|
||||
|
||||
--color-text: #e8e8ed;
|
||||
--color-text-secondary: #8e8e93;
|
||||
--color-text-tertiary: #636366;
|
||||
--color-text-inverse: #161618;
|
||||
--color-text: #ededef;
|
||||
--color-text-secondary: #8d8d93;
|
||||
--color-text-tertiary: #5c5c63;
|
||||
--color-text-inverse: #0a0a0b;
|
||||
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #60a5fa;
|
||||
--color-accent-subtle: #172554;
|
||||
--color-accent: #22d3ee;
|
||||
--color-accent-hover: #67e8f9;
|
||||
--color-accent-subtle: rgba(34, 211, 238, 0.1);
|
||||
--color-accent-glow: rgba(34, 211, 238, 0.3);
|
||||
|
||||
--color-accent-amber: #f59e0b;
|
||||
--color-accent-amber: #f5a524;
|
||||
--color-accent-amber-hover: #fbbf24;
|
||||
--color-accent-amber-subtle: rgba(245, 158, 11, 0.15);
|
||||
--color-accent-purple: #8b5cf6;
|
||||
--color-accent-purple-hover: #a78bfa;
|
||||
--color-accent-purple-subtle: rgba(139, 92, 246, 0.15);
|
||||
--color-accent-blue-subtle: rgba(59, 130, 246, 0.15);
|
||||
--color-accent-amber-subtle: rgba(245, 165, 36, 0.12);
|
||||
--color-accent-purple: #b794ff;
|
||||
--color-accent-purple-hover: #c9b0ff;
|
||||
--color-accent-purple-subtle: rgba(183, 148, 255, 0.12);
|
||||
--color-accent-blue-subtle: rgba(79, 209, 255, 0.12);
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #eab308;
|
||||
--color-error: #ef4444;
|
||||
--color-success: #3ddc97;
|
||||
--color-warning: #f5a524;
|
||||
--color-error: #ff6b6b;
|
||||
|
||||
--color-code-bg: #1e1e22;
|
||||
--color-input-bg: #1e1e21;
|
||||
--color-input-border: rgba(255, 255, 255, 0.1);
|
||||
--color-code-bg: #0c121a;
|
||||
--color-input-bg: #0c121a;
|
||||
--color-input-border: rgba(79, 209, 255, 0.12);
|
||||
|
||||
--color-user-bubble: #3b82f6;
|
||||
--color-user-bubble-text: #ffffff;
|
||||
--color-user-bubble: rgba(79, 209, 255, 0.14);
|
||||
--color-user-bubble-text: #e6f1ff;
|
||||
--color-on-accent: #070b11;
|
||||
--color-disabled-bg: #121a24;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.4);
|
||||
--shadow-md: 0 4px 14px -2px rgb(0 0 0 / 0.5), 0 0 0 1px rgba(79, 209, 255, 0.04);
|
||||
--shadow-lg: 0 18px 40px -12px rgb(0 0 0 / 0.7), 0 0 0 1px rgba(79, 209, 255, 0.06);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) {
|
||||
--color-bg: #161618;
|
||||
--color-bg-secondary: #1e1e21;
|
||||
--color-bg-tertiary: #2a2a2d;
|
||||
--color-surface: #1e1e21;
|
||||
--color-sidebar: rgba(30, 30, 33, 0.85);
|
||||
--color-border: rgba(255, 255, 255, 0.08);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.04);
|
||||
--color-bg: #0a0a0b;
|
||||
--color-bg-secondary: #121214;
|
||||
--color-bg-tertiary: #18181b;
|
||||
--color-surface: #121214;
|
||||
--color-sidebar: rgba(12, 12, 14, 0.72);
|
||||
--color-border: rgba(255, 255, 255, 0.09);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.05);
|
||||
|
||||
--color-text: #e8e8ed;
|
||||
--color-text-secondary: #8e8e93;
|
||||
--color-text-tertiary: #636366;
|
||||
--color-text-inverse: #161618;
|
||||
--color-text: #ededef;
|
||||
--color-text-secondary: #8d8d93;
|
||||
--color-text-tertiary: #5c5c63;
|
||||
--color-text-inverse: #0a0a0b;
|
||||
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #60a5fa;
|
||||
--color-accent-subtle: #172554;
|
||||
--color-accent: #22d3ee;
|
||||
--color-accent-hover: #67e8f9;
|
||||
--color-accent-subtle: rgba(34, 211, 238, 0.1);
|
||||
--color-accent-glow: rgba(34, 211, 238, 0.3);
|
||||
|
||||
--color-accent-amber: #f59e0b;
|
||||
--color-accent-amber: #f5a524;
|
||||
--color-accent-amber-hover: #fbbf24;
|
||||
--color-accent-amber-subtle: rgba(245, 158, 11, 0.15);
|
||||
--color-accent-purple: #8b5cf6;
|
||||
--color-accent-purple-hover: #a78bfa;
|
||||
--color-accent-purple-subtle: rgba(139, 92, 246, 0.15);
|
||||
--color-accent-blue-subtle: rgba(59, 130, 246, 0.15);
|
||||
--color-accent-amber-subtle: rgba(245, 165, 36, 0.12);
|
||||
--color-accent-purple: #b794ff;
|
||||
--color-accent-purple-hover: #c9b0ff;
|
||||
--color-accent-purple-subtle: rgba(183, 148, 255, 0.12);
|
||||
--color-accent-blue-subtle: rgba(79, 209, 255, 0.12);
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #eab308;
|
||||
--color-error: #ef4444;
|
||||
--color-success: #3ddc97;
|
||||
--color-warning: #f5a524;
|
||||
--color-error: #ff6b6b;
|
||||
|
||||
--color-code-bg: #1e1e22;
|
||||
--color-input-bg: #1e1e21;
|
||||
--color-input-border: rgba(255, 255, 255, 0.1);
|
||||
--color-code-bg: #0c121a;
|
||||
--color-input-bg: #0c121a;
|
||||
--color-input-border: rgba(79, 209, 255, 0.12);
|
||||
|
||||
--color-user-bubble: #3b82f6;
|
||||
--color-user-bubble-text: #ffffff;
|
||||
--color-user-bubble: rgba(79, 209, 255, 0.14);
|
||||
--color-user-bubble-text: #e6f1ff;
|
||||
--color-on-accent: #070b11;
|
||||
--color-disabled-bg: #121a24;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.4);
|
||||
--shadow-md: 0 4px 14px -2px rgb(0 0 0 / 0.5), 0 0 0 1px rgba(79, 209, 255, 0.04);
|
||||
--shadow-lg: 0 18px 40px -12px rgb(0 0 0 / 0.7), 0 0 0 1px rgba(79, 209, 255, 0.06);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -377,6 +389,221 @@
|
||||
0% { background-position: -200% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
/* HUD ambient animations (CSS-only — kept out of JS for zero-cost continuous motion) */
|
||||
@keyframes hud-breathe {
|
||||
0%, 100% { box-shadow: 0 0 0 1px var(--color-border), 0 0 0 0 var(--color-accent-glow); }
|
||||
50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 22%, transparent), 0 0 24px -4px var(--color-accent-glow); }
|
||||
}
|
||||
|
||||
@keyframes hud-sweep {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes hud-reticle {
|
||||
0% { transform: rotate(0deg); opacity: 0.4; }
|
||||
50% { opacity: 0.9; }
|
||||
100% { transform: rotate(360deg); opacity: 0.4; }
|
||||
}
|
||||
|
||||
.hud-label {
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.hud-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hud-mono {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.hud-panel {
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-surface) 92%, transparent),
|
||||
var(--color-surface)
|
||||
);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
transition: border-color 240ms ease, box-shadow 240ms ease, transform 240ms ease;
|
||||
}
|
||||
|
||||
.hud-panel:hover {
|
||||
border-color: color-mix(in srgb, var(--color-text) 14%, transparent);
|
||||
}
|
||||
|
||||
.data-skeleton {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.data-skeleton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--color-text) 6%, transparent) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: hud-sweep 1.6s linear infinite;
|
||||
}
|
||||
|
||||
.hud-shimmer {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--color-accent) 18%, transparent) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: hud-sweep 2.4s linear infinite;
|
||||
}
|
||||
|
||||
.hud-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 0%, color-mix(in srgb, var(--color-accent) 10%, transparent), transparent 42%),
|
||||
radial-gradient(circle at 85% 110%, color-mix(in srgb, var(--color-accent-purple) 8%, transparent), transparent 46%),
|
||||
linear-gradient(to bottom, transparent, transparent);
|
||||
}
|
||||
|
||||
.hud-backdrop::before {
|
||||
/* faint grid */
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--color-border-subtle) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--color-border-subtle) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||
-webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.hud-backdrop::after {
|
||||
/* noise grain */
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.31 0 0 0 0 0.82 0 0 0 0 1 0 0 0 0.035 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hud-reticle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.hud-reticle::before,
|
||||
.hud-reticle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.hud-reticle::before {
|
||||
inset: 0;
|
||||
opacity: 0.4;
|
||||
border-style: dashed;
|
||||
animation: hud-reticle 6s linear infinite;
|
||||
}
|
||||
|
||||
.hud-reticle::after {
|
||||
inset: 6px;
|
||||
opacity: 0.6;
|
||||
animation: hud-breathe 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Additional motion */
|
||||
@keyframes hud-grid-drift {
|
||||
0% { background-position: 0 0, 0 0; }
|
||||
100% { background-position: 48px 48px, 48px 48px; }
|
||||
}
|
||||
|
||||
@keyframes hud-blink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes hud-heartbeat {
|
||||
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 var(--color-accent-glow); }
|
||||
40% { transform: scale(1.3); box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-accent) 0%, transparent); }
|
||||
}
|
||||
|
||||
|
||||
/* Activate slow grid drift on backdrop */
|
||||
.hud-backdrop::before {
|
||||
animation: hud-grid-drift 60s linear infinite;
|
||||
}
|
||||
|
||||
/* Live heartbeat dot — use next to hud-label for alive indicator */
|
||||
.hud-heartbeat {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
animation: hud-heartbeat 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Blinking caret — place after titles */
|
||||
.hud-caret {
|
||||
display: inline-block;
|
||||
width: 0.5ch;
|
||||
height: 0.85em;
|
||||
margin-left: 0.25ch;
|
||||
background: var(--color-accent);
|
||||
vertical-align: text-bottom;
|
||||
animation: hud-blink 1.1s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
/* HUD panel header — static hairline (no animation) */
|
||||
.hud-panel > h3,
|
||||
.hud-panel-head {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hud-panel > h3::after,
|
||||
.hud-panel-head::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: -6px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--color-accent), transparent);
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hud-shimmer,
|
||||
.hud-reticle::before,
|
||||
.hud-reticle::after,
|
||||
.hud-backdrop::before,
|
||||
.hud-scan-beam::before,
|
||||
.hud-heartbeat,
|
||||
.hud-caret,
|
||||
.hud-panel > h3::after,
|
||||
.hud-panel-head::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
|
||||
@@ -13,6 +13,13 @@ import type {
|
||||
} from '../types';
|
||||
import type { ManagedAgent } from './api';
|
||||
|
||||
export interface CachedConnector {
|
||||
connector_id: string;
|
||||
display_name: string;
|
||||
connected: boolean;
|
||||
chunks: number;
|
||||
}
|
||||
|
||||
export interface AgentEvent {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
@@ -172,6 +179,10 @@ interface AppState {
|
||||
toggleSystemPanel: () => void;
|
||||
setSystemPanelOpen: (open: boolean) => void;
|
||||
|
||||
// Data sources (cached between visits to avoid empty-state flicker)
|
||||
cachedConnectors: CachedConnector[] | null;
|
||||
setCachedConnectors: (list: CachedConnector[] | null) => void;
|
||||
|
||||
// Agents
|
||||
managedAgents: ManagedAgent[];
|
||||
managedAgentsLoading: boolean;
|
||||
@@ -403,6 +414,9 @@ export const useAppStore = create<AppState>((set, get) => {
|
||||
setServerInfo: (info: ServerInfo | null) => set({ serverInfo: info }),
|
||||
setSavings: (data: SavingsData | null) => set({ savings: data }),
|
||||
|
||||
cachedConnectors: null,
|
||||
setCachedConnectors: (list) => set({ cachedConnectors: list }),
|
||||
|
||||
// ── Settings ───────────────────────────────────────────────────
|
||||
|
||||
updateSettings: (partial: Partial<Settings>) => {
|
||||
|
||||
+129
-120
@@ -82,18 +82,18 @@ type AgentStatus =
|
||||
| 'stalled';
|
||||
|
||||
const STATUS_COLOR: Record<AgentStatus, string> = {
|
||||
idle: '#22c55e',
|
||||
running: '#3b82f6',
|
||||
paused: '#6b7280',
|
||||
error: '#ef4444',
|
||||
archived: '#6b7280',
|
||||
needs_attention: '#f59e0b',
|
||||
budget_exceeded: '#f97316',
|
||||
stalled: '#eab308',
|
||||
idle: 'var(--color-success)',
|
||||
running: 'var(--color-accent)',
|
||||
paused: 'var(--color-text-tertiary)',
|
||||
error: 'var(--color-error)',
|
||||
archived: 'var(--color-text-tertiary)',
|
||||
needs_attention: 'var(--color-warning)',
|
||||
budget_exceeded: 'var(--color-warning)',
|
||||
stalled: 'var(--color-warning)',
|
||||
};
|
||||
|
||||
function statusColor(s: string): string {
|
||||
return STATUS_COLOR[s as AgentStatus] || '#6b7280';
|
||||
return STATUS_COLOR[s as AgentStatus] || 'var(--color-text-tertiary)';
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
@@ -793,7 +793,7 @@ function LaunchWizard({
|
||||
onClick={() => selectTemplate(tpl)}
|
||||
className="text-left p-4 rounded-lg transition-all items-start"
|
||||
style={{ border: '1px solid var(--color-border)', background: 'var(--color-bg-secondary)' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--color-accent)'; e.currentTarget.style.background = 'rgba(124,58,237,0.06)'; }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--color-accent)'; e.currentTarget.style.background = 'color-mix(in srgb, var(--color-accent-purple) 6%, transparent)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.background = 'var(--color-bg-secondary)'; }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -804,7 +804,7 @@ function LaunchWizard({
|
||||
{(tpl as any).tools && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{((tpl as any).tools as string[]).slice(0, 4).map((t: string) => (
|
||||
<span key={t} className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'rgba(124,58,237,0.12)', color: '#a78bfa' }}>{t}</span>
|
||||
<span key={t} className="text-xs px-1.5 py-0.5 rounded" style={{ background: 'color-mix(in srgb, var(--color-accent-purple) 12%, transparent)', color: 'var(--color-accent-purple)' }}>{t}</span>
|
||||
))}
|
||||
{((tpl as any).tools as string[]).length > 4 && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded" style={{ color: 'var(--color-text-tertiary)' }}>+{((tpl as any).tools as string[]).length - 4}</span>
|
||||
@@ -817,7 +817,7 @@ function LaunchWizard({
|
||||
onClick={() => selectTemplate(null)}
|
||||
className="text-left p-4 rounded-lg transition-all items-start"
|
||||
style={{ border: '1px solid var(--color-border)', background: 'var(--color-bg-secondary)' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--color-accent)'; e.currentTarget.style.background = 'rgba(124,58,237,0.06)'; }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--color-accent)'; e.currentTarget.style.background = 'color-mix(in srgb, var(--color-accent-purple) 6%, transparent)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.background = 'var(--color-bg-secondary)'; }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -871,7 +871,7 @@ function LaunchWizard({
|
||||
style={{ border: '1px solid var(--color-border)', color: 'var(--color-text)' }}
|
||||
/>
|
||||
{wizard.instruction.includes('[') && (
|
||||
<p className="text-[10px] mt-1" style={{ color: '#f59e0b' }}>
|
||||
<p className="text-[10px] mt-1" style={{ color: 'var(--color-warning)' }}>
|
||||
Replace the [bracketed text] with your own values
|
||||
</p>
|
||||
)}
|
||||
@@ -951,7 +951,7 @@ function LaunchWizard({
|
||||
className="px-1.5 py-1 rounded text-xs font-medium"
|
||||
style={{
|
||||
background: isSelected ? 'var(--color-accent)' : 'var(--color-bg)',
|
||||
color: isSelected ? '#fff' : 'var(--color-text-tertiary)',
|
||||
color: isSelected ? 'var(--color-on-accent)' : 'var(--color-text-tertiary)',
|
||||
border: `1px solid ${isSelected ? 'var(--color-accent)' : 'var(--color-border)'}`,
|
||||
}}
|
||||
>
|
||||
@@ -1013,7 +1013,7 @@ function LaunchWizard({
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{wizard.selectedTools.map((t) => (
|
||||
<span key={t} className="text-xs px-2 py-1 rounded" style={{ background: 'rgba(124,58,237,0.12)', color: '#a78bfa' }}>{t}</span>
|
||||
<span key={t} className="text-xs px-2 py-1 rounded" style={{ background: 'color-mix(in srgb, var(--color-accent-purple) 12%, transparent)', color: 'var(--color-accent-purple)' }}>{t}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1102,7 +1102,7 @@ function LaunchWizard({
|
||||
onClick={handleLaunch}
|
||||
disabled={launching || !wizard.name.trim()}
|
||||
className="flex-1 py-2.5 rounded-lg text-sm font-semibold"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff', opacity: launching || !wizard.name.trim() ? 0.5 : 1 }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)', opacity: launching || !wizard.name.trim() ? 0.5 : 1 }}
|
||||
>
|
||||
{launching ? 'Creating...' : 'Launch Agent'}
|
||||
</button>
|
||||
@@ -1163,7 +1163,7 @@ function OverflowMenu({
|
||||
setOpen(false);
|
||||
}}
|
||||
className="w-full text-left px-3 py-1.5 text-xs cursor-pointer flex items-center gap-2"
|
||||
style={{ color: '#ef4444' }}
|
||||
style={{ color: 'var(--color-error)' }}
|
||||
>
|
||||
<Trash2 size={12} /> Delete
|
||||
</button>
|
||||
@@ -1256,10 +1256,10 @@ function AgentCard({
|
||||
width: `${Math.min(100, ((agent.total_cost ?? 0) / (agent.config?.max_cost as number)) * 100)}%`,
|
||||
background:
|
||||
((agent.total_cost ?? 0) / (agent.config?.max_cost as number)) > 0.9
|
||||
? '#ef4444'
|
||||
? 'var(--color-error)'
|
||||
: ((agent.total_cost ?? 0) / (agent.config?.max_cost as number)) > 0.75
|
||||
? '#f59e0b'
|
||||
: '#22c55e',
|
||||
? 'var(--color-warning)'
|
||||
: 'var(--color-success)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1306,7 +1306,7 @@ function AgentCard({
|
||||
<button
|
||||
onClick={() => onResume(agent.id)}
|
||||
className="p-1 rounded cursor-pointer"
|
||||
style={{ color: '#22c55e' }}
|
||||
style={{ color: 'var(--color-success)' }}
|
||||
title="Resume"
|
||||
>
|
||||
<Play size={13} />
|
||||
@@ -1316,7 +1316,7 @@ function AgentCard({
|
||||
<button
|
||||
onClick={() => onRecover(agent.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded text-xs cursor-pointer"
|
||||
style={{ background: '#ef444420', color: '#ef4444' }}
|
||||
style={{ background: 'var(--color-error)20', color: 'var(--color-error)' }}
|
||||
title="Recover agent"
|
||||
>
|
||||
<AlertTriangle size={11} /> Recover
|
||||
@@ -1376,7 +1376,7 @@ function AgentInstructionSection({ agent, onAgentUpdated }: { agent: ManagedAgen
|
||||
style={{ border: '1px solid var(--color-border)', color: 'var(--color-text)' }}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={save} className="text-xs px-3 py-1 rounded font-medium cursor-pointer" style={{ background: 'var(--color-accent)', color: '#fff' }}>Save</button>
|
||||
<button onClick={save} className="text-xs px-3 py-1 rounded font-medium cursor-pointer" style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}>Save</button>
|
||||
<button onClick={() => setEditing(false)} className="text-xs px-3 py-1 rounded cursor-pointer" style={{ color: 'var(--color-text-tertiary)', border: '1px solid var(--color-border)' }}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1461,10 +1461,10 @@ function AgentConfigGrid({ agent, onAgentUpdated }: { agent: ManagedAgent; onAge
|
||||
}
|
||||
|
||||
const modelStatusDot = modelAvailable === 'available'
|
||||
? '#22c55e'
|
||||
? 'var(--color-success)'
|
||||
: modelAvailable === 'unavailable'
|
||||
? '#ef4444'
|
||||
: '#888';
|
||||
? 'var(--color-error)'
|
||||
: 'var(--color-text-tertiary)';
|
||||
|
||||
const rows: [string, React.ReactNode][] = [
|
||||
['Intelligence', editingModel ? (
|
||||
@@ -1482,7 +1482,7 @@ function AgentConfigGrid({ agent, onAgentUpdated }: { agent: ManagedAgent; onAge
|
||||
{models.map((m) => {
|
||||
const loaded = isModelLoaded(m);
|
||||
return (
|
||||
<option key={m} value={m} style={!loaded ? { color: '#888' } : undefined}>
|
||||
<option key={m} value={m} style={!loaded ? { color: 'var(--color-text-tertiary)' } : undefined}>
|
||||
{m}{!loaded ? ' (not loaded)' : ''}
|
||||
</option>
|
||||
);
|
||||
@@ -1508,14 +1508,14 @@ function AgentConfigGrid({ agent, onAgentUpdated }: { agent: ManagedAgent; onAge
|
||||
/>
|
||||
<span style={{ color: 'var(--color-text)' }}>{currentModel}</span>
|
||||
{modelAvailable === 'unavailable' && (
|
||||
<span className="text-xs" style={{ color: '#ef4444' }}>Not available</span>
|
||||
<span className="text-xs" style={{ color: 'var(--color-error)' }}>Not available</span>
|
||||
)}
|
||||
<button
|
||||
onClick={startEditingModel}
|
||||
className="text-xs px-2 py-0.5 rounded cursor-pointer"
|
||||
style={{
|
||||
color: modelAvailable === 'unavailable' ? '#ef4444' : 'var(--color-accent)',
|
||||
border: `1px solid ${modelAvailable === 'unavailable' ? '#ef4444' : 'var(--color-accent)'}`,
|
||||
color: modelAvailable === 'unavailable' ? 'var(--color-error)' : 'var(--color-accent)',
|
||||
border: `1px solid ${modelAvailable === 'unavailable' ? 'var(--color-error)' : 'var(--color-accent)'}`,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
@@ -1933,7 +1933,7 @@ function InteractTab({ agentId, agentStatus }: { agentId: string; agentStatus: s
|
||||
className="max-w-[75%] px-3 py-2 rounded-lg text-sm"
|
||||
style={{
|
||||
background: msg.direction === 'user_to_agent' ? 'var(--color-accent)' : 'var(--color-bg-secondary)',
|
||||
color: msg.direction === 'user_to_agent' ? '#fff' : 'var(--color-text)',
|
||||
color: msg.direction === 'user_to_agent' ? 'var(--color-on-accent)' : 'var(--color-text)',
|
||||
border: msg.direction === 'agent_to_user' ? '1px solid var(--color-border)' : 'none',
|
||||
}}
|
||||
>
|
||||
@@ -2035,7 +2035,7 @@ function InteractTab({ agentId, agentStatus }: { agentId: string; agentStatus: s
|
||||
onClick={() => handleSend('immediate')}
|
||||
disabled={sending || waitingForResponse || !input.trim()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm cursor-pointer font-medium"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff', opacity: sending || !input.trim() ? 0.5 : 1 }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)', opacity: sending || !input.trim() ? 0.5 : 1 }}
|
||||
>
|
||||
<Send size={13} /> Send
|
||||
</button>
|
||||
@@ -2142,7 +2142,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
key={c.connector_id}
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: '1px solid #2a5a3a',
|
||||
border: '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
gridColumn: isReconnecting ? '1 / -1' : undefined,
|
||||
@@ -2157,7 +2157,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
{c.display_name}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: c.chunks > 0 ? '#4ade80' : '#f59e0b' }}>
|
||||
<div style={{ fontSize: 12, color: c.chunks > 0 ? 'var(--color-success)' : 'var(--color-warning)' }}>
|
||||
{c.chunks > 0
|
||||
? `${c.chunks.toLocaleString()} ${unit}`
|
||||
: 'Connected — no data synced yet'}
|
||||
@@ -2182,7 +2182,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
padding: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 12, color: '#f59e0b',
|
||||
fontSize: 12, color: 'var(--color-warning)',
|
||||
marginBottom: 8,
|
||||
}}>
|
||||
Re-enter credentials to reconnect this source.
|
||||
@@ -2198,7 +2198,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
color: '#7c3aed', fontSize: 10,
|
||||
color: 'var(--color-accent-purple)', fontSize: 10,
|
||||
fontWeight: 600, marginBottom: 3,
|
||||
}}>
|
||||
STEP {i + 1}
|
||||
@@ -2212,7 +2212,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: '#60a5fa', fontSize: 11,
|
||||
color: 'var(--color-accent)', fontSize: 11,
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
@@ -2280,7 +2280,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
</div>
|
||||
</div>
|
||||
<span style={{
|
||||
color: '#7c3aed', fontSize: 11, fontWeight: 500,
|
||||
color: 'var(--color-accent-purple)', fontSize: 11, fontWeight: 500,
|
||||
}}>
|
||||
{isExpanded ? '\u2715 Close' : '+ Add'}
|
||||
</span>
|
||||
@@ -2303,7 +2303,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
color: '#7c3aed', fontSize: 10,
|
||||
color: 'var(--color-accent-purple)', fontSize: 10,
|
||||
fontWeight: 600, marginBottom: 3,
|
||||
}}>
|
||||
STEP {i + 1}
|
||||
@@ -2319,7 +2319,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: '#60a5fa', fontSize: 11,
|
||||
color: 'var(--color-accent)', fontSize: 11,
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
@@ -2410,8 +2410,8 @@ function InlineConnectForm({
|
||||
disabled={loading || !allFilled}
|
||||
style={{
|
||||
width: '100%', padding: 8,
|
||||
background: loading || !allFilled ? '#444' : '#7c3aed',
|
||||
color: 'white', border: 'none',
|
||||
background: loading || !allFilled ? 'var(--color-disabled-bg)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none',
|
||||
borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -2498,19 +2498,19 @@ function SendBlueWebhookStep({
|
||||
return (
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 14, background: 'var(--color-bg)' }}>
|
||||
<div style={{
|
||||
background: '#052e16', border: '1px solid #2a5a3a',
|
||||
background: 'color-mix(in srgb, var(--color-success) 10%, var(--color-bg))', border: '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)',
|
||||
borderRadius: 6, padding: 12, marginBottom: 12, textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ fontSize: 11, color: '#4ade80', fontWeight: 600, marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-success)', fontWeight: 600, marginBottom: 4 }}>
|
||||
{'\u2713'} Your agent is now reachable via iMessage / SMS
|
||||
</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#4ade80' }}>{selectedNumber}</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-success)' }}>{selectedNumber}</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook / ngrok step */}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<span style={{ background: '#7c3aed', color: 'white', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>4</span>
|
||||
<span style={{ background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>4</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Set up webhook to receive texts</span>
|
||||
</div>
|
||||
<div style={{
|
||||
@@ -2519,7 +2519,7 @@ function SendBlueWebhookStep({
|
||||
padding: '8px 10px', marginBottom: 10,
|
||||
background: 'var(--color-bg-secondary)',
|
||||
borderRadius: 6,
|
||||
borderLeft: '3px solid var(--color-accent, #7c3aed)',
|
||||
borderLeft: '3px solid var(--color-accent, var(--color-accent-purple))',
|
||||
}}>
|
||||
<div><strong>1.</strong> Open a terminal and run: <code style={{ color: 'var(--color-accent)', background: 'var(--color-bg)', padding: '1px 4px', borderRadius: 3 }}>ngrok http 8000</code></div>
|
||||
<div style={{ marginTop: 4 }}><strong>2.</strong> Copy the <code style={{ color: 'var(--color-accent)', background: 'var(--color-bg)', padding: '1px 4px', borderRadius: 3 }}>https://</code> forwarding URL</div>
|
||||
@@ -2541,8 +2541,8 @@ function SendBlueWebhookStep({
|
||||
disabled={!webhookUrl.trim() || webhookStatus === 'registering'}
|
||||
style={{
|
||||
fontSize: 11, padding: '7px 14px', whiteSpace: 'nowrap' as const,
|
||||
background: webhookStatus === 'done' ? '#22c55e' : '#7c3aed',
|
||||
color: 'white', border: 'none', borderRadius: 5,
|
||||
background: webhookStatus === 'done' ? 'var(--color-success)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: !webhookUrl.trim() || webhookStatus === 'registering' ? 0.5 : 1,
|
||||
}}
|
||||
@@ -2554,17 +2554,17 @@ function SendBlueWebhookStep({
|
||||
</button>
|
||||
</div>
|
||||
{webhookStatus === 'done' && (
|
||||
<div style={{ fontSize: 11, color: '#22c55e', marginTop: 6 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-success)', marginTop: 6 }}>
|
||||
Webhook registered! Incoming texts will be forwarded to your agent.
|
||||
</div>
|
||||
)}
|
||||
{webhookStatus === 'error' && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 6 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 6 }}>
|
||||
Failed to register. Check your ngrok URL and try again.
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 10, color: 'var(--color-text-tertiary)', marginTop: 8 }}>
|
||||
Don't have ngrok? <a href="https://ngrok.com/download" target="_blank" rel="noopener noreferrer" style={{ color: '#60a5fa', textDecoration: 'underline' }}>Download it free</a>
|
||||
Don't have ngrok? <a href="https://ngrok.com/download" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--color-accent)', textDecoration: 'underline' }}>Download it free</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2623,12 +2623,12 @@ function SendBlueWizard({
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: isActive ? '1px solid #2a5a3a' : '1px dashed var(--color-border)',
|
||||
border: isActive ? '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)' : '1px dashed var(--color-border)',
|
||||
borderRadius: 8, marginBottom: 10, overflow: 'hidden',
|
||||
};
|
||||
|
||||
const btnPrimary: React.CSSProperties = {
|
||||
fontSize: 12, padding: '7px 18px', background: '#7c3aed', color: 'white',
|
||||
fontSize: 12, padding: '7px 18px', background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 5, cursor: 'pointer', fontWeight: 600,
|
||||
};
|
||||
|
||||
@@ -2719,7 +2719,7 @@ function SendBlueWizard({
|
||||
<span style={{ fontSize: 18, marginRight: 10 }}>{'\uD83D\uDCAC'}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>iMessage / SMS</div>
|
||||
<div style={{ fontSize: 11, color: healthy ? '#4ade80' : '#f59e0b' }}>
|
||||
<div style={{ fontSize: 11, color: healthy ? 'var(--color-success)' : 'var(--color-warning)' }}>
|
||||
{healthy ? `Active on ${activeNumber}` : `Disconnected — ${activeNumber}`}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2734,8 +2734,8 @@ function SendBlueWizard({
|
||||
</button>
|
||||
)}
|
||||
<span style={{
|
||||
background: healthy ? '#2a5a3a' : '#78350f',
|
||||
color: healthy ? '#4ade80' : '#f59e0b',
|
||||
background: healthy ? 'color-mix(in srgb, var(--color-success) 22%, transparent)' : 'color-mix(in srgb, var(--color-warning) 18%, var(--color-bg))',
|
||||
color: healthy ? 'var(--color-success)' : 'var(--color-warning)',
|
||||
padding: '2px 8px', borderRadius: 10, fontSize: 10, fontWeight: 600,
|
||||
}}>{healthy ? 'Active' : 'Disconnected'}</span>
|
||||
<button onClick={() => setExpanded(true)} style={btnSecondary}>
|
||||
@@ -2755,11 +2755,11 @@ function SendBlueWizard({
|
||||
<span style={{ fontSize: 18, marginRight: 10 }}>{'\uD83D\uDCAC'}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>iMessage / SMS</div>
|
||||
<div style={{ fontSize: 11, color: '#4ade80' }}>Active on {activeNumber}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-success)' }}>Active on {activeNumber}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button onClick={() => setExpanded(false)} style={btnSecondary}>Collapse</button>
|
||||
<button onClick={() => onRemove(binding!.id)} style={{ ...btnSecondary, color: '#f87171' }}>Remove</button>
|
||||
<button onClick={() => onRemove(binding!.id)} style={{ ...btnSecondary, color: 'var(--color-error)' }}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 14, background: 'var(--color-bg)' }}>
|
||||
@@ -2786,7 +2786,7 @@ function SendBlueWizard({
|
||||
{testSent ? 'Sent!' : 'Send Test'}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div style={{ color: '#f87171', fontSize: 11, marginTop: 6 }}>{error}</div>}
|
||||
{error && <div style={{ color: 'var(--color-error)', fontSize: 11, marginTop: 6 }}>{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -2809,7 +2809,7 @@ function SendBlueWizard({
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setStep(step === 'idle' ? 'creds' : 'idle'); }}
|
||||
style={{ fontSize: 10, padding: '3px 12px', background: '#7c3aed', color: 'white', border: 'none', borderRadius: 5, cursor: 'pointer', fontWeight: 600 }}
|
||||
style={{ fontSize: 10, padding: '3px 12px', background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)', border: 'none', borderRadius: 5, cursor: 'pointer', fontWeight: 600 }}
|
||||
>
|
||||
{step === 'idle' ? 'Set Up' : 'Cancel'}
|
||||
</button>
|
||||
@@ -2819,7 +2819,7 @@ function SendBlueWizard({
|
||||
{(step === 'creds' || step === 'verifying') && (
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 14, background: 'var(--color-bg)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<span style={{ background: '#7c3aed', color: 'white', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>1</span>
|
||||
<span style={{ background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>1</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Create a SendBlue account</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -2830,12 +2830,12 @@ function SendBlueWizard({
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<span style={{ background: '#7c3aed', color: 'white', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>2</span>
|
||||
<span style={{ background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>2</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Paste your API credentials</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 8 }}>
|
||||
Go to your{' '}
|
||||
<a href="https://dashboard.sendblue.co/api-credentials" target="_blank" rel="noopener noreferrer" style={{ color: '#60a5fa', textDecoration: 'underline' }}>
|
||||
<a href="https://dashboard.sendblue.co/api-credentials" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--color-accent)', textDecoration: 'underline' }}>
|
||||
SendBlue API Credentials page
|
||||
</a>{' '}
|
||||
and copy the API Key and API Secret.
|
||||
@@ -2854,7 +2854,7 @@ function SendBlueWizard({
|
||||
<input value={apiSecret} onChange={(e) => setApiSecret(e.target.value)} placeholder="Your API secret key" type="password" style={inputStyle} />
|
||||
</div>
|
||||
|
||||
{error && <div style={{ color: '#f87171', fontSize: 11, marginBottom: 8 }}>{error}</div>}
|
||||
{error && <div style={{ color: 'var(--color-error)', fontSize: 11, marginBottom: 8 }}>{error}</div>}
|
||||
|
||||
<button
|
||||
onClick={handleVerify}
|
||||
@@ -2870,12 +2870,12 @@ function SendBlueWizard({
|
||||
{(step === 'verified' || step === 'connecting') && (
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 14, background: 'var(--color-bg)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<span style={{ background: '#22c55e', color: 'white', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>{'\u2713'}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#4ade80' }}>Credentials verified</span>
|
||||
<span style={{ background: 'var(--color-success)', color: 'var(--color-on-accent)', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>{'\u2713'}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-success)' }}>Credentials verified</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<span style={{ background: '#7c3aed', color: 'white', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>3</span>
|
||||
<span style={{ background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>3</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Your agent's phone number</span>
|
||||
</div>
|
||||
|
||||
@@ -2894,13 +2894,13 @@ function SendBlueWizard({
|
||||
</div>
|
||||
) : numbers.length === 1 ? (
|
||||
<div style={{
|
||||
background: 'var(--color-bg-secondary)', border: '1px solid #2a5a3a',
|
||||
background: 'var(--color-bg-secondary)', border: '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)',
|
||||
borderRadius: 6, padding: '10px 12px', marginBottom: 12,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<span style={{ fontSize: 20 }}>{'\uD83D\uDCF1'}</span>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: '#4ade80' }}>{selectedNumber}</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-success)' }}>{selectedNumber}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)' }}>This will be your agent's phone number</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2910,7 +2910,7 @@ function SendBlueWizard({
|
||||
fontSize: 11, color: 'var(--color-text-secondary)',
|
||||
marginBottom: 8, lineHeight: 1.5,
|
||||
padding: '8px 10px', background: 'var(--color-bg-secondary)',
|
||||
borderRadius: 6, borderLeft: '3px solid #7c3aed',
|
||||
borderRadius: 6, borderLeft: '3px solid var(--color-accent-purple)',
|
||||
}}>
|
||||
Copy the phone number shown under <strong>"Send from"</strong> in your SendBlue dashboard
|
||||
and paste it below. On the free tier this is a shared number.
|
||||
@@ -2927,7 +2927,7 @@ function SendBlueWizard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div style={{ color: '#f87171', fontSize: 11, marginBottom: 8 }}>{error}</div>}
|
||||
{error && <div style={{ color: 'var(--color-error)', fontSize: 11, marginBottom: 8 }}>{error}</div>}
|
||||
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
@@ -3045,7 +3045,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: binding
|
||||
? '1px solid #2a5a3a'
|
||||
? '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)'
|
||||
: '1px dashed var(--color-border)',
|
||||
borderRadius: 8, marginBottom: 10,
|
||||
overflow: 'hidden',
|
||||
@@ -3061,7 +3061,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{ch.name}</div>
|
||||
<div style={{
|
||||
fontSize: 11,
|
||||
color: binding ? '#4ade80' : 'var(--color-text-secondary)',
|
||||
color: binding ? 'var(--color-success)' : 'var(--color-text-secondary)',
|
||||
}}>
|
||||
{binding ? ch.activeLabel(cfg) : ch.description}
|
||||
</div>
|
||||
@@ -3069,7 +3069,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
{binding ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{
|
||||
background: '#2a5a3a', color: '#4ade80',
|
||||
background: 'color-mix(in srgb, var(--color-success) 22%, transparent)', color: 'var(--color-success)',
|
||||
padding: '2px 8px', borderRadius: 10,
|
||||
fontSize: 10, fontWeight: 600,
|
||||
}}>Active</span>
|
||||
@@ -3092,7 +3092,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
}}
|
||||
style={{
|
||||
fontSize: 10, padding: '3px 12px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
}}
|
||||
@@ -3134,7 +3134,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
padding: '8px 10px',
|
||||
background: 'var(--color-bg-secondary)',
|
||||
borderRadius: 6,
|
||||
borderLeft: '3px solid var(--color-accent, #7c3aed)',
|
||||
borderLeft: '3px solid var(--color-accent, var(--color-accent-purple))',
|
||||
}}>
|
||||
{ch.setupSteps.map((step, i) => {
|
||||
if (step.startsWith('COPYABLE:')) {
|
||||
@@ -3156,7 +3156,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
style={{
|
||||
position: 'sticky', float: 'right', top: 0,
|
||||
fontSize: 10, padding: '2px 8px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 3,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
}}
|
||||
@@ -3199,7 +3199,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
disabled={loading || !canConnect}
|
||||
style={{
|
||||
fontSize: 12, padding: '7px 20px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: loading || !canConnect ? 0.5 : 1,
|
||||
@@ -3250,8 +3250,8 @@ function LearningTab({ agentId, learningEnabled }: { agentId: string; learningEn
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded-full"
|
||||
style={{
|
||||
background: learningEnabled ? '#22c55e20' : 'var(--color-bg-secondary)',
|
||||
color: learningEnabled ? '#22c55e' : 'var(--color-text-tertiary)',
|
||||
background: learningEnabled ? 'var(--color-success)20' : 'var(--color-bg-secondary)',
|
||||
color: learningEnabled ? 'var(--color-success)' : 'var(--color-text-tertiary)',
|
||||
}}
|
||||
>
|
||||
{learningEnabled ? 'Enabled' : 'Disabled'}
|
||||
@@ -3263,7 +3263,7 @@ function LearningTab({ agentId, learningEnabled }: { agentId: string; learningEn
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs cursor-pointer font-medium"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
color: '#fff',
|
||||
color: 'var(--color-on-accent)',
|
||||
opacity: triggering ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
@@ -3354,11 +3354,11 @@ function LogsTab({ agentId }: { agentId: string }) {
|
||||
].sort((a, b) => b.ts - a.ts);
|
||||
|
||||
const learningEventColor = (eventType: string) => {
|
||||
if (eventType === 'query_start') return '#3b82f6';
|
||||
if (eventType === 'query_complete') return '#22c55e';
|
||||
if (eventType === 'tool_call') return '#f59e0b';
|
||||
if (eventType === 'tool_result') return '#8b5cf6';
|
||||
if (eventType === 'query_error') return '#ef4444';
|
||||
if (eventType === 'query_start') return 'var(--color-accent)';
|
||||
if (eventType === 'query_complete') return 'var(--color-success)';
|
||||
if (eventType === 'tool_call') return 'var(--color-warning)';
|
||||
if (eventType === 'tool_result') return 'var(--color-accent-purple)';
|
||||
if (eventType === 'query_error') return 'var(--color-error)';
|
||||
return 'var(--color-text-secondary)';
|
||||
};
|
||||
|
||||
@@ -3442,7 +3442,7 @@ function LogsTab({ agentId }: { agentId: string }) {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full inline-block"
|
||||
style={{ background: t.outcome === 'success' ? '#22c55e' : '#ef4444' }}
|
||||
style={{ background: t.outcome === 'success' ? 'var(--color-success)' : 'var(--color-error)' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--color-text)' }}>{t.outcome}</span>
|
||||
<span
|
||||
@@ -3455,10 +3455,10 @@ function LogsTab({ agentId }: { agentId: string }) {
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded font-medium"
|
||||
style={{
|
||||
background: errorDetail.error_type === 'fatal' ? '#ef444420' :
|
||||
errorDetail.error_type === 'escalate' ? '#f59e0b20' : '#3b82f620',
|
||||
color: errorDetail.error_type === 'fatal' ? '#ef4444' :
|
||||
errorDetail.error_type === 'escalate' ? '#f59e0b' : '#3b82f6',
|
||||
background: errorDetail.error_type === 'fatal' ? 'var(--color-error)20' :
|
||||
errorDetail.error_type === 'escalate' ? 'var(--color-warning)20' : 'var(--color-accent)20',
|
||||
color: errorDetail.error_type === 'fatal' ? 'var(--color-error)' :
|
||||
errorDetail.error_type === 'escalate' ? 'var(--color-warning)' : 'var(--color-accent)',
|
||||
}}
|
||||
>
|
||||
{errorDetail.error_type}
|
||||
@@ -3649,7 +3649,8 @@ export function AgentsPage() {
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-10">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={() => setSelectedAgentId(null)}
|
||||
@@ -3680,7 +3681,7 @@ export function AgentsPage() {
|
||||
{detailTab === 'interact' ? (
|
||||
<span
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs"
|
||||
style={{ background: '#22c55e20', color: '#22c55e', border: '1px solid #22c55e40' }}
|
||||
style={{ background: 'var(--color-success)20', color: 'var(--color-success)', border: '1px solid var(--color-success)40' }}
|
||||
>
|
||||
<MessageSquare size={13} /> Chat ready — just type below
|
||||
</span>
|
||||
@@ -3688,7 +3689,7 @@ export function AgentsPage() {
|
||||
<button
|
||||
onClick={() => handleRun(selectedAgent.id)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm cursor-pointer font-medium"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
>
|
||||
<Zap size={13} /> Run Now
|
||||
</button>
|
||||
@@ -3706,7 +3707,7 @@ export function AgentsPage() {
|
||||
<button
|
||||
onClick={() => handleResume(selectedAgent.id)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm cursor-pointer"
|
||||
style={{ background: '#22c55e20', color: '#22c55e', border: '1px solid #22c55e40' }}
|
||||
style={{ background: 'var(--color-success)20', color: 'var(--color-success)', border: '1px solid var(--color-success)40' }}
|
||||
>
|
||||
<Play size={13} /> Resume
|
||||
</button>
|
||||
@@ -3715,7 +3716,7 @@ export function AgentsPage() {
|
||||
<button
|
||||
onClick={() => handleRecover(selectedAgent.id)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm cursor-pointer"
|
||||
style={{ background: '#ef444420', color: '#ef4444', border: '1px solid #ef444440' }}
|
||||
style={{ background: 'var(--color-error)20', color: 'var(--color-error)', border: '1px solid var(--color-error)40' }}
|
||||
>
|
||||
<AlertTriangle size={13} /> Recover
|
||||
</button>
|
||||
@@ -3729,7 +3730,7 @@ export function AgentsPage() {
|
||||
}
|
||||
}}
|
||||
className="p-1.5 rounded-lg cursor-pointer transition-colors"
|
||||
style={{ color: '#ef4444', background: '#ef444415' }}
|
||||
style={{ color: 'var(--color-error)', background: 'var(--color-error)15' }}
|
||||
title="Delete agent"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
@@ -3852,11 +3853,11 @@ export function AgentsPage() {
|
||||
<p style={sectionTitle}>Local Utilization</p>
|
||||
<div className="flex gap-5">
|
||||
<div>
|
||||
<p className="text-xl font-bold leading-none" style={{ color: '#22c55e' }}>{fmtFlops}</p>
|
||||
<p className="text-xl font-bold leading-none" style={{ color: 'var(--color-success)' }}>{fmtFlops}</p>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--color-text-tertiary)' }}>Compute</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xl font-bold leading-none" style={{ color: '#22c55e' }}>{energyKj.toFixed(2)} kJ</p>
|
||||
<p className="text-xl font-bold leading-none" style={{ color: 'var(--color-success)' }}>{energyKj.toFixed(2)} kJ</p>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--color-text-tertiary)' }}>Energy</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3870,7 +3871,7 @@ export function AgentsPage() {
|
||||
const cost = (inTok / 1e6) * p.inPer1M + (outTok / 1e6) * p.outPer1M;
|
||||
return (
|
||||
<div key={p.label}>
|
||||
<p className="text-xl font-bold leading-none" style={{ color: '#22c55e' }}>${cost.toFixed(4)}</p>
|
||||
<p className="text-xl font-bold leading-none" style={{ color: 'var(--color-success)' }}>${cost.toFixed(4)}</p>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--color-text-tertiary)' }}>{p.label}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -3971,6 +3972,7 @@ export function AgentsPage() {
|
||||
{detailTab === 'logs' && (
|
||||
<LogsTab agentId={selectedAgent.id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3978,7 +3980,8 @@ export function AgentsPage() {
|
||||
// ── List View ───────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-10">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Launch wizard modal */}
|
||||
{showWizard && (
|
||||
<LaunchWizard
|
||||
@@ -3991,29 +3994,34 @@ export function AgentsPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-xl font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Agents
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => agentManagerAvailable && setShowWizard(true)}
|
||||
disabled={agentManagerAvailable === false}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: agentManagerAvailable === false ? 'var(--color-bg-tertiary)' : 'var(--color-accent)',
|
||||
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : '#fff',
|
||||
}}
|
||||
>
|
||||
<Plus size={15} /> New Agent
|
||||
</button>
|
||||
</div>
|
||||
<header className="mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Agents
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => agentManagerAvailable && setShowWizard(true)}
|
||||
disabled={agentManagerAvailable === false}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: agentManagerAvailable === false ? 'var(--color-bg-tertiary)' : 'var(--color-accent)',
|
||||
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
|
||||
}}
|
||||
>
|
||||
<Plus size={15} /> New Agent
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm mt-2 max-w-2xl" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Long-running autonomous agents that can monitor sources, run tasks on a schedule, and message you through connected channels.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{agentManagerAvailable === false && (
|
||||
<div
|
||||
className="mx-4 mt-2 px-4 py-3 rounded-lg flex items-center gap-3 text-sm"
|
||||
style={{
|
||||
background: 'var(--color-accent-amber-subtle)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.2)',
|
||||
border: '1px solid color-mix(in srgb, var(--color-warning) 20%, transparent)',
|
||||
color: 'var(--color-accent-amber)',
|
||||
}}
|
||||
>
|
||||
@@ -4062,13 +4070,14 @@ export function AgentsPage() {
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: agentManagerAvailable === false ? 'var(--color-bg-tertiary)' : 'var(--color-accent)',
|
||||
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : '#fff',
|
||||
color: agentManagerAvailable === false ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
|
||||
}}
|
||||
>
|
||||
<Plus size={15} /> Launch your first agent
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import { EnergyDashboard } from '../components/Dashboard/EnergyDashboard';
|
||||
import { CostComparison } from '../components/Dashboard/CostComparison';
|
||||
import { TraceDebugger } from '../components/Dashboard/TraceDebugger';
|
||||
|
||||
export function DashboardPage() {
|
||||
const now = new Date();
|
||||
const stamp = now.toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-10">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<BarChart3 size={24} style={{ color: 'var(--color-accent)' }} />
|
||||
<h1 className="text-xl font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Dashboard
|
||||
</h1>
|
||||
</div>
|
||||
<header className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
System Overview
|
||||
</h1>
|
||||
<div className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{stamp}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm mt-2 max-w-2xl" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Live telemetry for the on-device inference engine — power draw, token throughput, and cost savings versus cloud APIs.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<EnergyDashboard />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { useAppStore } from '../lib/store';
|
||||
import {
|
||||
fetchManagedAgents,
|
||||
@@ -15,7 +16,12 @@ import {
|
||||
} from '../lib/api';
|
||||
import type { ChannelBinding, ManagedAgent, MemoryStats, MemorySearchResult } from '../lib/api';
|
||||
import { getBase, isTauri } from '../lib/api';
|
||||
import { Database, MessageSquare, Loader2, Brain, Search, FolderOpen, FileText } from 'lucide-react';
|
||||
import {
|
||||
Database, MessageSquare, Loader2, Brain, Search, FolderOpen, FileText,
|
||||
Mail, Hash, MessageCircle, CalendarDays, Contact, StickyNote, BookText,
|
||||
Package, Upload, Link2, PhoneCall,
|
||||
} from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { SOURCE_CATALOG } from '../types/connectors';
|
||||
import type { ConnectRequest } from '../types/connectors';
|
||||
import { listConnectors, connectSource, getSyncStatus, triggerSync } from '../lib/connectors-api';
|
||||
@@ -81,8 +87,8 @@ function InlineConnectForm({
|
||||
disabled={loading || !allFilled}
|
||||
style={{
|
||||
width: '100%', padding: 8,
|
||||
background: loading || !allFilled ? '#444' : '#7c3aed',
|
||||
color: 'white', border: 'none',
|
||||
background: loading || !allFilled ? 'var(--color-disabled-bg)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none',
|
||||
borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -167,7 +173,7 @@ function UploadForm({ onDone }: { onDone?: () => void }) {
|
||||
const tabStyle = (active: boolean): React.CSSProperties => ({
|
||||
flex: 1, padding: '6px 0', textAlign: 'center',
|
||||
fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
||||
background: active ? '#7c3aed' : 'transparent',
|
||||
background: active ? 'var(--color-accent-purple)' : 'transparent',
|
||||
color: active ? 'white' : 'var(--color-text-secondary)',
|
||||
border: 'none', borderRadius: 4,
|
||||
});
|
||||
@@ -221,8 +227,8 @@ function UploadForm({ onDone }: { onDone?: () => void }) {
|
||||
disabled={busy || !content.trim()}
|
||||
style={{
|
||||
width: '100%', padding: 8,
|
||||
background: busy || !content.trim() ? '#444' : '#7c3aed',
|
||||
color: 'white', border: 'none',
|
||||
background: busy || !content.trim() ? 'var(--color-disabled-bg)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none',
|
||||
borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -253,8 +259,8 @@ function UploadForm({ onDone }: { onDone?: () => void }) {
|
||||
disabled={busy || files.length === 0}
|
||||
style={{
|
||||
width: '100%', padding: 8,
|
||||
background: busy || files.length === 0 ? '#444' : '#7c3aed',
|
||||
color: 'white', border: 'none',
|
||||
background: busy || files.length === 0 ? 'var(--color-disabled-bg)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none',
|
||||
borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
@@ -264,12 +270,12 @@ function UploadForm({ onDone }: { onDone?: () => void }) {
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div style={{ fontSize: 12, color: '#4ade80', marginTop: 8 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-success)', marginTop: 8 }}>
|
||||
{result}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div style={{ fontSize: 12, color: '#ef4444', marginTop: 8 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-error)', marginTop: 8 }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -281,12 +287,29 @@ function UploadForm({ onDone }: { onDone?: () => void }) {
|
||||
// Icon map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
gmail: '\u2709\uFE0F', gmail_imap: '\u2709\uFE0F', gmail_api: '\u2709\uFE0F', slack: '#',
|
||||
imessage: '\uD83D\uDCAC', gdrive: '\uD83D\uDCC1', notion: '\uD83D\uDCC4',
|
||||
obsidian: '\uD83D\uDCC1', granola: '\uD83C\uDF99\uFE0F', gcalendar: '\uD83D\uDCC5',
|
||||
gcontacts: '\uD83D\uDCC7', apple_contacts: '📇', outlook: '\u2709\uFE0F', apple_notes: '\uD83C\uDF4E',
|
||||
dropbox: '\uD83D\uDCE6', whatsapp: '\uD83D\uDCF1', upload: '\uD83D\uDCC2',
|
||||
const iconMap: Record<string, LucideIcon> = {
|
||||
gmail: Mail,
|
||||
gmail_imap: Mail,
|
||||
gmail_api: Mail,
|
||||
outlook: Mail,
|
||||
slack: Hash,
|
||||
imessage: MessageCircle,
|
||||
whatsapp: PhoneCall,
|
||||
gdrive: FolderOpen,
|
||||
dropbox: Package,
|
||||
notion: BookText,
|
||||
obsidian: FileText,
|
||||
apple_notes: StickyNote,
|
||||
granola: FileText,
|
||||
gcalendar: CalendarDays,
|
||||
gcontacts: Contact,
|
||||
apple_contacts: Contact,
|
||||
upload: Upload,
|
||||
};
|
||||
|
||||
const IconFor = ({ id, size = 18 }: { id: string; size?: number }) => {
|
||||
const Ico = iconMap[id] ?? Link2;
|
||||
return <Ico size={size} />;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -327,7 +350,7 @@ function SyncStatusDisplay({
|
||||
if (sync?.error) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#ef4444', marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-error)', marginBottom: 4 }}>
|
||||
Error: {sync.error}
|
||||
</div>
|
||||
<button
|
||||
@@ -335,7 +358,7 @@ function SyncStatusDisplay({
|
||||
disabled={syncing}
|
||||
style={{
|
||||
fontSize: 10, padding: '2px 10px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 3,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: syncing ? 0.5 : 1,
|
||||
@@ -350,7 +373,7 @@ function SyncStatusDisplay({
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, color: '#4ade80' }}>
|
||||
<span style={{ fontSize: 12, color: 'var(--color-success)' }}>
|
||||
{chunks.toLocaleString()} {unitLabel}
|
||||
</span>
|
||||
<button
|
||||
@@ -366,7 +389,7 @@ function SyncStatusDisplay({
|
||||
>{syncing ? '...' : 'Re-sync'}</button>
|
||||
</div>
|
||||
{syncError && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 4 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 4 }}>
|
||||
{syncError}
|
||||
</div>
|
||||
)}
|
||||
@@ -386,7 +409,7 @@ function SyncStatusDisplay({
|
||||
: 'Starting...';
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#f59e0b', marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-warning)', marginBottom: 4 }}>
|
||||
Syncing — {label}
|
||||
</div>
|
||||
<div style={{
|
||||
@@ -396,7 +419,7 @@ function SyncStatusDisplay({
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 2,
|
||||
background: '#f59e0b',
|
||||
background: 'var(--color-warning)',
|
||||
width: pct != null ? `${pct}%` : '30%',
|
||||
transition: 'width 0.5s ease',
|
||||
animationName: pct == null ? 'pulse' : undefined,
|
||||
@@ -412,7 +435,7 @@ function SyncStatusDisplay({
|
||||
if (sync?.state === 'idle' && sync.items_synced > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#f59e0b', marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-warning)', marginBottom: 4 }}>
|
||||
Indexing {sync.items_synced.toLocaleString()} items...
|
||||
</div>
|
||||
<div style={{
|
||||
@@ -421,7 +444,7 @@ function SyncStatusDisplay({
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 2, background: '#f59e0b',
|
||||
height: '100%', borderRadius: 2, background: 'var(--color-warning)',
|
||||
width: '60%',
|
||||
animationName: 'pulse', animationDuration: '1.5s', animationIterationCount: 'infinite',
|
||||
}} />
|
||||
@@ -445,7 +468,7 @@ function SyncStatusDisplay({
|
||||
disabled={syncing}
|
||||
style={{
|
||||
fontSize: 10, padding: '2px 10px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 3,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: syncing ? 0.5 : 1,
|
||||
@@ -458,7 +481,7 @@ function SyncStatusDisplay({
|
||||
</div>
|
||||
)}
|
||||
{syncError && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 4 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 4 }}>
|
||||
{syncError}
|
||||
</div>
|
||||
)}
|
||||
@@ -467,9 +490,10 @@ function SyncStatusDisplay({
|
||||
}
|
||||
|
||||
function DataSourcesSection() {
|
||||
const [connectors, setConnectors] = useState<
|
||||
Array<{ connector_id: string; display_name: string; connected: boolean; chunks: number }>
|
||||
>([]);
|
||||
const cachedConnectors = useAppStore((s) => s.cachedConnectors);
|
||||
const setCachedConnectors = useAppStore((s) => s.setCachedConnectors);
|
||||
const connectors = cachedConnectors ?? [];
|
||||
const isFirstLoad = cachedConnectors === null;
|
||||
const [syncStatuses, setSyncStatuses] = useState<Record<string, SyncStatus>>({});
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -477,7 +501,7 @@ function DataSourcesSection() {
|
||||
const loadConnectors = useCallback(() => {
|
||||
listConnectors()
|
||||
.then((list) =>
|
||||
setConnectors(
|
||||
setCachedConnectors(
|
||||
list.map((c) => ({
|
||||
connector_id: c.connector_id,
|
||||
display_name: c.display_name,
|
||||
@@ -487,7 +511,9 @@ function DataSourcesSection() {
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
}, [setCachedConnectors]);
|
||||
|
||||
const setConnectors = setCachedConnectors;
|
||||
|
||||
// Poll sync status for connected sources
|
||||
const loadSyncStatuses = useCallback(async () => {
|
||||
@@ -580,15 +606,41 @@ function DataSourcesSection() {
|
||||
? notConnectedBase
|
||||
: [...notConnectedBase, uploadEntry];
|
||||
|
||||
if (isFirstLoad) {
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<section>
|
||||
<div className="hud-label mb-2" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
Loading sources…
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hud-panel data-skeleton"
|
||||
style={{
|
||||
padding: '14px 18px',
|
||||
height: 60,
|
||||
opacity: 0.6 - i * 0.08,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Connected sources grid */}
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Connected sources */}
|
||||
{connected.length > 0 && (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 6, marginBottom: 12,
|
||||
}}>
|
||||
<section>
|
||||
<div className="hud-label mb-2 flex items-center gap-2">
|
||||
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: 999, background: 'var(--color-success)' }} />
|
||||
Connected · {connected.length}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{connected.map((c) => {
|
||||
const meta = SOURCE_CATALOG.find(s => s.connector_id === c.connector_id);
|
||||
const unit = meta?.unitLabel || 'items';
|
||||
@@ -598,21 +650,19 @@ function DataSourcesSection() {
|
||||
return (
|
||||
<div
|
||||
key={c.connector_id}
|
||||
className="hud-panel"
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: hasError ? '1px solid #7f1d1d' : '1px solid #2a5a3a',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
gridColumn: isReconnecting ? '1 / -1' : undefined,
|
||||
borderColor: hasError
|
||||
? 'color-mix(in srgb, var(--color-error) 28%, transparent)'
|
||||
: 'var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
padding: '12px 14px',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '14px 18px',
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
}}>
|
||||
<span style={{ fontSize: 20 }}>{iconMap[c.connector_id] || '\uD83D\uDD17'}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="font-semibold" style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
{c.display_name}
|
||||
</div>
|
||||
<SyncStatusDisplay
|
||||
@@ -625,12 +675,14 @@ function DataSourcesSection() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setExpandedId(isReconnecting ? null : c.connector_id)}
|
||||
className="hud-label"
|
||||
style={{
|
||||
fontSize: 10, padding: '3px 10px',
|
||||
padding: '6px 12px',
|
||||
background: 'transparent',
|
||||
color: 'var(--color-text-secondary)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 4, cursor: 'pointer',
|
||||
letterSpacing: '0.15em',
|
||||
}}
|
||||
>
|
||||
{isReconnecting ? 'Cancel' : 'Reconnect'}
|
||||
@@ -638,7 +690,7 @@ function DataSourcesSection() {
|
||||
</div>
|
||||
{isReconnecting && meta?.steps && (
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 12 }}>
|
||||
<div style={{ fontSize: 12, color: '#f59e0b', marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-warning)', marginBottom: 8 }}>
|
||||
Re-enter credentials to reconnect this source.
|
||||
</div>
|
||||
{meta.steps.map((step, i) => (
|
||||
@@ -651,7 +703,7 @@ function DataSourcesSection() {
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#7c3aed', fontSize: 10, fontWeight: 600, marginBottom: 3 }}>
|
||||
<div style={{ color: 'var(--color-accent-purple)', fontSize: 10, fontWeight: 600, marginBottom: 3 }}>
|
||||
STEP {i + 1}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, marginBottom: step.url ? 4 : 0 }}>{step.label}</div>
|
||||
@@ -660,7 +712,7 @@ function DataSourcesSection() {
|
||||
href={step.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#60a5fa', fontSize: 11, textDecoration: 'underline' }}
|
||||
style={{ color: 'var(--color-accent)', fontSize: 11, textDecoration: 'underline' }}
|
||||
>
|
||||
{step.urlLabel || 'Open'} →
|
||||
</a>
|
||||
@@ -679,16 +731,18 @@ function DataSourcesSection() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Not connected grid */}
|
||||
{/* Not connected list */}
|
||||
{notConnected.length > 0 && (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 6,
|
||||
}}>
|
||||
<section>
|
||||
<div className="hud-label mb-2 flex items-center gap-2">
|
||||
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: 999, background: 'var(--color-text-tertiary)' }} />
|
||||
Available · {notConnected.length}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{notConnected.map((c) => {
|
||||
const meta = SOURCE_CATALOG.find(s => s.connector_id === c.connector_id);
|
||||
const isExpanded = expandedId === c.connector_id;
|
||||
@@ -696,33 +750,31 @@ function DataSourcesSection() {
|
||||
return (
|
||||
<div
|
||||
key={c.connector_id}
|
||||
className="hud-panel"
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: '1px dashed var(--color-border)',
|
||||
borderRadius: 6, overflow: 'hidden',
|
||||
opacity: isExpanded ? 1 : 0.6,
|
||||
gridColumn: isExpanded ? '1 / -1' : undefined,
|
||||
opacity: isExpanded ? 1 : 0.85,
|
||||
borderStyle: isExpanded ? 'solid' : 'dashed',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 14px', display: 'flex',
|
||||
alignItems: 'center', gap: 8,
|
||||
alignItems: 'center', gap: 12,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setExpandedId(isExpanded ? null : c.connector_id)}
|
||||
>
|
||||
<span style={{ fontSize: 20 }}>{iconMap[c.connector_id] || '\uD83D\uDD17'}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text-secondary)' }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="font-semibold" style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
{c.display_name}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-tertiary)', marginTop: 2 }}>
|
||||
Not connected
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ color: '#7c3aed', fontSize: 11, fontWeight: 500 }}>
|
||||
{isExpanded ? '\u2715 Close' : '+ Add'}
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: 12, fontWeight: 500 }}>
|
||||
{isExpanded ? '× Close' : '+ Add'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -747,7 +799,7 @@ function DataSourcesSection() {
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#7c3aed', fontSize: 10, fontWeight: 600, marginBottom: 3 }}>
|
||||
<div style={{ color: 'var(--color-accent-purple)', fontSize: 10, fontWeight: 600, marginBottom: 3 }}>
|
||||
STEP {i + 1}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, marginBottom: step.url ? 4 : 0 }}>{step.label}</div>
|
||||
@@ -756,7 +808,7 @@ function DataSourcesSection() {
|
||||
href={step.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#60a5fa', fontSize: 11, textDecoration: 'underline' }}
|
||||
style={{ color: 'var(--color-accent)', fontSize: 11, textDecoration: 'underline' }}
|
||||
>
|
||||
{step.urlLabel || 'Open'} →
|
||||
</a>
|
||||
@@ -789,11 +841,11 @@ function DataSourcesSection() {
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontSize: 12, color: '#f59e0b',
|
||||
fontSize: 12, color: 'var(--color-warning)',
|
||||
}}>
|
||||
<div className="animate-spin" style={{
|
||||
width: 12, height: 12, borderRadius: '50%',
|
||||
border: '2px solid #f59e0b',
|
||||
border: '2px solid var(--color-warning)',
|
||||
borderTopColor: 'transparent',
|
||||
}} />
|
||||
{connectStage}
|
||||
@@ -804,7 +856,7 @@ function DataSourcesSection() {
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 2, background: '#f59e0b',
|
||||
height: '100%', borderRadius: 2, background: 'var(--color-warning)',
|
||||
width: connectStage.includes('Sync') ? '75%' : connectStage.includes('Connected') ? '50%' : '25%',
|
||||
transition: 'width 0.5s ease',
|
||||
}} />
|
||||
@@ -813,7 +865,7 @@ function DataSourcesSection() {
|
||||
)}
|
||||
{/* Connection error */}
|
||||
{connectError && connectingId === null && expandedId === c.connector_id && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 6 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 6 }}>
|
||||
{connectError}
|
||||
</div>
|
||||
)}
|
||||
@@ -822,7 +874,8 @@ function DataSourcesSection() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -921,7 +974,7 @@ function SendBlueSection({
|
||||
return (
|
||||
<div style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: '1px solid #2a5a3a',
|
||||
border: '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)',
|
||||
borderRadius: 8, marginBottom: 10,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
@@ -929,7 +982,7 @@ function SendBlueSection({
|
||||
<span style={{ fontSize: 18, marginRight: 10 }}>{'\uD83D\uDCF1'}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>iMessage + SMS</div>
|
||||
<div style={{ fontSize: 11, color: '#4ade80' }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-success)' }}>
|
||||
Active — text {(cfg.phone_number as string) || 'your number'} to chat
|
||||
</div>
|
||||
</div>
|
||||
@@ -979,7 +1032,7 @@ function SendBlueSection({
|
||||
href="https://sendblue.co"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#60a5fa', fontSize: 12, textDecoration: 'underline' }}
|
||||
style={{ color: 'var(--color-accent)', fontSize: 12, textDecoration: 'underline' }}
|
||||
>
|
||||
1. Sign up at sendblue.co →
|
||||
</a>
|
||||
@@ -989,7 +1042,7 @@ function SendBlueSection({
|
||||
href="https://dashboard.sendblue.co/api-credentials"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#60a5fa', fontSize: 12, textDecoration: 'underline' }}
|
||||
style={{ color: 'var(--color-accent)', fontSize: 12, textDecoration: 'underline' }}
|
||||
>
|
||||
2. Go to your API Credentials page →
|
||||
</a>
|
||||
@@ -1031,7 +1084,7 @@ function SendBlueSection({
|
||||
padding: '8px 10px', marginBottom: 10,
|
||||
background: 'var(--color-bg-secondary)',
|
||||
borderRadius: 6,
|
||||
borderLeft: '3px solid var(--color-accent, #7c3aed)',
|
||||
borderLeft: '3px solid var(--color-accent, var(--color-accent-purple))',
|
||||
}}>
|
||||
<div><strong>1.</strong> Open a terminal and run: <code style={{ color: 'var(--color-accent)', background: 'var(--color-bg)', padding: '1px 4px', borderRadius: 3 }}>ngrok http 8000</code></div>
|
||||
<div style={{ marginTop: 4 }}><strong>2.</strong> Copy the <code style={{ color: 'var(--color-accent)', background: 'var(--color-bg)', padding: '1px 4px', borderRadius: 3 }}>https://</code> forwarding URL (e.g. https://abc123.ngrok.io)</div>
|
||||
@@ -1049,8 +1102,8 @@ function SendBlueSection({
|
||||
disabled={!webhookUrl.trim() || webhookStatus === 'registering'}
|
||||
style={{
|
||||
fontSize: 11, padding: '6px 12px', whiteSpace: 'nowrap',
|
||||
background: webhookStatus === 'done' ? '#22c55e' : '#7c3aed',
|
||||
color: 'white', border: 'none', borderRadius: 4,
|
||||
background: webhookStatus === 'done' ? 'var(--color-success)' : 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none', borderRadius: 4,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: !webhookUrl.trim() || webhookStatus === 'registering' ? 0.5 : 1,
|
||||
}}
|
||||
@@ -1062,17 +1115,17 @@ function SendBlueSection({
|
||||
</button>
|
||||
</div>
|
||||
{webhookStatus === 'done' && (
|
||||
<div style={{ fontSize: 11, color: '#22c55e', marginTop: 6 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-success)', marginTop: 6 }}>
|
||||
Webhook registered! Incoming texts will be forwarded to your agent.
|
||||
</div>
|
||||
)}
|
||||
{webhookStatus === 'error' && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 6 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 6 }}>
|
||||
Failed to register webhook. Check your ngrok URL and SendBlue credentials.
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 10, color: 'var(--color-text-tertiary)', marginTop: 8 }}>
|
||||
Don't have ngrok? <a href="https://ngrok.com/download" target="_blank" rel="noopener noreferrer" style={{ color: '#60a5fa', textDecoration: 'underline' }}>Download it free</a>. You can also skip this step and register the webhook later.
|
||||
Don't have ngrok? <a href="https://ngrok.com/download" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--color-accent)', textDecoration: 'underline' }}>Download it free</a>. You can also skip this step and register the webhook later.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@@ -1132,7 +1185,7 @@ function SendBlueSection({
|
||||
Let people text your agent from any phone
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ color: '#7c3aed', fontSize: 11, fontWeight: 500 }}>
|
||||
<span style={{ color: 'var(--color-accent-purple)', fontSize: 11, fontWeight: 500 }}>
|
||||
{step >= 0 ? 'Set Up' : '+ Add'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1146,7 +1199,7 @@ function SendBlueSection({
|
||||
key={i}
|
||||
style={{
|
||||
flex: 1, height: 3, borderRadius: 2,
|
||||
background: i <= step ? '#7c3aed' : 'var(--color-border)',
|
||||
background: i <= step ? 'var(--color-accent-purple)' : 'var(--color-border)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -1158,7 +1211,7 @@ function SendBlueSection({
|
||||
{steps[step]?.content}
|
||||
|
||||
{error && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 6 }}>{error}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-error)', marginTop: 6 }}>{error}</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
@@ -1180,7 +1233,7 @@ function SendBlueSection({
|
||||
disabled={!steps[step]?.canAdvance}
|
||||
style={{
|
||||
fontSize: 12, padding: '6px 16px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: steps[step]?.canAdvance ? 1 : 0.5,
|
||||
@@ -1192,7 +1245,7 @@ function SendBlueSection({
|
||||
disabled={loading || !steps[step]?.canAdvance}
|
||||
style={{
|
||||
fontSize: 12, padding: '6px 16px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: loading || !steps[step]?.canAdvance ? 0.5 : 1,
|
||||
@@ -1275,7 +1328,7 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
key={ch.type}
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: binding ? '1px solid #2a5a3a' : '1px dashed var(--color-border)',
|
||||
border: binding ? '1px solid color-mix(in srgb, var(--color-success) 22%, transparent)' : '1px dashed var(--color-border)',
|
||||
borderRadius: 8, marginBottom: 10, overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
@@ -1285,7 +1338,7 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{ch.name}</div>
|
||||
<div style={{
|
||||
fontSize: 11,
|
||||
color: binding ? '#4ade80' : 'var(--color-text-secondary)',
|
||||
color: binding ? 'var(--color-success)' : 'var(--color-text-secondary)',
|
||||
}}>
|
||||
{binding ? ch.activeLabel(cfg) : ch.description}
|
||||
</div>
|
||||
@@ -1293,7 +1346,7 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
{binding ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{
|
||||
background: '#2a5a3a', color: '#4ade80',
|
||||
background: 'color-mix(in srgb, var(--color-success) 22%, transparent)', color: 'var(--color-success)',
|
||||
padding: '2px 8px', borderRadius: 10,
|
||||
fontSize: 10, fontWeight: 600,
|
||||
}}>Active</span>
|
||||
@@ -1311,8 +1364,8 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
<button
|
||||
onClick={() => { setSetupType(isSetup ? null : ch.type); setFormValues({}); }}
|
||||
style={{
|
||||
fontSize: 10, padding: '3px 12px', background: '#7c3aed',
|
||||
color: 'white', border: 'none', borderRadius: 5,
|
||||
fontSize: 10, padding: '3px 12px', background: 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
}}
|
||||
>{isSetup ? 'Cancel' : 'Set Up'}</button>
|
||||
@@ -1342,7 +1395,7 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
marginBottom: 12, padding: '8px 10px',
|
||||
background: 'var(--color-bg-secondary)',
|
||||
borderRadius: 6,
|
||||
borderLeft: '3px solid var(--color-accent, #7c3aed)',
|
||||
borderLeft: '3px solid var(--color-accent, var(--color-accent-purple))',
|
||||
}}>
|
||||
{ch.setupSteps.map((s, i) => {
|
||||
if (s.startsWith('COPYABLE:')) {
|
||||
@@ -1364,7 +1417,7 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
style={{
|
||||
position: 'sticky', float: 'right', top: 0,
|
||||
fontSize: 10, padding: '2px 8px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
background: 'var(--color-accent-purple)', color: 'var(--color-on-accent)',
|
||||
border: 'none', borderRadius: 3,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
}}
|
||||
@@ -1400,8 +1453,8 @@ function MessagingSection({ agentId }: { agentId: string }) {
|
||||
onClick={() => handleSetup(ch)}
|
||||
disabled={loading || !canConnect}
|
||||
style={{
|
||||
fontSize: 12, padding: '7px 20px', background: '#7c3aed',
|
||||
color: 'white', border: 'none', borderRadius: 5,
|
||||
fontSize: 12, padding: '7px 20px', background: 'var(--color-accent-purple)',
|
||||
color: 'var(--color-on-accent)', border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: loading || !canConnect ? 0.5 : 1, marginTop: 4,
|
||||
}}
|
||||
@@ -1609,7 +1662,7 @@ function MemorySection() {
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all cursor-pointer whitespace-nowrap"
|
||||
style={{
|
||||
background: searching || !searchQuery.trim() ? 'var(--color-bg-tertiary)' : 'var(--color-accent-purple)',
|
||||
color: searching || !searchQuery.trim() ? 'var(--color-text-tertiary)' : '#fff',
|
||||
color: searching || !searchQuery.trim() ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
|
||||
opacity: searching || !searchQuery.trim() ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
@@ -1713,7 +1766,7 @@ function MemorySection() {
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-lg text-sm font-medium cursor-pointer transition-all"
|
||||
style={{
|
||||
background: indexing || !indexPath.trim() ? 'var(--color-bg-tertiary)' : 'var(--color-accent-purple)',
|
||||
color: indexing || !indexPath.trim() ? 'var(--color-text-tertiary)' : '#fff',
|
||||
color: indexing || !indexPath.trim() ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
|
||||
opacity: indexing || !indexPath.trim() ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
@@ -1761,7 +1814,7 @@ function MemorySection() {
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-lg text-sm font-medium cursor-pointer transition-all"
|
||||
style={{
|
||||
background: storing || !storeContent.trim() ? 'var(--color-bg-tertiary)' : 'var(--color-accent-purple)',
|
||||
color: storing || !storeContent.trim() ? 'var(--color-text-tertiary)' : '#fff',
|
||||
color: storing || !storeContent.trim() ? 'var(--color-text-tertiary)' : 'var(--color-on-accent)',
|
||||
opacity: storing || !storeContent.trim() ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
@@ -1830,39 +1883,48 @@ export function DataSourcesPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="shrink-0 px-6 pt-6 pb-4">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-10">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<header className="mb-6">
|
||||
<h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Data Sources, Channels & Memory
|
||||
</h1>
|
||||
<p className="text-sm mt-1" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Connect your personal data so your AI can search across everything, and set up messaging channels to chat from your phone.
|
||||
<p className="text-sm mt-2 max-w-2xl" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Connect personal data so the assistant can search across everything, and set up messaging channels to chat from your phone.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="flex gap-1 mb-6"
|
||||
style={{ borderBottom: '1px solid var(--color-border)' }}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className="relative px-4 py-2.5 text-sm transition-colors cursor-pointer"
|
||||
style={{
|
||||
color: isActive ? 'var(--color-text)' : 'var(--color-text-secondary)',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
{isActive && (
|
||||
<motion.span
|
||||
layoutId="data-sources-tab-indicator"
|
||||
className="absolute left-0 right-0 -bottom-px h-[2px]"
|
||||
style={{ background: 'var(--color-text)' }}
|
||||
transition={{ type: 'spring', stiffness: 380, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="shrink-0 px-6 flex gap-1 mb-4">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm transition-colors cursor-pointer"
|
||||
style={{
|
||||
background: activeTab === tab.id ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
color: activeTab === tab.id ? 'var(--color-text)' : 'var(--color-text-secondary)',
|
||||
fontWeight: activeTab === tab.id ? 600 : 400,
|
||||
border: activeTab === tab.id ? '1px solid var(--color-border)' : '1px solid transparent',
|
||||
}}
|
||||
>
|
||||
<tab.icon size={14} />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||
<div>
|
||||
{activeTab === 'sources' && <DataSourcesSection />}
|
||||
{activeTab === 'messaging' && (
|
||||
firstAgent ? (
|
||||
@@ -1876,6 +1938,7 @@ export function DataSourcesPage() {
|
||||
)}
|
||||
{activeTab === 'memory' && <MemorySection />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function HostedView() {
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="inline-flex items-center gap-2.5 px-6 py-3 rounded-xl text-sm font-medium transition-opacity cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: 'white' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.9')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
>
|
||||
@@ -232,7 +232,7 @@ function HostedView() {
|
||||
{healthy === false && (
|
||||
<div
|
||||
className="mt-4 inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm"
|
||||
style={{ background: 'rgba(239, 68, 68, 0.1)', color: '#ef4444' }}
|
||||
style={{ background: 'color-mix(in srgb, var(--color-error) 10%, transparent)', color: 'var(--color-error)' }}
|
||||
>
|
||||
Server is not responding. The backend may be starting up.
|
||||
</div>
|
||||
@@ -294,7 +294,7 @@ function DesktopView() {
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="inline-flex items-center gap-2.5 px-6 py-3 rounded-xl text-sm font-medium transition-opacity cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: 'white' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.9')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
>
|
||||
@@ -372,7 +372,7 @@ function SelfHostedView() {
|
||||
<a
|
||||
href={`${GITHUB_BASE}/${primary.file}`}
|
||||
className="inline-flex items-center gap-2.5 px-6 py-3 rounded-xl text-sm font-medium transition-opacity cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: 'white' }}
|
||||
style={{ background: 'var(--color-accent)', color: 'var(--color-on-accent)' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.9')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { ScrollText, Copy, Trash2 } from 'lucide-react';
|
||||
import { Copy, Trash2 } from 'lucide-react';
|
||||
import { useAppStore } from '../lib/store';
|
||||
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
@@ -30,32 +30,37 @@ export function LogsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden p-6">
|
||||
<div className="flex-1 flex flex-col overflow-hidden px-6 py-10">
|
||||
<div className="max-w-4xl mx-auto w-full flex flex-col flex-1 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-4 shrink-0">
|
||||
<ScrollText size={24} style={{ color: 'var(--color-accent)' }} />
|
||||
<h1 className="text-xl font-semibold flex-1" style={{ color: 'var(--color-text)' }}>
|
||||
Logs
|
||||
</h1>
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{logEntries.length} entries
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer"
|
||||
style={{ background: 'var(--color-bg-secondary)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<Copy size={12} /> Copy All
|
||||
</button>
|
||||
<button
|
||||
onClick={clearLogs}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer"
|
||||
style={{ background: 'var(--color-bg-secondary)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<Trash2 size={12} /> Clear
|
||||
</button>
|
||||
</div>
|
||||
<header className="mb-6 shrink-0">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Logs
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
{logEntries.length} entries
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer"
|
||||
style={{ background: 'var(--color-bg-secondary)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<Copy size={12} /> Copy All
|
||||
</button>
|
||||
<button
|
||||
onClick={clearLogs}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer"
|
||||
style={{ background: 'var(--color-bg-secondary)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<Trash2 size={12} /> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm mt-2 max-w-2xl" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Recent activity — chat events, model switches, tool calls, and system messages from this session.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Log entries */}
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Settings,
|
||||
Palette,
|
||||
Globe,
|
||||
Cpu,
|
||||
@@ -35,7 +34,7 @@ function OllamaModelList() {
|
||||
{models.map(m => (
|
||||
<span key={m.name} className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px]"
|
||||
style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text)' }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#22c55e', display: 'inline-block' }} />
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--color-success)', display: 'inline-block' }} />
|
||||
{m.name} ({(m.size / 1e9).toFixed(1)} GB)
|
||||
</span>
|
||||
))}
|
||||
@@ -59,7 +58,7 @@ function ApiKeyInput({ storageKey, placeholder }: { storageKey: string; placehol
|
||||
<input type="password" value={value} onChange={e => save(e.target.value)} placeholder={placeholder}
|
||||
className="w-48 px-2 py-1 rounded text-xs"
|
||||
style={{ background: 'var(--color-bg)', border: '1px solid var(--color-border)', color: 'var(--color-text)' }} />
|
||||
{saved && <span className="text-[10px]" style={{ color: '#22c55e' }}>Saved</span>}
|
||||
{saved && <span className="text-[10px]" style={{ color: 'var(--color-success)' }}>Saved</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -73,7 +72,7 @@ function CloudProviderStatus({ label, storageKey }: { label: string; storageKey:
|
||||
<span className="flex items-center gap-1 text-xs" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
<span style={{
|
||||
width: 6, height: 6, borderRadius: '50%', display: 'inline-block',
|
||||
background: hasKey ? '#22c55e' : 'var(--color-text-tertiary)',
|
||||
background: hasKey ? 'var(--color-success)' : 'var(--color-text-tertiary)',
|
||||
}} />
|
||||
{label}
|
||||
</span>
|
||||
@@ -203,22 +202,26 @@ export function SettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-10">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Settings size={24} style={{ color: 'var(--color-accent)' }} />
|
||||
<h1 className="text-xl font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Settings
|
||||
</h1>
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1 text-xs px-2 py-1 rounded-full" style={{
|
||||
background: 'var(--color-accent-subtle)',
|
||||
color: 'var(--color-success)',
|
||||
}}>
|
||||
<Check size={12} /> Saved
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<header className="mb-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||
Settings
|
||||
</h1>
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1 text-xs px-2 py-1 rounded-full" style={{
|
||||
background: 'var(--color-accent-subtle)',
|
||||
color: 'var(--color-success)',
|
||||
}}>
|
||||
<Check size={12} /> Saved
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm mt-2 max-w-2xl" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
App preferences — appearance, model defaults, keyboard shortcuts, and data management.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Appearance */}
|
||||
|
||||
Reference in New Issue
Block a user