import React, { useState } from 'react'; import { generateArticle } from '../../service/visibility/visibility.service'; import ArticleModal from './ArticleModal'; import ArticleProgress from './ArticleProgress'; import GenerateLanguagesDialog from './GenerateLanguagesDialog'; interface TopWinsLossesProps { topWins: string[]; topLosses: string[]; brandId: string; clientId: string; token: string; language?: string | null; /** * Bubbled up when a loss row's generated article finishes. Lets the * parent dashboard refetch the Articles list / tab counts so the new * article appears in the Recommended section without a manual page * refresh. */ onArticleGenerated?: () => void; } const MAX_ROWS = 5; /** * Two side-by-side cards: * - "Where you appeared this week" (green) - ``top_wins`` * - "Where you missed out" (orange) - ``top_losses`` + "Generate * article" button that dispatches article generation with the loss * query pre-filled as the topic. */ const TopWinsLosses = ({ topWins, topLosses, brandId, clientId, token, language, onArticleGenerated, }: TopWinsLossesProps): JSX.Element | null => { // Loss row ({index, topic}) awaiting the "which languages?" dialog before // its article is dispatched. Null when the dialog is closed. const [langDialogLoss, setLangDialogLoss] = useState<{ index: number; topic: string; } | null>(null); const [pendingByLoss, setPendingByLoss] = useState>( {} ); const [errorByLoss, setErrorByLoss] = useState>({}); const [busyByLoss, setBusyByLoss] = useState>({}); // Loss indices whose generated article landed ``ready`` in this // session - hidden from the rendered list so the merchant stops // seeing a "Generate article" CTA over a topic they just covered. const [dismissedLosses, setDismissedLosses] = useState>( () => new Set() ); const [openArticleId, setOpenArticleId] = useState(null); if (topWins.length === 0 && topLosses.length === 0) return null; const handleGenerate = async ( lossIndex: number, topic: string, languages: string[] ) => { setLangDialogLoss(null); setBusyByLoss(prev => ({ ...prev, [lossIndex]: true })); setErrorByLoss(prev => { const next = { ...prev }; delete next[lossIndex]; return next; }); try { const dispatch = await generateArticle(clientId, token, { brand_id: brandId, topic, // Anchor the article to the originating loss so the backend can // drop this row from "Where you missed out" on subsequent // weekly-report fetches. target_prompts: [topic], language: language ?? undefined, languages: languages.length ? languages : undefined, triggered_by: 'manual', }); setPendingByLoss(prev => ({ ...prev, [lossIndex]: dispatch.article_id, })); } catch (err) { setErrorByLoss(prev => ({ ...prev, [lossIndex]: err instanceof Error ? err.message : 'Failed to start generation.', })); } finally { setBusyByLoss(prev => ({ ...prev, [lossIndex]: false })); } }; const visibleWins = topWins.slice(0, MAX_ROWS); const visibleLosses = topLosses .slice(0, MAX_ROWS) .map((loss, index) => ({ loss, index })) .filter(({ index }) => !dismissedLosses.has(index)); return ( <>
{/* Wins */}

Where you appeared this week

{visibleWins.length === 0 ? (

No wins recorded for this week.

) : (
    {visibleWins.map((win, idx) => (
  • {win}
  • ))}
)}
{/* Losses */}

Where you missed out

{visibleLosses.length === 0 ? (

No missed opportunities flagged this week.

) : (
    {visibleLosses.map(({ loss, index: idx }, position) => { const pendingId = pendingByLoss[idx]; const isGenerating = Boolean(pendingId); const err = errorByLoss[idx]; return (
  • {loss}
    {err &&

    {err}

    } {isGenerating && pendingId && ( { setPendingByLoss(prev => { const next = { ...prev }; delete next[idx]; return next; }); setDismissedLosses(prev => { const next = new Set(prev); next.add(idx); return next; }); setOpenArticleId(article.summary.article_id); onArticleGenerated?.(); }} onFailed={message => setErrorByLoss(prev => ({ ...prev, [idx]: message, })) } /> )}
  • ); })}
)}
setOpenArticleId(null)} clientId={clientId} token={token} /> { if (!next) setLangDialogLoss(null); }} primaryLanguage={language || 'en'} busy={ langDialogLoss ? Boolean(busyByLoss[langDialogLoss.index]) : false } onConfirm={languages => { if (langDialogLoss) { void handleGenerate( langDialogLoss.index, langDialogLoss.topic, languages ); } }} /> ); }; export default TopWinsLosses;