import React, { useEffect, useState, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; const SvgIcon = ({ d, className = '' }: { d: string; className?: string }) => ( ); const IconEye = ({ className = '' }: { className?: string }) => ( ); const IconMousePointer = ({ className = '' }: { className?: string }) => ( ); const IconShoppingCart = ({ className = '' }: { className?: string }) => ( ); const IconPercent = ({ className = '' }: { className?: string }) => ( ); const IconDollarSign = ({ className = '' }: { className?: string }) => ( ); const IconUsers = ({ className = '' }: { className?: string }) => ( ); const IconTrendingUp = ({ className = '' }: { className?: string }) => ( ); const IconMessageSquare = ({ className = '' }: { className?: string }) => ( ); const IconClock = ({ className = '' }: { className?: string }) => ( ); const IconTarget = ({ className = '' }: { className?: string }) => ( ); const IconThumbsUp = ({ className = '' }: { className?: string }) => ( ); const IconZap = ({ className = '' }: { className?: string }) => ( ); const IconHelpCircle = ({ className = '' }: { className?: string }) => ( ); const IconAlertTriangle = ({ className = '' }: { className?: string }) => ( ); import { getCampaign, updateCampaign, deleteCampaign, activateCampaign, pauseCampaign, resumeCampaign, completeCampaign, previewCampaignProducts, getCampaignAnalytics, getCampaignConversations, } from '../service/campaigns/campaigns.service'; import type { ICampaign, ICampaignMetrics, ICampaignJourneyItem, } from '../service/campaigns/campaigns.interface'; import KPICard from '../components/agent-analytics/KPICard'; import Section from '../components/agent-analytics/Section'; import HBar from '../components/agent-analytics/HBar'; import { formatNumber, formatDateTime, formatIntentScore, RECOMAZE_COLORS, } from '../components/agent-analytics/helpers'; import { getOutcomeBadgeTone } from '../components/agent-analytics/conversationsTab.utils'; import JourneyDetailModal from '../components/agent-analytics/JourneyDetailModal'; const STATUS_COLORS: Record = { active: 'bg-green-100 text-green-800', paused: 'bg-yellow-100 text-yellow-800', building: 'bg-blue-100 text-blue-800', draft: 'bg-gray-100 text-gray-800', completed: 'bg-gray-100 text-gray-800', failed: 'bg-red-100 text-red-800', }; const ANALYTICS_STATUSES = ['active', 'paused', 'completed']; const SENTIMENT_COLORS = { positive: '#22c55e', neutral: '#94a3b8', negative: '#ef4444', }; const OUTCOME_COLORS: Record = { purchase_intent: RECOMAZE_COLORS.primary, abandoned: '#ef4444', information_only: '#6366f1', comparison: '#f59e0b', support_resolved: '#22c55e', complaint: '#dc2626', }; const OUTCOME_BADGE_CLASSES: Record = { success: 'bg-green-100 text-green-800', critical: 'bg-red-100 text-red-800', warning: 'bg-yellow-100 text-yellow-800', info: 'bg-blue-100 text-blue-800', }; const CampaignDetail = (): JSX.Element => { const navigate = useNavigate(); const { id: campaignId } = useParams<{ id: string }>(); const [campaign, setCampaign] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editPrompt, setEditPrompt] = useState(''); const [saving, setSaving] = useState(false); const [previewCount, setPreviewCount] = useState(0); const [analytics, setAnalytics] = useState(null); const [loadingAnalytics, setLoadingAnalytics] = useState(false); const [conversations, setConversations] = useState( [] ); const [nextCursor, setNextCursor] = useState(null); const [loadingMore, setLoadingMore] = useState(false); const [loadingConversations, setLoadingConversations] = useState(false); const [selectedFp, setSelectedFp] = useState(null); const [outcomeFilter, setOutcomeFilter] = useState(''); const [actionLoading, setActionLoading] = useState(null); const fetchCampaign = useCallback(async () => { if (!campaignId) return; try { setLoading(true); setError(null); const response = await getCampaign(campaignId); if (!response.errors && response.data) { setCampaign(response.data); setEditPrompt(response.data.prompt_instruction || ''); } } catch (err: any) { setError(err?.message || 'Failed to load campaign'); } finally { setLoading(false); } }, [campaignId]); const fetchPreview = useCallback(async () => { if (!campaignId) return; try { const res = await previewCampaignProducts(campaignId); if (!res.errors && res.data) { setPreviewCount(res.data.count || 0); } } catch { /* noop */ } }, [campaignId]); const fetchAnalytics = useCallback(async () => { if (!campaignId || !campaign) return; if (!ANALYTICS_STATUSES.includes(campaign.status)) return; setLoadingAnalytics(true); try { const res = await getCampaignAnalytics(campaignId); if (!res.errors && res.data) { setAnalytics(res.data); } } catch { /* ignore */ } finally { setLoadingAnalytics(false); } }, [campaignId, campaign?.status]); const fetchConversations = useCallback( async (cursor?: string | null) => { if (!campaignId || !campaign) return; if (!ANALYTICS_STATUSES.includes(campaign.status)) return; const isLoadMore = !!cursor; if (isLoadMore) { setLoadingMore(true); } else { setLoadingConversations(true); } try { const res = await getCampaignConversations(campaignId, 20, cursor); if (!res.errors && res.data) { if (isLoadMore) { setConversations(prev => [...prev, ...(res.data.journeys || [])]); } else { setConversations(res.data.journeys || []); } setNextCursor(res.data.next_cursor || null); } } finally { setLoadingMore(false); setLoadingConversations(false); } }, [campaignId, campaign?.status] ); useEffect(() => { fetchCampaign(); }, [fetchCampaign]); // The Reco copilot dispatches ``copilot:data-changed`` after a turn where a // write tool ran (pause/resume/complete/update a campaign, etc.). WordPress // has no route loaders to revalidate, so this page re-fetches the campaign // when the event fires - mirroring the Remix ``revalidator.revalidate()`` // the copilot used in the Shopify plugin. The listener is removed on unmount. useEffect(() => { const onCopilotDataChanged = (): void => { void fetchCampaign(); }; window.addEventListener('copilot:data-changed', onCopilotDataChanged); return () => { window.removeEventListener('copilot:data-changed', onCopilotDataChanged); }; }, [fetchCampaign]); // Poll while campaign is building useEffect(() => { if (campaign?.status !== 'building') return; const interval = setInterval(async () => { if (!campaignId) return; try { const response = await getCampaign(campaignId); if (!response.errors && response.data) { setCampaign(response.data); setEditPrompt(response.data.prompt_instruction || ''); } } catch { /* ignore polling errors */ } }, 5000); return () => clearInterval(interval); }, [campaign?.status, campaignId]); useEffect(() => { if (campaign) { fetchPreview(); fetchAnalytics(); fetchConversations(); } }, [campaign?.campaign_id]); const handleAction = async ( action: 'activate' | 'pause' | 'resume' | 'complete' | 'delete' ) => { if (!campaignId) return; const actions = { activate: activateCampaign, pause: pauseCampaign, resume: resumeCampaign, complete: completeCampaign, delete: deleteCampaign, }; try { setActionLoading(action); await actions[action](campaignId); if (action === 'delete') { navigate('/campaigns'); } else { await fetchCampaign(); } } catch (err: any) { setError(err?.message || `Failed to ${action} campaign`); } finally { setActionLoading(null); } }; const handleSavePrompt = async () => { if (!campaignId) return; try { setSaving(true); const response = await updateCampaign(campaignId, { prompt_instruction: editPrompt, }); if (!response.errors && response.data) { setCampaign(response.data); } } catch (err: any) { setError(err?.message || 'Failed to save prompt'); } finally { setSaving(false); } }; if (loading) { return (
); } if (!campaign) { return (

{error || 'Campaign not found'}

); } const hasAnalytics = ANALYTICS_STATUSES.includes(campaign.status); const funnelMax = Math.max(analytics?.impressions || 0, 1); const conv = analytics?.conversations; const sentimentTotal = (conv?.sentiment_breakdown?.positive || 0) + (conv?.sentiment_breakdown?.neutral || 0) + (conv?.sentiment_breakdown?.negative || 0); const outcomeEntries = Object.entries(conv?.outcome_distribution || {}).sort( (a, b) => b[1] - a[1] ); const outcomeMax = Math.max(...outcomeEntries.map(([, v]) => v), 1); const filteredConversations = outcomeFilter ? conversations.filter(j => j.outcome === outcomeFilter) : conversations; return (
{/* Header */}

{campaign.name}

{campaign.status} {campaign.priority}
{campaign.status === 'draft' && ( )} {campaign.status === 'active' && ( )} {campaign.status === 'paused' && ( )} {['active', 'paused'].includes(campaign.status) && ( )} {['draft', 'failed', 'completed'].includes(campaign.status) && ( )}
{error && (

{error}

)} {/* Campaign info */}
Categories: {campaign.target_categories.map(cat => ( {cat} ))}
{campaign.description && (

{campaign.description}

)}
{campaign.goal && ( {campaign.goal} )} {campaign.tone_of_voice && ( {campaign.tone_of_voice.split(' - ')[0]} )} {(campaign.min_price || campaign.max_price) && ( Price: {campaign.min_price ? `${campaign.min_price}` : '0'} -{' '} {campaign.max_price ? `${campaign.max_price}` : '\u221E'} )}
{campaign.discount_codes && campaign.discount_codes.length > 0 && (
Discount codes:
{campaign.discount_codes.map(dc => ( {dc.code} ( {dc.type === 'percentage' ? `${dc.value}%` : dc.value}) ยท{' '} {dc.used_count}/{dc.max_uses || '\u221E'} ))}
)}
{campaign.indexed_product_count > 0 && ( {campaign.indexed_product_count} products indexed )} {previewCount > 0 && ( {previewCount} matching products )} {campaign.created_at && ( Created:{' '} {new Date(campaign.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })} )}
{/* Analytics */} {hasAnalytics && ( <> {loadingAnalytics ? (
) : analytics ? ( <> {/* Product Performance KPIs */}

Product Performance

{/* Campaign Funnel + Cart Value */}
${(analytics.cart_value || 0).toFixed(2)}

Avg per Click

$ {(analytics.clicks ?? 0) > 0 ? ( (analytics.cart_value ?? 0) / analytics.clicks ).toFixed(2) : '0.00'}

Avg per Session

$ {(analytics.unique_sessions ?? 0) > 0 ? ( (analytics.cart_value ?? 0) / analytics.unique_sessions ).toFixed(2) : '0.00'}

{/* Conversation Analytics KPIs */} {conv && conv.total_conversations > 0 && ( <>

Conversation Analytics

{/* Conversation details row */}
{/* Sentiment breakdown */}
{sentimentTotal > 0 ? ( <>
{( ['positive', 'neutral', 'negative'] as const ).map(s => { const val = conv.sentiment_breakdown[s] || 0; const pct = (val / sentimentTotal) * 100; if (pct === 0) return null; return (
); })}
{( ['positive', 'neutral', 'negative'] as const ).map(s => (
{s}: {conv.sentiment_breakdown[s] || 0}
))}
) : (

No sentiment data yet

)}
{/* Outcome distribution */}
{outcomeEntries.length > 0 ? ( outcomeEntries.map(([outcome, count]) => ( )) ) : (

No outcome data yet

)}
{/* Extra stats row */}

Avg Messages/Conv

{conv.avg_messages ?? 0}

Avg Duration

{conv.avg_duration_minutes ?? 0}{' '} min

High Intent Rate

{conv.high_intent_rate ?? 0}%

Satisfaction Rate

{conv.satisfaction_rate ?? 0}%

{/* Questions & Pain Points */}
{conv.top_questions && conv.top_questions.length > 0 && (
{conv.top_questions.map((q, i) => (

{q.question}

{q.count}x
))}
)} {conv.top_pain_points && conv.top_pain_points.length > 0 && (
{conv.top_pain_points.map((pp, i) => (

{pp.issue}

{pp.count}x
))}
)}
)} {/* Top Products */} {analytics.top_products && analytics.top_products.length > 0 && ( <>
{analytics.top_products.slice(0, 10).map((p, i) => (
{i + 1}
{p.name}
{p.impressions} views {p.clicks} clicks {p.add_to_cart} cart
))}
)} ) : (

No analytics data yet. Data appears after visitors interact with campaign products.

)} )} {/* Conversations */} {hasAnalytics && ( <>

Campaign Conversations

{loadingConversations ? (
) : conversations.length === 0 ? (

No conversations found for this campaign yet.

) : ( <>

{outcomeFilter ? `${filteredConversations.length} of ${conversations.length}` : `${conversations.length}`}{' '} Conversations

{filteredConversations.map(j => ( setSelectedFp(j.fingerprint)} > ))}
Date Duration Messages Outcome Intent Clicks Carts Stage
{formatDateTime(j.first_message_at)} {j.duration_minutes != null ? `${j.duration_minutes.toFixed(1)}m` : '-'} {j.message_count || 0} {j.outcome || '-'} {formatIntentScore(j.purchase_intent_score)} {j.product_clicks || 0} {j.add_to_cart_count || 0} {j.last_stage ? ( {j.last_stage} ) : ( '-' )}

Click a row to view conversation details

{nextCursor && (
)}
{selectedFp && ( setSelectedFp(null)} /> )} )} )}
{/* Prompt editor */}