Built the Oracle Tab (#14)

This commit is contained in:
2026-04-11 19:35:45 +05:30
committed by Sagnik
parent 8e1ffe0e43
commit fb656d1443
54 changed files with 10651 additions and 818 deletions

View File

@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import {
Megaphone, Clapperboard, BarChart3, Globe, Settings2,
Zap, TrendingUp, Eye, MousePointerClick, DollarSign,
Upload, Play, Image, Film, RefreshCw, ArrowRight, Plus,
Upload, Play, Image, Film, RefreshCw, ArrowRight, Plus, X,
AlertTriangle, ArrowRightLeft, PlusCircle, SlidersHorizontal,
Activity, Check, Link2,
type LucideIcon,
@@ -13,7 +13,10 @@ import {
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts';
import { useMarketingStore } from '@/store/useMarketingStore';
import { useCurrency } from '@/store/useCurrencyStore';
import type { Campaign, MarketingAsset, LiveOptimizationEvent, LiveEventType } from '@/types';
import { GroundTruthPicker } from './GroundTruthPicker';
import type { GroundTruthSelection } from './GroundTruthPicker';
// ── Design tokens ─────────────────────────────────────────────────────────────
const GLASS = {
@@ -243,22 +246,151 @@ function AssetCard({ asset }: { asset: MarketingAsset }) {
);
}
// ── Reference Slot ───────────────────────────────────────────────────────────
interface RefSelection { name: string; preview: string; }
function ReferenceSlot({ value, onSelect, onClear, onRemove }: {
value: RefSelection | null;
onSelect: (sel: RefSelection) => void;
onClear: () => void;
onRemove?: () => void;
}) {
const inputRef = useRef<HTMLInputElement>(null);
function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
onSelect({ name: file.name, preview: URL.createObjectURL(file) });
e.target.value = '';
}
// Show X when: slot has content (clear it), or slot is removable (remove it)
const showX = !!value || !!onRemove;
const handleX = (e: React.MouseEvent) => {
e.stopPropagation();
if (onRemove) onRemove();
else onClear();
};
return (
<div className="relative w-[60px] h-[60px]">
<motion.button
className="w-[60px] h-[60px] rounded-2xl flex flex-col items-center justify-center gap-1 overflow-hidden transition-colors"
style={{
background: value?.preview ? 'transparent' : 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.08)',
}}
onClick={() => inputRef.current?.click()}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
title="Add reference image"
>
{value?.preview ? (
<img src={value.preview} alt={value.name} className="w-full h-full object-cover" />
) : value?.name ? (
<>
<Check className="w-4 h-4 text-blue-400" />
<span className="text-[8px] text-blue-400 font-medium text-center px-1 leading-tight line-clamp-2">{value.name}</span>
</>
) : (
<span className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.2)' }}>+</span>
)}
</motion.button>
<AnimatePresence>
{showX && (
<motion.button
className="absolute -top-2 -right-3 w-4 h-4 rounded-full flex items-center justify-center z-10"
style={{ background: 'rgba(20,20,30,0.95)', border: '1px solid rgba(255,255,255,0.22)' }}
onClick={handleX}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.6 }}
transition={{ duration: 0.12 }}
whileHover={{ scale: 1.2, backgroundColor: 'rgba(239,68,68,0.8)' }}
title={onRemove ? 'Remove slot' : 'Clear selection'}
>
<X className="w-2 h-2 text-white" />
</motion.button>
)}
</AnimatePresence>
<input ref={inputRef} type="file" accept="image/*,video/*" className="hidden" onChange={handleFile} />
</div>
);
}
function WorkflowInput() {
const [mode, setMode] = useState<'image' | 'video'>('image');
const [prompt, setPrompt] = useState('');
const [keywords, setKeywords] = useState('');
const [textCopy, setTextCopy] = useState('');
const [groundTruth, setGroundTruth] = useState<GroundTruthSelection | null>(null);
const [pickerOpen, setPickerOpen] = useState(false);
const [refs, setRefs] = useState<(RefSelection | null)[]>([null, null]);
const anchorRef = useRef<HTMLButtonElement>(null);
return (
<Widget delay={0.04} colSpan={1} className="!p-6" style={{ background: '#111216', borderRadius: '28px' }}>
<Widget delay={0.04} colSpan={1} className="!p-6 !overflow-visible" style={{ background: '#111216', borderRadius: '28px' }}>
<div className="flex flex-col gap-6">
{/* Top Section: Ground Truth & References */}
<div className="flex items-end gap-6">
{/* Ground Truth slot */}
<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 className="relative">
<motion.button
ref={anchorRef}
onClick={() => setPickerOpen(v => !v)}
className="w-[60px] h-[60px] rounded-2xl flex flex-col items-center justify-center gap-1 overflow-hidden transition-colors"
style={{
background: groundTruth?.preview ? 'transparent' : pickerOpen ? 'rgba(59,130,246,0.12)' : 'rgba(255,255,255,0.03)',
border: pickerOpen ? '1px solid rgba(59,130,246,0.4)' : '1px solid rgba(255,255,255,0.08)',
}}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
title="Select Ground Truth image"
>
{groundTruth?.preview ? (
<img src={groundTruth.preview} alt="ground truth" className="w-full h-full object-cover" />
) : groundTruth?.name ? (
<>
<Check className="w-4 h-4 text-green-400" />
<span className="text-[8px] text-green-400 font-medium text-center px-1 leading-tight line-clamp-2">{groundTruth.name}</span>
</>
) : (
<span className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.2)' }}>{pickerOpen ? '✕' : '+'}</span>
)}
</motion.button>
{/* Clear selection button */}
<AnimatePresence>
{groundTruth && (
<motion.button
className="absolute -top-2 right-2 w-4 h-4 rounded-full flex items-center justify-center z-10"
style={{ background: 'rgba(20,20,30,0.95)', border: '1px solid rgba(255,255,255,0.22)' }}
onClick={(e) => { e.stopPropagation(); setGroundTruth(null); setPickerOpen(false); }}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.6 }}
transition={{ duration: 0.12 }}
whileHover={{ scale: 1.2, backgroundColor: 'rgba(239,68,68,0.8)' }}
title="Remove selection"
>
<X className="w-2 h-2 text-white" />
</motion.button>
)}
</AnimatePresence>
<AnimatePresence>
{pickerOpen && (
<GroundTruthPicker
anchorRef={anchorRef}
onSelect={sel => { setGroundTruth(sel); setPickerOpen(false); }}
onClose={() => setPickerOpen(false)}
/>
)}
</AnimatePresence>
</div>
<span className="text-xs font-semibold text-white tracking-wide">Ground Truth</span>
</div>
@@ -269,11 +401,25 @@ function WorkflowInput() {
<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)' }}>
{refs.map((ref, i) => (
<ReferenceSlot
key={i}
value={ref}
onSelect={(sel) => setRefs(prev => prev.map((r, idx) => idx === i ? sel : r))}
onClear={() => setRefs(prev => prev.map((r, idx) => idx === i ? null : r))}
onRemove={i >= 2 ? () => setRefs(prev => prev.filter((_, idx) => idx !== i)) : undefined}
/>
))}
<motion.button
title="Add more references"
className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors"
style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}
onClick={() => setRefs(prev => prev.length < 6 ? [...prev, null] : prev)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Plus className="w-5 h-5 text-white/40" />
</button>
</motion.button>
</div>
<span className="text-xs font-semibold text-white tracking-wide">References</span>
</div>
@@ -389,6 +535,7 @@ function TheStudio() {
function CampaignCommand() {
const { campaigns, adInsights } = useMarketingStore();
const { formatAmount, rate } = useCurrency();
const totalSpend = campaigns.reduce((s, c) => s + c.lifetimeSpend / 100, 0);
const totalImpr = campaigns.reduce((s, c) => s + c.impressions, 0);
@@ -398,7 +545,7 @@ function CampaignCommand() {
// 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,
spend: Math.round(d.spend * rate),
impressions: Math.round(d.impressions / 1000),
})).reverse();
@@ -407,9 +554,9 @@ function CampaignCommand() {
{/* 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={DollarSign} label="Total Spend" value={formatAmount(totalSpend, { compact: true })} 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} />
<StatCard icon={MousePointerClick} label="Avg CTR" value={`${avgCtr.toFixed(2)}%`} sub={`Avg CPA: ${formatAmount(avgCpa)}`} glowColor="rgba(167,139,250,0.2)" delay={0.18} />
</div>
{/* Spend chart + campaign list */}
@@ -463,7 +610,7 @@ function CampaignCommand() {
<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.
{formatAmount(c.lifetimeSpend / 100)} · {c.impressions.toLocaleString()} impr.
</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
@@ -487,24 +634,25 @@ function CampaignCommand() {
function IntelligenceROI() {
const { campaigns, adInsights } = useMarketingStore();
const { formatAmount, symbol, rate } = useCurrency();
const cpaData = adInsights.slice(0, 7).map((d) => ({
date: d.date.slice(5),
cpa: d.cpa,
cpa: Math.round(d.cpa * rate),
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 },
{ name: '3BHK Dubai', ctr: 2.1, cpa: Math.round(210 * rate), spend: Math.round(3400 * rate) },
{ name: 'PH Retarget', ctr: 3.2, cpa: Math.round(2100 * rate), spend: Math.round(5800 * rate) },
{ name: '1BHK Lookalike', ctr: 1.8, cpa: Math.round(270 * rate), spend: Math.round(980 * rate) },
];
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={DollarSign} label="Avg CPA" value={formatAmount(campaigns.reduce((s,c)=>s+c.cpa,0)/campaigns.length)} 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} />
@@ -529,7 +677,7 @@ function IntelligenceROI() {
<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)" />
<Area type="monotone" dataKey="cpa" stroke="#22d3ee" strokeWidth={2} fillOpacity={1} fill="url(#gCpa)" name={`CPA (${symbol})`} />
</AreaChart>
</ResponsiveContainer>
</div>
@@ -547,7 +695,7 @@ function IntelligenceROI() {
<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} />
<Bar dataKey="spend" fill="#3b82f6" radius={[4, 4, 0, 0]} name={`Spend (${symbol})`} opacity={0.8} />
</BarChart>
</ResponsiveContainer>
</div>
@@ -572,6 +720,7 @@ const EVENT_ICON: Record<LiveEventType, { icon: LucideIcon; color: string; bg: s
function LiveEventItem({ event }: { event: LiveOptimizationEvent }) {
const cfg = EVENT_ICON[event.type];
const { formatText } = useCurrency();
const Icon = cfg.icon;
return (
<motion.div
@@ -596,10 +745,10 @@ function LiveEventItem({ event }: { event: LiveOptimizationEvent }) {
</span>
)}
{event.value && (
<span className="text-[10px] font-medium ml-auto" style={{ color: cfg.color }}>{event.value}</span>
<span className="text-[10px] font-medium ml-auto" style={{ color: cfg.color }}>{formatText(event.value)}</span>
)}
</div>
<p className="text-xs leading-relaxed" style={{ color: 'rgba(148,163,184,0.75)' }}>{event.message}</p>
<p className="text-xs leading-relaxed" style={{ color: 'rgba(148,163,184,0.75)' }}>{formatText(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>