import React, { useState, useRef, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import { PlusIcon } from './icons/PlusIcon'; import { ArrowLeftIcon } from './icons/ArrowLeftIcon'; import type { AgentReview, FlaggedItem, ResolvedItem, OverallStatus } from '../types'; import { FeedbackReport } from './FeedbackReport'; import { CreateCampaignModal } from './CreateCampaignModal'; import { CheckCircleIcon, ArrowPathIcon } from './icons/StatusIcons'; import { ProofPreview } from './ProofPreview'; import { HistoryIcon } from './icons/HistoryIcon'; import { DropdownOptions } from '../App'; import { ChannelIcon } from './icons/ChannelIcon'; import { TagIcon } from './icons/TagIcon'; import { ChevronDownIcon } from './icons/ChevronDownIcon'; import { UploadIcon } from './icons/UploadIcon'; import { DocumentIcon } from './icons/DocumentIcon'; import { SpinnerIcon } from './icons/SpinnerIcon'; import { AGENT_NAMES } from '../constants'; import { ToggleSwitch } from './ToggleSwitch'; import { TrashIcon } from './icons/TrashIcon'; import { DownloadIcon } from './icons/DownloadIcon'; import { PDFIcon } from './icons/PDFIcon'; import { PDFReport } from './PDFReport'; import { ExportIcon } from './icons/ExportIcon'; import { XIcon } from './icons/XIcon'; export const initialCampaigns = [ { name: 'Barclays Q4 campaign', workfrontId: '#WF_12822', clientLead: 'Jane Doe', agency: 'OLIVER Agency', agencyLead: 'Steve O\'Donoghue', proofs: 3, status: 'In Progress', lastModified: '2024-07-22', brandGuidelines: 'Barclays', }, { name: 'Barclays Q3 Roundup', workfrontId: '#WF_12750', clientLead: 'Jane Doe', agency: 'OLIVER Agency', agencyLead: 'Steve O\'Donoghue', proofs: 1, status: 'Completed', lastModified: '2024-06-30', brandGuidelines: 'Barclays', }, ]; const IG_HERO_POST_1_IMAGE_V1 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSIxMDgwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiMwMDFmNWEiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2lçZz0iODAiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj5JRyBIZXJvIFBvc3QgMSB2MVc8L3RleHQ+PHRleHQgeD0iNTAlIiB5PSI2MCUiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjQwIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+KEFzc2V0IFByZXZpZXcpPC90ZXh0Pjwvc3ZnPg=='; const IG_HERO_POST_1_IMAGE_V2 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSIxMDgwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiMwMDcwYzAiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iODAiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj5JRyBIZXJvIFBvc3QgMSB2MjwvdGV4dD48dGV4dCB4PSI1MCUiIHk9IjYwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNDAiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj4oQ29ycmVjdGlvbnMgQXBwbGllZCk8L3RleHQ+PC9zdmc+'; const IG_HERO_POST_1_FEEDBACK_V1: AgentReview = { overallStatus: "Analysis Error", leadAgentSummary: "The proof could not be reliably processed by some agents due to low confidence in the analysis. This may happen if the proof is too abstract or irrelevant to typical marketing content. This version has been logged for manual human review. Please try uploading a revised version of the proof.", legalAgentReview: { ragStatus: "Error", feedback: "The agent could not analyze this proof with high confidence. This may be because the content is irrelevant, nonsensical, or too far outside of expected marketing materials.", issues: [] }, brandAgentReview: { ragStatus: "Error", feedback: "The agent could not analyze this proof with high confidence. This may be because the content is irrelevant, nonsensical, or too far outside of expected marketing materials.", issues: [] }, toneAgentReview: { ragStatus: "Green", feedback: "The copy is clear, concise, and professional. The tone aligns well with the brand's voice and effectively communicates the new partnership.", issues: [] }, channelAgentReview: { ragStatus: "Amber", feedback: "While the image quality is high, the composition is not optimized for vertical formats like Instagram Stories. The high ratio of text in the image could also potentially reduce advertising reach on some platforms.", issues: ["Image composition is not mobile-first or suitable for vertical formats.", "High ratio of text in the image may negatively impact ad performance."] } }; const IG_HERO_POST_1_FEEDBACK_V2: AgentReview = { overallStatus: "Passed", leadAgentSummary: "The proof has Passed review. All critical legal and brand issues from the previous version have been successfully addressed. The partnership claim is clear, and the design now fully aligns with brand guidelines. One minor suggestion is to consider a separate vertical version for Story placements, but the proof is approved for deployment.", legalAgentReview: { ragStatus: "Green", feedback: "All legal concerns have been addressed. The partnership claim is now clearly stated as 'Proud principal partner' and all necessary documentation for the use of the Lord's branding has been verified.", issues: [] }, brandAgentReview: { ragStatus: "Green", feedback: "The proof now fully aligns with brand guidelines. The 'Principal partner' lockup uses the correct brand font, and the non-standard blue border has been removed, resulting in a cleaner, on-brand look.", issues: [] }, toneAgentReview: { ragStatus: "Green", feedback: "The copy remains clear, professional, and well-aligned with the brand's tone of voice. No issues found.", issues: [] }, channelAgentReview: { ragStatus: "Amber", feedback: "The image composition has been improved and is suitable for standard feeds. However, for optimal performance on Instagram, creating a separate vertical version for Story placements is recommended.", issues: ["Consider creating a separate vertical version for Story placements."] } }; export const initialCampaignProofs: { [key: string]: any[] } = { 'Barclays Q4 campaign': [ { proofName: 'IG Hero Post 1', channel: 'Social', subChannel: 'Meta', proofType: 'In-feed 4x5', status: 'completed', overallStatus: 'Passed', versions: [ { version: 2, timestamp: '2024-07-25', workfrontId: '#WF_12823-V2', proofPreviewUrl: IG_HERO_POST_1_IMAGE_V2, feedback: IG_HERO_POST_1_FEEDBACK_V2, overallStatus: 'Passed', }, { version: 1, timestamp: '2024-07-23', workfrontId: '#WF_12823-V1', proofPreviewUrl: IG_HERO_POST_1_IMAGE_V1, feedback: IG_HERO_POST_1_FEEDBACK_V1, overallStatus: 'Analysis Error', } ] }, { proofName: 'Q4 FB Static 4x5', channel: 'Social', subChannel: 'Meta', proofType: 'In-feed 4x5', status: 'completed', overallStatus: 'Passed', versions: [ { version: 1, timestamp: '2024-07-26', workfrontId: '#WF_12824-V1', proofPreviewUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSIxMzUwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiMwMDcwYzAiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iODAiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj5GQiBTdGF0aWMgNHg1PC90ZXh0Pjwvc3ZnPg==', feedback: { overallStatus: "Passed", leadAgentSummary: "Passed. Compliant with all guidelines.", legalAgentReview: { ragStatus: "Green", feedback: "Disclaimers present and correct.", issues: [] }, brandAgentReview: { ragStatus: "Green", feedback: "On brand colors used.", issues: [] }, toneAgentReview: { ragStatus: "Green", feedback: "Professional tone.", issues: [] }, channelAgentReview: { ragStatus: "Green", feedback: "Correct specs for FB.", issues: [] } }, overallStatus: 'Passed', } ] }, { proofName: 'Q4 IG Story Promo', channel: 'Social', subChannel: 'Meta', proofType: 'Stories Static 9x16', status: 'completed', overallStatus: 'Passed', versions: [ { version: 1, timestamp: '2024-07-27', workfrontId: '#WF_12825-V1', proofPreviewUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSIxOTIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiMwMDFmNWEiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iODAiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj5JRyBTdG9yeTwvdGV4dD48L3N2Zz4=', feedback: { overallStatus: "Passed", leadAgentSummary: "Approved for IG Stories.", legalAgentReview: { ragStatus: "Green", feedback: "Compliant.", issues: [] }, brandAgentReview: { ragStatus: "Green", feedback: "Brand aligned.", issues: [] }, toneAgentReview: { ragStatus: "Green", feedback: "Engaging copy.", issues: [] }, channelAgentReview: { ragStatus: "Green", feedback: "Optimized for mobile.", issues: [] } }, overallStatus: 'Passed', } ] } ], 'Barclays Q3 Roundup': [ { proofName: 'Q3 Results Infographic', channel: 'Display', subChannel: 'Banner', proofType: '300x600', status: 'completed', overallStatus: 'Passed', versions: [ { version: 1, timestamp: '2024-06-28', workfrontId: '#WF_12751-V1', proofPreviewUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSIxMDgwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNmNGY2ZjgiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iODAiIGZpbGw9IiMwMDFmNWEiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iODAiIGZpbGw9IiMwMDFmNWEiIHRleH0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj5RMyBSZXN1bHRzIEluZm9ncmFwaGljPC90ZXh0Pjwvc3ZnPg==', feedback: { overallStatus: "Passed", leadAgentSummary: "The proof has Passed review with no issues found across all categories. It is well-designed, compliant, and ready for deployment.", legalAgentReview: { ragStatus: "Green", feedback: "All claims are substantiated and disclaimers are correctly placed.", issues: [] }, brandAgentReview: { ragStatus: "Green", feedback: "The proof adheres perfectly to all brand guidelines.", issues: [] }, toneAgentReview: { ragStatus: "Green", feedback: "The copy is clear, accurate, and professional.", issues: [] }, channelAgentReview: { ragStatus: "Green", feedback: "The infographic format is highly suitable for the selected channel.", issues: [] }, }, overallStatus: 'Passed', } ] } ] }; const StatusBadge: React.FC<{ status: string }> = ({ status }) => { let colorClasses = ''; switch (status) { case 'In Progress': colorClasses = 'bg-blue-100 text-blue-800'; break; case 'Completed': colorClasses = 'bg-green-100 text-green-800'; break; case 'Needs Review': colorClasses = 'bg-yellow-100 text-yellow-800'; break; default: colorClasses = 'bg-gray-100 text-gray-800'; } return ( {status} ); }; const OverallStatusBadge: React.FC<{ status: OverallStatus }> = ({ status }) => { let colorClasses = ''; switch (status) { case 'Passed': colorClasses = 'bg-green-100 text-green-800'; break; case 'Failed': colorClasses = 'bg-red-100 text-red-800'; break; case 'Requires Manual Legal Review': colorClasses = 'bg-purple-100 text-purple-800'; break; case 'Analysis Error': colorClasses = 'bg-gray-200 text-gray-800'; break; } return ( {status} ); }; const CampaignList: React.FC<{ onSelectCampaign: (name: string) => void; campaigns: typeof initialCampaigns; onOpenModal: () => void; onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void; }> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange }) => { const [showCompleted, setShowCompleted] = useState(true); const filteredCampaigns = showCompleted ? campaigns : campaigns.filter(p => p.status !== 'Completed'); const getStatusSelectClasses = (status: string) => { let baseClasses = 'w-full text-center px-2.5 py-0.5 text-xs font-semibold rounded-full border border-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent appearance-none cursor-pointer'; let colorClasses = ''; switch (status) { case 'In Progress': colorClasses = 'bg-blue-100 text-blue-800'; break; case 'Completed': colorClasses = 'bg-green-100 text-green-800'; break; default: colorClasses = 'bg-gray-100 text-gray-800'; } return `${baseClasses} ${colorClasses}`; }; return (

Campaigns

Manage your campaigns and proof collections.

{filteredCampaigns.map((campaign) => { return ( onSelectCampaign(campaign.name)}> ); })}
Campaign Name Proofs Status Created By Owning Agency Last Modified
{campaign.name} {campaign.proofs}
{campaign.agencyLead} {campaign.agency} {campaign.lastModified}
); }; const UploadProofModal: React.FC<{ isOpen: boolean; onClose: () => void; dropdownOptions: DropdownOptions; onSubmit: (file: File, proofName: string, channel: string, subChannel: string, proofType?: string) => void; isLoading: boolean; existingProofNames: string[]; }> = ({ isOpen, onClose, dropdownOptions, onSubmit, isLoading, existingProofNames }) => { const [file, setFile] = useState(null); const [proofName, setProofName] = useState(''); const [channel, setChannel] = useState(''); const [subChannel, setSubChannel] = useState(''); const [proofType, setProofType] = useState(''); const fileInputRef = useRef(null); const availableChannels = Object.keys(dropdownOptions.channels); const availableSubChannels = channel ? Object.keys(dropdownOptions.channels[channel] || {}) : []; const availableProofTypes = (channel && subChannel) ? (dropdownOptions.channels[channel][subChannel] || []) : []; const showProofType = availableProofTypes.length > 0; useEffect(() => { if (isOpen) { setFile(null); setProofName(''); setChannel(''); setSubChannel(''); setProofType(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }, [isOpen]); // Reset dependents when parent changes useEffect(() => { setSubChannel(''); setProofType(''); }, [channel]); useEffect(() => { if (!showProofType) { setProofType(''); } else { setProofType(''); } }, [subChannel, showProofType]); const handleFileChange = (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; if (selectedFile) { setFile(selectedFile); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!isLoading && e.dataTransfer.files && e.dataTransfer.files.length > 0) { setFile(e.dataTransfer.files[0]); e.dataTransfer.clearData(); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (file && proofName && channel && subChannel) { if (showProofType && !proofType) return; onSubmit(file, proofName, channel, subChannel, showProofType ? proofType : undefined); } }; const isSubmitDisabled = !proofName || !file || !channel || !subChannel || (showProofType && !proofType) || isLoading; const isNewVersion = existingProofNames.includes(proofName.trim()) && proofName.trim() !== ''; if (!isOpen) return null; return (
{/* Header */}

Drag and drop your asset below to start AI analysis.

{/* Content */}
{/* Proof Name */}
setProofName(e.target.value)} placeholder="e.g., Q4 Hero Instagram Post" disabled={isLoading} className="w-full rounded-xl border-gray-300 shadow-sm focus:border-brand-accent focus:ring-brand-accent p-3 pl-10 bg-white transition-all text-gray-900" required />
{isNewVersion && (

A proof with this name already exists. This will be uploaded as a new version.

)}
{/* Dropdowns Grid */}
{/* Drag & Drop Zone */}
{!file ? ( <>
Click to upload or drag and drop

SVG, PNG, JPG or GIF (max. 800x400px)

) : ( <>
{file.name}

Ready for analysis

)}
{/* Footer */}
); }; const LoadingCell: React.FC<{ progress: { completed: number; total: number } }> = ({ progress }) => { const percent = progress.total > 0 ? Math.round((progress.completed / progress.total) * 100) : 0; let statusText = 'Analyzing...'; if (progress.completed < AGENT_NAMES.length) { const agentName = AGENT_NAMES[progress.completed]; statusText = `Analyzing: ${agentName}...`; } else if (progress.completed === AGENT_NAMES.length) { statusText = 'Lead Agent Review...'; } else { statusText = 'Finalizing...'; } return (
{statusText} ({percent}%)
); }; const DeleteConfirmationModal: React.FC<{ isOpen: boolean; onClose: () => void; onConfirm: () => void; proofName: string; }> = ({ isOpen, onClose, onConfirm, proofName }) => { if (!isOpen) return null; return (
e.stopPropagation()} >

Confirm Deletion

Are you sure you want to permanently delete the proof "{proofName}"? This action cannot be undone.

); }; const CampaignDetail: React.FC<{ campaignName: string; onBack: () => void; onSelectProof: (proof: any) => void; campaignProofs: { [key: string]: any[] }; onProofUpload: (file: File, proofName: string, channel: string, subChannel: string, proofType?: string) => void; dropdownOptions: DropdownOptions; onRetryAnalysis: (campaignName: string, tempId: string) => void; onDeleteProof: (campaignName: string, proofName: string) => void; }> = ({ campaignName, onBack, onSelectProof, campaignProofs, onProofUpload, dropdownOptions, onRetryAnalysis, onDeleteProof }) => { const [isUploadFormVisible, setIsUploadFormVisible] = useState(false); const [proofToDelete, setProofToDelete] = useState(null); const [proofForUpload, setProofForUpload] = useState(null); const [isExporting, setIsExporting] = useState(false); const fileInputRef = useRef(null); const proofs = campaignProofs[campaignName] || []; const isUploading = proofs.some(proof => proof.status === 'analyzing'); const existingProofNames = proofs .filter(proof => proof.status === 'completed') .map(proof => proof.proofName); const handleConfirmDelete = () => { if (proofToDelete) { onDeleteProof(campaignName, proofToDelete.proofName); setProofToDelete(null); } }; const handleNewVersionClick = (e: React.MouseEvent, proof: any) => { e.stopPropagation(); setProofForUpload(proof); fileInputRef.current?.click(); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file && proofForUpload) { onProofUpload( file, proofForUpload.proofName, proofForUpload.channel, proofForUpload.subChannel, proofForUpload.proofType ); } setProofForUpload(null); if (event.target) { event.target.value = ''; } }; const handleExportPDF = async (proofsToExport: any[], fileName: string) => { setIsExporting(true); const reportRootEl = document.createElement('div'); reportRootEl.style.position = 'absolute'; reportRootEl.style.left = '-9999px'; reportRootEl.style.top = '0px'; reportRootEl.style.zIndex = '-1'; document.body.appendChild(reportRootEl); const reactRoot = ReactDOM.createRoot(reportRootEl); try { const proofsWithLatestVersion = proofsToExport.map(p => ({ ...p, versions: [p.versions[0]], // Only render the latest version })); reactRoot.render(); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for render and images const { default: jspdf } = await import('jspdf'); const { default: html2canvas } = await import('html2canvas'); const reportContent = reportRootEl.children[0] as HTMLElement; if (!reportContent) throw new Error("PDF report element not found"); const canvas = await html2canvas(reportContent, { scale: 2, useCORS: true }); const imgData = canvas.toDataURL('image/png'); const pdf = new jspdf('p', 'mm', 'a4', true); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const canvasWidth = canvas.width; const canvasHeight = canvas.height; const ratio = canvasWidth / pdfWidth; const pagedCanvasHeight = canvasHeight / ratio; let heightLeft = pagedCanvasHeight; let position = 0; pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST'); heightLeft -= pdfHeight; while (heightLeft > 0) { position -= pdfHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST'); heightLeft -= pdfHeight; } pdf.save(`${fileName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`); } catch (error) { console.error("Failed to generate PDF:", error); alert("Sorry, there was an error creating the PDF. Please try again."); } finally { reactRoot.unmount(); document.body.removeChild(reportRootEl); setIsExporting(false); } }; return (
setProofToDelete(null)} onConfirm={handleConfirmDelete} proofName={proofToDelete?.proofName || ''} />

{campaignName}

Proof overview and compliance status.

setIsUploadFormVisible(false)} dropdownOptions={dropdownOptions} onSubmit={(file, proofName, channel, subChannel, proofType) => { onProofUpload(file, proofName, channel, subChannel, proofType); setIsUploadFormVisible(false); }} isLoading={isUploading} existingProofNames={existingProofNames} />
{proofs.map((proof, index) => { if (proof.status === 'analyzing') { return ( ); } if (proof.status === 'error') { return ( ); } const isVersioned = proof.versions && proof.versions.length > 0; const latestVersion = isVersioned ? proof.versions[0] : null; const isClickable = isVersioned; if (!latestVersion) return null; // Should not happen for completed proofs return ( isClickable && onSelectProof(proof)} > ); })}
Proof Name Workfront # Channel Sub-Channel Proof Type Overall Status Actions
{proof.proofName} Pending {proof.channel} {proof.subChannel} {proof.proofType || 'N/A'} {proof.analysisProgress ? :
Preparing...
}
{proof.proofName} Failed {proof.channel} {proof.subChannel} {proof.proofType || 'N/A'}
Analysis failed.
{proof.proofName} {isVersioned && ( V{latestVersion.version} )} {latestVersion.workfrontId} {proof.channel} {proof.subChannel} {proof.proofType || 'N/A'}
); }; const ProofDetailView: React.FC<{ campaignName: string; proof: any; onBack: () => void; onNewVersionUpload: (file: File) => void; isUploadingNewVersion: boolean; onFlagSubmit: (flagData: Omit) => void; onResolveSubmit: (resolveData: Omit) => void; }> = ({ campaignName, proof, onBack, onNewVersionUpload, isUploadingNewVersion, onFlagSubmit, onResolveSubmit }) => { const getInitialVersionIndex = () => { if (proof.initialVersion && proof.versions) { const index = proof.versions.findIndex((v: any) => v.version === proof.initialVersion); return index > -1 ? index : 0; } return 0; // Default to the latest version (index 0) }; const [selectedVersionIndex, setSelectedVersionIndex] = useState(getInitialVersionIndex); const [isExporting, setIsExporting] = useState(false); const fileInputRef = useRef(null); const handleUploadClick = () => { fileInputRef.current?.click(); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { onNewVersionUpload(file); } }; useEffect(() => { // When proof versions change (e.g., new upload), reset to show the latest version. setSelectedVersionIndex(0); }, [proof.versions]); const versions = proof.versions || []; const selectedVersion = versions[selectedVersionIndex]; const handleFlagSubmitWrapper = (agentName: string, comments: string) => { onFlagSubmit({ campaignName, proofName: proof.proofName, version: selectedVersion.version, agentFlagged: agentName, comments, }); }; const handleResolveSubmitWrapper = (agentName: string, issueText: string, reason: string) => { onResolveSubmit({ campaignName, proofName: proof.proofName, version: selectedVersion.version, agent: agentName, issue: issueText, resolution: reason, }); }; const handleDownload = async () => { const url = selectedVersion.proofPreviewUrl; const fileName = `${proof.proofName}_V${selectedVersion.version}`; try { if (url.startsWith('data:')) { // Data URL - extract mime type and download directly const link = document.createElement('a'); link.href = url; const mimeMatch = url.match(/data:([^;]+)/); const ext = mimeMatch ? mimeMatch[1].split('/')[1] : 'png'; link.download = `${fileName}.${ext}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else { // Remote URL - fetch as blob to handle CORS and ensure download const response = await fetch(url); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; const ext = url.split('.').pop()?.split('?')[0] || 'png'; link.download = `${fileName}.${ext}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(blobUrl); } } catch (error) { console.error('Failed to download file:', error); alert('Sorry, there was an error downloading the file. Please try again.'); } }; const handleDownloadReport = async () => { setIsExporting(true); const reportRootEl = document.createElement('div'); reportRootEl.style.position = 'absolute'; reportRootEl.style.left = '-9999px'; reportRootEl.style.top = '0px'; reportRootEl.style.zIndex = '-1'; document.body.appendChild(reportRootEl); const reactRoot = ReactDOM.createRoot(reportRootEl); try { const proofForReport = { ...proof, versions: [selectedVersion], }; reactRoot.render(); await new Promise(resolve => setTimeout(resolve, 1000)); const { default: jspdf } = await import('jspdf'); const { default: html2canvas } = await import('html2canvas'); const reportContent = reportRootEl.children[0] as HTMLElement; if (!reportContent) throw new Error("PDF report element not found"); const canvas = await html2canvas(reportContent, { scale: 2, useCORS: true }); const imgData = canvas.toDataURL('image/png'); const pdf = new jspdf('p', 'mm', 'a4', true); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const ratio = canvas.width / pdfWidth; const pagedCanvasHeight = canvas.height / ratio; let heightLeft = pagedCanvasHeight; let position = 0; pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST'); heightLeft -= pdfHeight; while (heightLeft > 0) { position -= pdfHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST'); heightLeft -= pdfHeight; } const fileName = `${campaignName} - ${proof.proofName} V${selectedVersion.version} Report`; pdf.save(`${fileName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`); } catch (error) { console.error("Failed to generate PDF:", error); alert("Sorry, there was an error creating the PDF. Please try again."); } finally { reactRoot.unmount(); document.body.removeChild(reportRootEl); setIsExporting(false); } }; if (!selectedVersion) { return (

Error: This proof has no versions to display.

); } return (

{proof.proofName}

{proof.channel} {proof.subChannel} {proof.proofType && ( <> {proof.proofType} )}

Proof Preview

Version History

{versions.map((version: any, index: number) => { const isActive = index === selectedVersionIndex; return ( ); })}
) }; interface CampaignsProps { selectedCampaign: string | null; selectedProof: any | null; onSelectCampaign: (campaignName: string) => void; onSelectProof: (proof: any) => void; onBackToCampaignsList: () => void; onBackToCampaignDetails: () => void; campaigns: typeof initialCampaigns; campaignProofs: typeof initialCampaignProofs; onAddNewCampaign: (campaignData: { name: string; workfrontId: string; clientLead: string; brandGuidelines: string; }) => void; onProofUpload: (campaignName: string, file: File, proofName: string, channel: string, subChannel: string, proofType?: string) => void; dropdownOptions: DropdownOptions; onRetryAnalysis: (campaignName: string, tempId: string) => void; onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void; onDeleteProof: (campaignName: string, proofName: string) => void; onFlagSubmit: (flagData: Omit) => void; onResolveSubmit: (resolveData: Omit) => void; } export const Campaigns: React.FC = ({ selectedCampaign, selectedProof, onSelectCampaign, onSelectProof, onBackToCampaignsList, onBackToCampaignDetails, campaigns, campaignProofs, onAddNewCampaign, onProofUpload, dropdownOptions, onRetryAnalysis, onCampaignStatusChange, onDeleteProof, onFlagSubmit, onResolveSubmit, }) => { const [isModalOpen, setIsModalOpen] = useState(false); if (selectedCampaign && selectedProof) { const handleNewVersionUpload = (file: File) => { onProofUpload( selectedCampaign, file, selectedProof.proofName, selectedProof.channel, selectedProof.subChannel, selectedProof.proofType ); }; const isUploadingNewVersion = campaignProofs[selectedCampaign]?.some( proof => (proof.status === 'analyzing' || proof.status === 'loading') && proof.proofName === selectedProof.proofName ); return ; } if (selectedCampaign) { return onProofUpload(selectedCampaign, file, proofName, channel, subChannel, proofType)} dropdownOptions={dropdownOptions} onRetryAnalysis={onRetryAnalysis} onDeleteProof={onDeleteProof} />; } return ( <> setIsModalOpen(false)} onAddCampaign={onAddNewCampaign} brandGuidelines={dropdownOptions.brandGuidelines} /> setIsModalOpen(true)} onCampaignStatusChange={onCampaignStatusChange} /> ); };