forked from sagnik/Project_Velocity
feat: Added frontend for The Catalyst tab (#10)
Added frontend for "The Catalyst" tab. Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#10
This commit is contained in:
873
app/src/components/modules/Catalyst.tsx
Normal file
873
app/src/components/modules/Catalyst.tsx
Normal file
@@ -0,0 +1,873 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Megaphone, Clapperboard, BarChart3, Globe, Settings2,
|
||||
Zap, TrendingUp, Eye, MousePointerClick, DollarSign,
|
||||
Upload, Play, Image, Film, RefreshCw, ArrowRight, Plus,
|
||||
AlertTriangle, ArrowRightLeft, PlusCircle, SlidersHorizontal,
|
||||
Activity, Check, Link2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
AreaChart, Area, BarChart, Bar,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { useMarketingStore } from '@/store/useMarketingStore';
|
||||
import type { Campaign, MarketingAsset, LiveOptimizationEvent, LiveEventType } from '@/types';
|
||||
|
||||
// ── Design tokens ─────────────────────────────────────────────────────────────
|
||||
const GLASS = {
|
||||
background: 'rgba(8, 10, 18, 0.82)',
|
||||
border: '1px solid rgba(59,130,246,0.14)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
WebkitBackdropFilter: 'blur(24px)',
|
||||
boxShadow: '0 0 0 1px rgba(255,255,255,0.04), 0 4px 32px rgba(0,0,0,0.55)',
|
||||
} as const;
|
||||
|
||||
const INNER = {
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
} as const;
|
||||
|
||||
// ── Shared primitives ─────────────────────────────────────────────────────────
|
||||
|
||||
function Widget({ children, className = '', delay = 0, colSpan = 1, rowSpan = 1, style }: {
|
||||
children: React.ReactNode; className?: string; delay?: number; colSpan?: number; rowSpan?: number; style?: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative rounded-2xl p-5 overflow-hidden ${className}`}
|
||||
style={{ gridColumn: `span ${colSpan}`, gridRow: `span ${rowSpan}`, ...GLASS, ...style }}
|
||||
initial={{ opacity: 0, y: 16, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.35, delay, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 h-px pointer-events-none"
|
||||
style={{ background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.10), transparent)' }} />
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function GhostButton({ children, onClick, danger = false, className = '' }: {
|
||||
children: React.ReactNode; onClick?: () => void; danger?: boolean; className?: string;
|
||||
}) {
|
||||
return (
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors ${className}`}
|
||||
style={danger
|
||||
? { background: 'rgba(239,68,68,0.12)', color: '#f87171', border: '1px solid rgba(239,68,68,0.2)' }
|
||||
: { ...INNER, color: 'rgba(255,255,255,0.8)' }
|
||||
}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
function DarkInput({ value, onChange, placeholder, type = 'text', readOnly = false }: {
|
||||
value: string; onChange?: (v: string) => void; placeholder?: string; type?: string; readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
readOnly={readOnly}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="rounded-xl px-3 py-2 text-sm text-white w-full focus:outline-none transition-all"
|
||||
style={{ ...INNER, caretColor: 'hsl(var(--accent))' }}
|
||||
onFocus={(e) => { if (!readOnly) e.currentTarget.style.border = '1px solid rgba(59,130,246,0.4)'; }}
|
||||
onBlur={(e) => { e.currentTarget.style.border = '1px solid rgba(255,255,255,0.07)'; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LiveBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium" style={{ color: '#4ade80' }}>
|
||||
<motion.div className="w-1.5 h-1.5 rounded-full bg-green-400"
|
||||
animate={{ opacity: [1, 0.3, 1] }} transition={{ repeat: Infinity, duration: 1.6 }} />
|
||||
Live
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── KPI Stat card ─────────────────────────────────────────────────────────────
|
||||
function StatCard({ icon: Icon, label, value, sub, glowColor = 'rgba(59,130,246,0.2)', delay = 0 }: {
|
||||
icon: LucideIcon; label: string; value: string; sub: string; glowColor?: string; delay?: number;
|
||||
}) {
|
||||
return (
|
||||
<Widget delay={delay} className="flex flex-col justify-between min-h-[130px]">
|
||||
<div className="absolute -bottom-6 -right-6 w-44 h-44 pointer-events-none"
|
||||
style={{ background: `radial-gradient(ellipse at 70% 80%, ${glowColor} 0%, transparent 65%)`, filter: 'blur(18px)' }} />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-8 h-8 rounded-xl flex items-center justify-center"
|
||||
style={{ background: `${glowColor.replace('0.2', '0.12')}`, border: `1px solid ${glowColor.replace('0.2', '0.25')}` }}>
|
||||
<Icon className="w-4 h-4" style={{ color: glowColor.includes('59,130,246') ? '#60a5fa' : glowColor.includes('34,211,238') ? '#22d3ee' : glowColor.includes('251,191,36') ? '#fbbf24' : '#a78bfa' }} />
|
||||
</div>
|
||||
<LiveBadge />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-widest mb-1" style={{ color: 'rgba(148,163,184,0.65)' }}>{label}</p>
|
||||
<motion.p className="text-3xl font-semibold text-white leading-none"
|
||||
initial={{ opacity: 0, scale: 0.7 }} animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.4, delay: delay + 0.12, type: 'spring', stiffness: 200 }}>
|
||||
{value}
|
||||
</motion.p>
|
||||
<p className="text-xs mt-1" style={{ color: 'rgba(148,163,184,0.5)' }}>{sub}</p>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Campaign status badge ─────────────────────────────────────────────────────
|
||||
function CampaignStatusBadge({ status }: { status: Campaign['status'] }) {
|
||||
const map: Record<Campaign['status'], { label: string; color: string; bg: string }> = {
|
||||
ACTIVE: { label: 'Active', color: '#4ade80', bg: 'rgba(74,222,128,0.12)' },
|
||||
PAUSED: { label: 'Paused', color: '#fbbf24', bg: 'rgba(251,191,36,0.12)' },
|
||||
DELETED: { label: 'Deleted', color: '#f87171', bg: 'rgba(248,113,113,0.12)' },
|
||||
ARCHIVED: { label: 'Archived', color: '#94a3b8', bg: 'rgba(148,163,184,0.12)' },
|
||||
};
|
||||
const { label, color, bg } = map[status];
|
||||
return (
|
||||
<span className="px-2 py-0.5 rounded-full text-[11px] font-medium" style={{ color, background: bg }}>{label}</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 1 — The Studio
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const RENDER_MESSAGES: Record<string, string[]> = {
|
||||
rendering: [
|
||||
'Wan 2.2 is rendering cinematic lighting...',
|
||||
'Wan 2.2 is compositing the infinity pool reflection...',
|
||||
'Qwen-Image 2512 is synthesizing Arabic typography...',
|
||||
'Flux-Schnell is upscaling to 4K resolution...',
|
||||
],
|
||||
queued: [
|
||||
'Qwen-Image 2512 queued for cinematic poster render...',
|
||||
'Wan 2.2 14B queued — GPU resources freeing up...',
|
||||
],
|
||||
};
|
||||
|
||||
function AssetCard({ asset }: { asset: MarketingAsset }) {
|
||||
const isProcessing = asset.status === 'rendering' || asset.status === 'queued';
|
||||
const [msgIdx, setMsgIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProcessing) return;
|
||||
const msgs = RENDER_MESSAGES[asset.status] ?? [];
|
||||
if (msgs.length === 0) return;
|
||||
const t = setInterval(() => setMsgIdx((i) => (i + 1) % msgs.length), 2800);
|
||||
return () => clearInterval(t);
|
||||
}, [isProcessing, asset.status]);
|
||||
|
||||
const statusColor: Record<typeof asset.status, string> = {
|
||||
queued: 'rgba(148,163,184,0.8)',
|
||||
rendering: '#fbbf24',
|
||||
ready: '#4ade80',
|
||||
uploaded: '#60a5fa',
|
||||
failed: '#f87171',
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="relative rounded-2xl overflow-hidden flex flex-col"
|
||||
style={{ ...INNER, minHeight: 200, border: `1px solid ${statusColor[asset.status]}22` }}
|
||||
whileHover={{ scale: 1.015 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Thumbnail / shimmer area */}
|
||||
<div className="relative flex-1 flex items-center justify-center min-h-[120px]"
|
||||
style={{ background: 'rgba(255,255,255,0.02)' }}>
|
||||
{isProcessing ? (
|
||||
<div className="w-full h-full absolute inset-0">
|
||||
<motion.div className="absolute inset-0"
|
||||
style={{ background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%)' }}
|
||||
animate={{ x: ['-100%', '200%'] }}
|
||||
transition={{ repeat: Infinity, duration: 1.6, ease: 'linear' }} />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 p-3 text-center">
|
||||
{asset.type === 'video'
|
||||
? <Film className="w-8 h-8 text-amber-400 opacity-60" />
|
||||
: <Image className="w-8 h-8 text-blue-400 opacity-60" />}
|
||||
<motion.p className="text-xs font-medium leading-relaxed"
|
||||
style={{ color: statusColor[asset.status] }}
|
||||
key={msgIdx}
|
||||
initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}>
|
||||
{(RENDER_MESSAGES[asset.status] ?? [])[msgIdx] ?? asset.renderMessage}
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||
{asset.type === 'video'
|
||||
? <Film className="w-10 h-10 text-blue-400" />
|
||||
: <Image className="w-10 h-10 text-blue-400" />}
|
||||
<span className="text-xs" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
{asset.metaAssetId ? `Meta ID: ${asset.metaAssetId}` : 'Ready for upload'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info footer */}
|
||||
<div className="px-3 py-2.5" style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div className="flex items-start justify-between gap-2 mb-1.5">
|
||||
<p className="text-xs font-medium text-white leading-snug line-clamp-2">{asset.name}</p>
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full flex-shrink-0"
|
||||
style={{ color: statusColor[asset.status], background: `${statusColor[asset.status]}18` }}>
|
||||
{asset.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded"
|
||||
style={{ background: 'rgba(255,255,255,0.06)', color: 'rgba(148,163,184,0.6)' }}>
|
||||
{asset.type === 'video' ? 'Wan 2.2' : 'Qwen-2512'}
|
||||
</span>
|
||||
{asset.language && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded uppercase"
|
||||
style={{ background: 'rgba(255,255,255,0.06)', color: 'rgba(148,163,184,0.6)' }}>
|
||||
{asset.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowInput() {
|
||||
const [mode, setMode] = useState<'image' | 'video'>('image');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [keywords, setKeywords] = useState('');
|
||||
const [textCopy, setTextCopy] = useState('');
|
||||
|
||||
return (
|
||||
<Widget delay={0.04} colSpan={1} className="!p-6" style={{ background: '#111216', borderRadius: '28px' }}>
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Top Section: Ground Truth & References */}
|
||||
<div className="flex items-end gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button title="Select Image from Gallery or Dream Weaver / Click Image from iPad" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<span className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.2)' }}>Start</span>
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-white tracking-wide">Ground Truth</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-[60px] mb-[26px]">
|
||||
<Plus className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button title="Add reference image" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<button title="Add reference image" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<button title="Add more references" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<Plus className="w-5 h-5 text-white/40" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-white tracking-wide">References</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Input & Controls */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex-1 flex flex-col gap-3">
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="What do you want to create?"
|
||||
className="w-full bg-transparent text-xl font-medium text-white placeholder-white/20 resize-none outline-none leading-relaxed"
|
||||
rows={1}
|
||||
/>
|
||||
{/* Extra inputs for keywords and text copy as requested */}
|
||||
<div className="flex items-center gap-3 pt-2" style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={keywords}
|
||||
onChange={(e) => setKeywords(e.target.value)}
|
||||
placeholder="Keywords (comma separated)"
|
||||
className="bg-transparent text-sm text-white/80 placeholder-white/20 outline-none flex-1 min-w-0"
|
||||
/>
|
||||
<span className="text-white/10">|</span>
|
||||
<input
|
||||
type="text"
|
||||
value={textCopy}
|
||||
onChange={(e) => setTextCopy(e.target.value)}
|
||||
placeholder='Text copy inside ""'
|
||||
className="bg-transparent text-sm text-white/80 placeholder-white/20 outline-none flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 ml-6 pl-4">
|
||||
{/* Toggle */}
|
||||
<div className="flex items-center rounded-full p-1" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<button
|
||||
onClick={() => setMode('image')}
|
||||
className={`px-5 py-2.5 rounded-full text-sm font-semibold transition-all ${mode === 'image' ? 'text-black shadow-lg' : 'text-white/60'}`}
|
||||
style={{ background: mode === 'image' ? '#22c55e' : 'transparent' }}
|
||||
>
|
||||
Image
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('video')}
|
||||
className={`px-5 py-2.5 rounded-full text-sm font-semibold transition-all ${mode === 'video' ? 'text-black shadow-lg' : 'text-white/60'}`}
|
||||
style={{ background: mode === 'video' ? '#22c55e' : 'transparent' }}
|
||||
>
|
||||
Video
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Send Button */}
|
||||
<button className="w-[44px] h-[44px] rounded-full bg-zinc-200 flex items-center justify-center hover:bg-white transition-colors flex-shrink-0">
|
||||
<ArrowRight className="w-5 h-5 text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
function TheStudio() {
|
||||
const { activeAssets } = useMarketingStore();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header row */}
|
||||
<Widget delay={0} colSpan={1}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white mb-0.5 flex items-center gap-2">
|
||||
<Clapperboard className="w-4 h-4 text-blue-400" /> Visual AI Engine
|
||||
</h3>
|
||||
<p className="text-xs" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
ComfyUI workflows (Wan 2.2 & Qwen-Image 2512) feed directly into the Meta Asset Library
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<GhostButton>
|
||||
<span className="flex items-center gap-1.5"><Upload className="w-3.5 h-3.5" /> Sync to Meta</span>
|
||||
</GhostButton>
|
||||
<GhostButton>
|
||||
<span className="flex items-center gap-1.5"><Play className="w-3.5 h-3.5 text-blue-400" /> Trigger Render</span>
|
||||
</GhostButton>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* ComfyUI Workflow Input Panel */}
|
||||
<WorkflowInput />
|
||||
|
||||
{/* Asset grid */}
|
||||
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{activeAssets.map((asset, i) => (
|
||||
<motion.div key={asset.id}
|
||||
initial={{ opacity: 0, y: 14 }} animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.07 }}>
|
||||
<AssetCard asset={asset} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 2 — Campaign Command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function CampaignCommand() {
|
||||
const { campaigns, adInsights } = useMarketingStore();
|
||||
|
||||
const totalSpend = campaigns.reduce((s, c) => s + c.lifetimeSpend / 100, 0);
|
||||
const totalImpr = campaigns.reduce((s, c) => s + c.impressions, 0);
|
||||
const avgCtr = campaigns.reduce((s, c) => s + c.ctr, 0) / campaigns.length;
|
||||
const avgCpa = campaigns.reduce((s, c) => s + c.cpa, 0) / campaigns.length;
|
||||
|
||||
// Build spend-over-time from insights (last 14 days)
|
||||
const spendData = adInsights.slice(0, 14).map((d) => ({
|
||||
date: d.date.slice(5), // MM-DD
|
||||
spend: d.spend,
|
||||
impressions: Math.round(d.impressions / 1000),
|
||||
})).reverse();
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* KPI row */}
|
||||
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<StatCard icon={Megaphone} label="Active Campaigns" value={String(campaigns.filter(c => c.status === 'ACTIVE').length)} sub={`${campaigns.length} total campaigns`} glowColor="rgba(59,130,246,0.2)" delay={0} />
|
||||
<StatCard icon={DollarSign} label="Total Spend" value={`AED ${(totalSpend / 1000).toFixed(1)}K`} sub="Lifetime across all campaigns" glowColor="rgba(34,211,238,0.2)" delay={0.06} />
|
||||
<StatCard icon={Eye} label="Impressions" value={`${(totalImpr / 1000).toFixed(0)}K`} sub="Total ad impressions" glowColor="rgba(251,191,36,0.2)" delay={0.12} />
|
||||
<StatCard icon={MousePointerClick} label="Avg CTR" value={`${avgCtr.toFixed(2)}%`} sub={`Avg CPA: AED ${avgCpa.toFixed(0)}`} glowColor="rgba(167,139,250,0.2)" delay={0.18} />
|
||||
</div>
|
||||
|
||||
{/* Spend chart + campaign list */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* Spend over time chart */}
|
||||
<Widget delay={0.24} colSpan={1}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-blue-400" /> Spend vs Impressions
|
||||
</h3>
|
||||
<LiveBadge />
|
||||
</div>
|
||||
<div className="h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={spendData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gSpend" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="gImpr" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22d3ee" stopOpacity={0.28} />
|
||||
<stop offset="95%" stopColor="#22d3ee" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis dataKey="date" stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<Tooltip contentStyle={{ backgroundColor: 'rgba(8,10,18,0.92)', border: '1px solid rgba(59,130,246,0.2)', borderRadius: 10 }} labelStyle={{ color: 'rgba(148,163,184,0.8)', fontSize: 11 }} itemStyle={{ color: '#93c5fd', fontSize: 12 }} />
|
||||
<Area type="monotone" dataKey="spend" stroke="#3b82f6" strokeWidth={2} fillOpacity={1} fill="url(#gSpend)" />
|
||||
<Area type="monotone" dataKey="impressions" stroke="#22d3ee" strokeWidth={2} fillOpacity={1} fill="url(#gImpr)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* Campaign list */}
|
||||
<Widget delay={0.28} colSpan={1}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Campaigns
|
||||
</h3>
|
||||
<GhostButton>
|
||||
<span className="flex items-center gap-1.5 text-xs"><PlusCircle className="w-3.5 h-3.5" /> New</span>
|
||||
</GhostButton>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{campaigns.map((c) => (
|
||||
<motion.div key={c.id} className="flex items-center justify-between p-3 rounded-xl" style={INNER}
|
||||
whileHover={{ scale: 1.01 }} transition={{ duration: 0.15 }}>
|
||||
<div className="flex-1 min-w-0 mr-3">
|
||||
<p className="text-sm font-medium text-white truncate">{c.name}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
AED {(c.lifetimeSpend / 100).toLocaleString()} · {c.impressions.toLocaleString()} impr.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className="text-xs font-medium" style={{ color: c.roi > 20 ? '#4ade80' : '#fbbf24' }}>
|
||||
{c.roi.toFixed(1)}% ROI
|
||||
</span>
|
||||
<CampaignStatusBadge status={c.status} />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 3 — Intelligence & ROI
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function IntelligenceROI() {
|
||||
const { campaigns, adInsights } = useMarketingStore();
|
||||
|
||||
const cpaData = adInsights.slice(0, 7).map((d) => ({
|
||||
date: d.date.slice(5),
|
||||
cpa: d.cpa,
|
||||
roi: d.roi,
|
||||
})).reverse();
|
||||
|
||||
const adSetPerf = [
|
||||
{ name: '3BHK Dubai', ctr: 2.1, cpa: 210, spend: 3400 },
|
||||
{ name: 'PH Retarget', ctr: 3.2, cpa: 2100, spend: 5800 },
|
||||
{ name: '1BHK Lookalike', ctr: 1.8, cpa: 270, spend: 980 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* CPA / ROI KPIs */}
|
||||
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<StatCard icon={DollarSign} label="Avg CPA" value={`AED ${(campaigns.reduce((s,c)=>s+c.cpa,0)/campaigns.length).toFixed(0)}`} sub="Cost per acquisition" glowColor="rgba(34,211,238,0.2)" delay={0} />
|
||||
<StatCard icon={TrendingUp} label="Portfolio ROI" value={`${(campaigns.reduce((s,c)=>s+c.roi,0)/campaigns.length).toFixed(1)}%`} sub="Blended across all ad sets" glowColor="rgba(74,222,128,0.2)" delay={0.06} />
|
||||
<StatCard icon={Activity} label="Avg CTR" value={`${(campaigns.reduce((s,c)=>s+c.ctr,0)/campaigns.length).toFixed(2)}%`} sub="Click-through rate" glowColor="rgba(167,139,250,0.2)" delay={0.12} />
|
||||
<StatCard icon={Zap} label="Optimization Runs" value="47" sub="Agent actions today" glowColor="rgba(251,191,36,0.2)" delay={0.18} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* CPA trend */}
|
||||
<Widget delay={0.24}>
|
||||
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-cyan-400" /> CPA Trend (7 Days)
|
||||
</h3>
|
||||
<div className="h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={cpaData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gCpa" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22d3ee" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#22d3ee" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis dataKey="date" stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<Tooltip contentStyle={{ backgroundColor: 'rgba(8,10,18,0.92)', border: '1px solid rgba(34,211,238,0.2)', borderRadius: 10 }} labelStyle={{ color: 'rgba(148,163,184,0.8)', fontSize: 11 }} itemStyle={{ color: '#67e8f9', fontSize: 12 }} />
|
||||
<Area type="monotone" dataKey="cpa" stroke="#22d3ee" strokeWidth={2} fillOpacity={1} fill="url(#gCpa)" name="CPA (AED)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* Ad-set CPA bar chart */}
|
||||
<Widget delay={0.28}>
|
||||
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-blue-400" /> Ad Set Performance
|
||||
</h3>
|
||||
<div className="h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={adSetPerf} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis dataKey="name" stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<Tooltip contentStyle={{ backgroundColor: 'rgba(8,10,18,0.92)', border: '1px solid rgba(59,130,246,0.2)', borderRadius: 10 }} labelStyle={{ color: 'rgba(148,163,184,0.8)', fontSize: 11 }} itemStyle={{ color: '#93c5fd', fontSize: 12 }} />
|
||||
<Bar dataKey="spend" fill="#3b82f6" radius={[4, 4, 0, 0]} name="Spend (AED)" opacity={0.8} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Live Optimization Feed (shared across tabs)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const EVENT_ICON: Record<LiveEventType, { icon: LucideIcon; color: string; bg: string; label: string }> = {
|
||||
pause: { icon: AlertTriangle, color: '#fbbf24', bg: 'rgba(251,191,36,0.12)', label: 'Paused' },
|
||||
shift: { icon: ArrowRightLeft, color: '#60a5fa', bg: 'rgba(96,165,250,0.12)', label: 'Budget Shift' },
|
||||
create: { icon: PlusCircle, color: '#4ade80', bg: 'rgba(74,222,128,0.12)', label: 'Created' },
|
||||
optimize: { icon: Zap, color: '#a78bfa', bg: 'rgba(167,139,250,0.12)', label: 'Optimized' },
|
||||
alert: { icon: AlertTriangle, color: '#f87171', bg: 'rgba(248,113,113,0.12)', label: 'Alert' },
|
||||
rotate: { icon: RefreshCw, color: '#22d3ee', bg: 'rgba(34,211,238,0.12)', label: 'Rotated' },
|
||||
};
|
||||
|
||||
function LiveEventItem({ event }: { event: LiveOptimizationEvent }) {
|
||||
const cfg = EVENT_ICON[event.type];
|
||||
const Icon = cfg.icon;
|
||||
return (
|
||||
<motion.div
|
||||
className="flex items-start gap-3 p-3 rounded-xl"
|
||||
style={INNER}
|
||||
initial={{ opacity: 0, x: -12, scale: 0.97 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 12, scale: 0.95 }}
|
||||
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
|
||||
layout
|
||||
>
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: cfg.bg }}>
|
||||
<Icon className="w-3.5 h-3.5" style={{ color: cfg.color }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: cfg.color }}>{cfg.label}</span>
|
||||
{event.campaignName && (
|
||||
<span className="text-[10px] px-1.5 py-px rounded" style={{ background: 'rgba(255,255,255,0.05)', color: 'rgba(148,163,184,0.6)' }}>
|
||||
{event.campaignName}
|
||||
</span>
|
||||
)}
|
||||
{event.value && (
|
||||
<span className="text-[10px] font-medium ml-auto" style={{ color: cfg.color }}>{event.value}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed" style={{ color: 'rgba(148,163,184,0.75)' }}>{event.message}</p>
|
||||
<p className="text-[10px] mt-1" style={{ color: 'rgba(148,163,184,0.35)' }}>
|
||||
{event.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const MOCK_STREAM: Array<{ type: LiveEventType; message: string; campaign: string; value?: string }> = [
|
||||
{ type: 'optimize', message: 'Expanded 3BHK audience targeting — added "Property Investment" interest layer.', campaign: '3BHK Prestige Launch', value: '+22k reach' },
|
||||
{ type: 'rotate', message: 'Rotated in Arabic Poster (Qwen-2512) as new creative variant for A/B test.', campaign: 'Penthouse Whale Retarget' },
|
||||
{ type: 'shift', message: 'Shifted AED 150 from underperforming Ad Set C to Ad Set A (CTR 3.2%).', campaign: '1BHK Investment', value: '+AED 150' },
|
||||
{ type: 'pause', message: 'Paused Ad Set D — CPA crossed AED 480 threshold (target: AED 400).', campaign: 'Penthouse Whale Retarget', value: 'CPA: AED 481' },
|
||||
{ type: 'create', message: 'Created new Custom Audience from 18 Closed/Won CRM leads (hashed emails).', campaign: '3BHK Prestige Launch' },
|
||||
];
|
||||
|
||||
function LiveOptimizationFeed() {
|
||||
const { liveEvents, pushLiveEvent } = useMarketingStore();
|
||||
const streamIdx = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
const item = MOCK_STREAM[streamIdx.current % MOCK_STREAM.length];
|
||||
streamIdx.current++;
|
||||
pushLiveEvent({
|
||||
id: `ev_${Date.now()}`,
|
||||
type: item.type,
|
||||
message: item.message,
|
||||
campaignName: item.campaign,
|
||||
timestamp: new Date(),
|
||||
value: item.value,
|
||||
});
|
||||
}, 4000);
|
||||
return () => clearInterval(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Widget delay={0.4} colSpan={1}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-blue-400" /> Live Optimization Feed
|
||||
</h3>
|
||||
<LiveBadge />
|
||||
</div>
|
||||
<div className="space-y-2 max-h-72 overflow-y-auto custom-scrollbar pr-1">
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{liveEvents.slice(0, 8).map((ev) => (
|
||||
<LiveEventItem key={ev.id} event={ev} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 4 — War Room (Meta Graph Settings)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function WarRoom() {
|
||||
const { settings, setMetaSettings } = useMarketingStore();
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
setMetaSettings({ isConnected: true });
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2500);
|
||||
};
|
||||
|
||||
const fields: Array<{ key: keyof typeof settings; label: string; desc: string; placeholder: string; type?: string }> = [
|
||||
{ key: 'metaAccessToken', label: 'Meta System User Access Token', desc: 'Long-lived system user token from Meta Business Manager', placeholder: 'EAAxxxxxxxx...' , type: 'password' },
|
||||
{ key: 'metaAdAccountId', label: 'Ad Account ID', desc: 'Format: act_XXXXXXXXXX', placeholder: 'act_123456789' },
|
||||
{ key: 'metaBusinessId', label: 'Business Portfolio ID', desc: 'Found in Meta Business Settings → Business Info', placeholder: '1234567890' },
|
||||
{ key: 'metaAppId', label: 'App ID', desc: 'From Meta Developers → Your App', placeholder: '9876543210' },
|
||||
{ key: 'whatsappPhoneNumberId',label: 'WhatsApp Phone Number ID', desc: 'Required for WhatsApp Business API integration', placeholder: '1098765432' },
|
||||
];
|
||||
|
||||
const assetLinks = [
|
||||
{ label: 'Instagram Business Page', href: '#', icon: Link2 },
|
||||
{ label: 'Facebook Business Page', href: '#', icon: Link2 },
|
||||
{ label: 'WhatsApp Business Account', href: '#', icon: Link2 },
|
||||
{ label: 'Ad Library (Creative Hub)', href: '#', icon: Link2 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Connection status banner */}
|
||||
<Widget delay={0} colSpan={1}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className={`w-3 h-3 rounded-full ${settings.isConnected ? 'bg-green-500' : 'bg-zinc-600'}`} />
|
||||
{settings.isConnected && <div className="absolute inset-0 rounded-full bg-green-500 status-pulse" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">Meta Graph API</p>
|
||||
<p className="text-xs" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
{settings.isConnected ? 'Connected — System User Token Active' : 'Not connected — enter credentials below'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" style={{ color: 'rgba(148,163,184,0.4)' }} />
|
||||
<span className="text-xs font-medium" style={{ color: settings.isConnected ? '#4ade80' : 'rgba(148,163,184,0.4)' }}>
|
||||
{settings.isConnected ? 'ONLINE' : 'OFFLINE'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* API Credentials */}
|
||||
<Widget delay={0.08}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Settings2 className="w-4 h-4 text-blue-400" />
|
||||
<h3 className="text-sm font-semibold text-white">API Credentials</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{fields.map(({ key, label, desc, placeholder, type }) => (
|
||||
<div key={key}>
|
||||
<p className="text-xs font-medium text-white mb-0.5">{label}</p>
|
||||
<p className="text-[10px] mb-1.5" style={{ color: 'rgba(148,163,184,0.5)' }}>{desc}</p>
|
||||
<DarkInput
|
||||
type={type ?? 'text'}
|
||||
value={String(settings[key])}
|
||||
onChange={(v) => setMetaSettings({ [key]: v } as Partial<typeof settings>)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<motion.button
|
||||
type="button"
|
||||
className="w-full py-2.5 mt-2 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all"
|
||||
style={{ background: saved ? 'rgba(74,222,128,0.15)' : 'hsl(var(--accent))', color: saved ? '#4ade80' : 'hsl(var(--accent-fg))' }}
|
||||
onClick={handleSave}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
{saved ? <><Check className="w-4 h-4" /> Saved & Connected</> : 'Save & Connect to Meta'}
|
||||
</motion.button>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* Business Asset Links */}
|
||||
<Widget delay={0.12}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Link2 className="w-4 h-4 text-blue-400" />
|
||||
<h3 className="text-sm font-semibold text-white">Business Asset Management</h3>
|
||||
</div>
|
||||
<p className="text-xs mb-4" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
Link WhatsApp, Instagram, and Facebook Pages to your Meta Business Portfolio for unified campaign management.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{assetLinks.map(({ label, icon: Icon }) => (
|
||||
<div key={label} className="flex items-center justify-between p-3 rounded-xl" style={INNER}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm text-white">{label}</span>
|
||||
</div>
|
||||
<GhostButton>
|
||||
<span className="text-xs">Configure</span>
|
||||
</GhostButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
<div className="mt-4 p-3 rounded-xl" style={{ background: 'rgba(59,130,246,0.06)', border: '1px solid rgba(59,130,246,0.15)' }}>
|
||||
<p className="text-xs font-medium text-blue-300 mb-1">Required Scopes</p>
|
||||
{['ads_management', 'ads_read', 'business_management', 'pages_manage_ads', 'whatsapp_business_management'].map((scope) => (
|
||||
<div key={scope} className="flex items-center gap-2 py-0.5">
|
||||
<Check className="w-3 h-3 text-blue-400" />
|
||||
<span className="text-[11px] font-mono" style={{ color: 'rgba(148,163,184,0.7)' }}>{scope}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tab Bar
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room';
|
||||
|
||||
const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
|
||||
{ id: 'studio', label: 'The Studio', icon: Clapperboard },
|
||||
{ id: 'command', label: 'Campaign Command', icon: Megaphone },
|
||||
{ id: 'intelligence', label: 'Intelligence & ROI', icon: BarChart3 },
|
||||
{ id: 'war-room', label: 'War Room', icon: Globe },
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Root export
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function Catalyst() {
|
||||
const { activeTab, setActiveTab } = useMarketingStore();
|
||||
|
||||
const panels: Record<TabId, React.ReactNode> = {
|
||||
'studio': <TheStudio />,
|
||||
'command': <CampaignCommand />,
|
||||
'intelligence': <IntelligenceROI />,
|
||||
'war-room': <WarRoom />,
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="space-y-5 pb-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white tracking-tight flex items-center gap-2">
|
||||
<Megaphone className="w-5 h-5 text-blue-400" /> The Catalyst
|
||||
</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
Autonomous Deployment Engine · Meta Marketing API
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div animate={{ opacity: [1, 0.4, 1] }} transition={{ repeat: Infinity, duration: 2 }}
|
||||
className="w-1.5 h-1.5 rounded-full bg-blue-400" />
|
||||
<span className="text-xs font-medium" style={{ color: '#60a5fa' }}>Claw Agent Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="flex items-center gap-1 p-1 rounded-2xl w-fit" style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)' }}>
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className="relative flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors outline-none"
|
||||
style={{ color: isActive ? 'white' : 'rgba(148,163,184,0.6)' }}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div layoutId="catalyst-tab-bg" className="absolute inset-0 rounded-xl"
|
||||
style={{ background: 'rgba(59,130,246,0.15)', border: '1px solid rgba(59,130,246,0.25)' }}
|
||||
transition={{ type: 'spring', stiffness: 380, damping: 30 }} />
|
||||
)}
|
||||
<Icon className="w-4 h-4 relative z-10" style={{ color: isActive ? '#60a5fa' : 'rgba(148,163,184,0.5)' }} />
|
||||
<span className="relative z-10">{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tab panel */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
{panels[activeTab]}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Live Optimization Feed — always visible */}
|
||||
<LiveOptimizationFeed />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user