import React, { useState, useRef, useEffect } from 'react'; import { PlusIcon } from './icons/PlusIcon'; import { ArrowLeftIcon } from './icons/ArrowLeftIcon'; import type { AgentReview, FlaggedItem, ResolvedItem } from '../types'; import { FeedbackReport } from './FeedbackReport'; import { CreateProjectModal } from './CreateProjectModal'; import { CheckCircleIcon, ArrowPathIcon, ExclamationTriangleIcon } from './icons/StatusIcons'; import { AssetPreview } from './AssetPreview'; 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'; export const initialProjects = [ { name: 'Barclays Q4 campaign', workfrontId: '#WF_12822', clientLead: 'Jane Doe', agency: 'OLIVER Agency', agencyLead: 'Steve O\'Donoghue', assets: 1, status: 'In Progress', lastModified: '2024-07-22', }, { name: 'Barclays Q3 Roundup', workfrontId: '#WF_12750', clientLead: 'Jane Doe', agency: 'OLIVER Agency', agencyLead: 'Steve O\'Donoghue', assets: 1, status: 'Completed', lastModified: '2024-06-30', }, ]; 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 analyse 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 analyse this proof with high confidence. This may be because the content is irrelevant, nonsensical, or too far outside of expected marketing materials.", issues: [] }, channelBestPracticesAgentReview: { ragStatus: "Green", feedback: "The content strategy is clear and effectively communicates the partnership message. The call-to-action is prominent.", issues: [] }, channelTechSpecsAgentReview: { ragStatus: "Amber", feedback: "While the image quality is high, the composition is not optimised 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: [] }, channelBestPracticesAgentReview: { ragStatus: "Green", feedback: "The content strategy is effective with clear messaging and well-optimised visual hierarchy. No issues found.", issues: [] }, channelTechSpecsAgentReview: { 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 initialProjectAssets: { [key: string]: any[] } = { 'Barclays Q4 campaign': [ { assetName: 'IG Hero Post 1', channel: 'Social', subChannel: 'Meta', status: 'completed', overallStatus: 'Passed', versions: [ { version: 2, timestamp: '2024-07-25', workfrontId: '#WF_12823-V2', assetPreviewUrl: 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', assetPreviewUrl: IG_HERO_POST_1_IMAGE_V1, feedback: IG_HERO_POST_1_FEEDBACK_V1, overallStatus: 'Analysis Error', } ] } ], 'Barclays Q3 Roundup': [ { assetName: 'Q3 Results Infographic', channel: 'Display', subChannel: 'Banner', status: 'completed', overallStatus: 'Passed', versions: [ { version: 1, timestamp: '2024-06-28', workfrontId: '#WF_12751-V1', assetPreviewUrl: '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: [] }, channelBestPracticesAgentReview: { ragStatus: "Green", feedback: "The content strategy is effective and well-optimised for engagement.", issues: [] }, channelTechSpecsAgentReview: { 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-warning text-oliver-black'; break; case 'Completed': colorClasses = 'bg-success text-white'; break; case 'Needs Review': colorClasses = 'bg-warning-light text-oliver-black'; break; default: colorClasses = 'bg-oliver-grey text-oliver-black'; } return ( {status} ); }; const OverallStatusBadge: React.FC<{ status: 'Passed' | 'Failed' | 'Analysis Error' }> = ({ status }) => { let colorClasses = ''; switch (status) { case 'Passed': colorClasses = 'bg-success text-white'; break; case 'Failed': colorClasses = 'bg-error text-white'; break; case 'Analysis Error': colorClasses = 'bg-grey-300 text-oliver-black'; break; } return ( {status} ); }; const ProjectList: React.FC<{ onSelectProject: (name: string) => void; projects: typeof initialProjects; projectAssets: typeof initialProjectAssets; onOpenModal: () => void; onProjectStatusChange: (projectName: string, newStatus: 'In Progress' | 'Completed') => void; }> = ({ onSelectProject, projects, onOpenModal, onProjectStatusChange }) => { const [showCompleted, setShowCompleted] = useState(true); const filteredProjects = showCompleted ? projects : projects.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-oliver-azure appearance-none cursor-pointer'; let colorClasses = ''; switch (status) { case 'In Progress': colorClasses = 'bg-warning text-oliver-black'; break; case 'Completed': colorClasses = 'bg-success text-white'; break; default: colorClasses = 'bg-oliver-grey text-oliver-black'; } return `${baseClasses} ${colorClasses}`; }; return (

Projects

Manage your campaigns and proof collections.

{filteredProjects.map((project) => { return ( onSelectProject(project.name)}> ); })}
Project Name Proofs Status Created By Owning Agency Last Modified
{project.name} {project.assets}
{project.agencyLead} {project.agency} {project.lastModified}
); }; const ProjectAssetUpload: React.FC<{ dropdownOptions: DropdownOptions; onSubmit: (file: File, assetName: string, channel: string, subChannel: string) => void; onCancel: () => void; isLoading: boolean; existingAssetNames: string[]; }> = ({ dropdownOptions, onSubmit, onCancel, isLoading, existingAssetNames }) => { const [file, setFile] = useState(null); const [assetName, setAssetName] = useState(''); const [channel, setChannel] = useState(''); const [subChannel, setSubChannel] = useState(''); const fileInputRef = useRef(null); const availableChannels = Object.keys(dropdownOptions.channels); const availableSubChannels = channel ? Object.keys(dropdownOptions.channels[channel] || {}) : []; // Reset dependents useEffect(() => { setSubChannel(''); }, [channel]); const handleFileChange = (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; if (selectedFile) { setFile(selectedFile); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (file && assetName && channel && subChannel) { onSubmit(file, assetName, channel, subChannel); setFile(null); setAssetName(''); setChannel(''); setSubChannel(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; const isSubmitDisabled = !assetName || !file || !channel || !subChannel || isLoading; const isNewVersion = existingAssetNames.includes(assetName.trim()) && assetName.trim() !== ''; return (

Upload New Proof

setAssetName(e.target.value)} placeholder="e.g., Q4 Hero Instagram Post" disabled={isLoading} className="w-full bg-white border border-grey-300 rounded-[10px] py-2 px-3 text-oliver-black focus:outline-none focus:ring-2 focus:ring-oliver-azure disabled:bg-oliver-grey" required /> {isNewVersion && (

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

)}
{/* Channel Dropdown */}
{/* Sub-Channel Dropdown */}

or drag and drop

{file ?

{file.name}

:

PNG, JPG, GIF, WEBP, MP4, PDF

}
); }; 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 = 'Analysing...'; if (progress.completed < AGENT_NAMES.length) { const agentName = AGENT_NAMES[progress.completed]; statusText = `Analysing: ${agentName}...`; } else if (progress.completed === AGENT_NAMES.length) { statusText = 'Lead Agent Review...'; } else { statusText = 'Finalizing...'; } return (
{statusText} ({percent}%)
); }; const AnalysisErrorModal: React.FC<{ isOpen: boolean; onClose: () => void; assetName: string; feedback: AgentReview | null; }> = ({ isOpen, onClose, assetName, feedback }) => { if (!isOpen || !feedback) return null; const agentEntries: { label: string; review: { ragStatus: string; feedback: string } }[] = [ { label: 'Risk & Control Agent', review: feedback.legalAgentReview }, { label: 'Brand Agent', review: feedback.brandAgentReview }, { label: 'Channel Best Practices Agent', review: feedback.channelBestPracticesAgentReview }, { label: 'Channel Tech Specs Agent', review: feedback.channelTechSpecsAgentReview }, ]; const failedAgents = agentEntries.filter(a => a.review.ragStatus === 'Error'); return (
e.stopPropagation()} >

Analysis Error

{assetName}

{feedback.leadAgentSummary && (

Summary

{feedback.leadAgentSummary}

)} {failedAgents.length > 0 && (

Agent Details

{failedAgents.map(agent => (

{agent.label}

{agent.review.feedback}

))}
)}
); }; const DeleteConfirmationModal: React.FC<{ isOpen: boolean; onClose: () => void; onConfirm: () => void; assetName: string; }> = ({ isOpen, onClose, onConfirm, assetName }) => { if (!isOpen) return null; return (
e.stopPropagation()} >

Confirm Deletion

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

); }; const ProjectDetail: React.FC<{ projectName: string; onBack: () => void; onSelectAsset: (asset: any) => void; projectAssets: { [key: string]: any[] }; onAssetUpload: (file: File, assetName: string, channel: string, subChannel: string) => void; dropdownOptions: DropdownOptions; onRetryAnalysis: (projectName: string, tempId: string) => void; onDeleteAsset: (projectName: string, assetName: string) => void; }> = ({ projectName, onBack, onSelectAsset, projectAssets, onAssetUpload, dropdownOptions, onRetryAnalysis, onDeleteAsset }) => { const [isUploadFormVisible, setIsUploadFormVisible] = useState(false); const [assetToDelete, setAssetToDelete] = useState(null); const [assetForUpload, setAssetForUpload] = useState(null); const [errorAsset, setErrorAsset] = useState(null); const fileInputRef = useRef(null); const assets = projectAssets[projectName] || []; const isUploading = assets.some(asset => asset.status === 'analyzing'); const existingAssetNames = assets .filter(asset => asset.status === 'completed') .map(asset => asset.assetName); const handleConfirmDelete = () => { if (assetToDelete) { onDeleteAsset(projectName, assetToDelete.assetName); setAssetToDelete(null); } }; const handleNewVersionClick = (e: React.MouseEvent, asset: any) => { e.stopPropagation(); setAssetForUpload(asset); fileInputRef.current?.click(); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file && assetForUpload) { onAssetUpload( file, assetForUpload.assetName, assetForUpload.channel, assetForUpload.subChannel ); } setAssetForUpload(null); if (event.target) { event.target.value = ''; } }; return (
setAssetToDelete(null)} onConfirm={handleConfirmDelete} assetName={assetToDelete?.assetName || ''} /> setErrorAsset(null)} assetName={errorAsset?.assetName || ''} feedback={errorAsset?.versions?.[0]?.feedback || null} />

{projectName}

Proof overview and compliance status.

{isUploadFormVisible ? ( { onAssetUpload(file, assetName, channel, subChannel); setIsUploadFormVisible(false); }} onCancel={() => setIsUploadFormVisible(false)} isLoading={isUploading} existingAssetNames={existingAssetNames} /> ) : null}
{!isUploadFormVisible && (
)}
{assets.map((asset, index) => { if (asset.status === 'analyzing') { return ( ); } if (asset.status === 'error') { return ( ); } const isVersioned = asset.versions && asset.versions.length > 0; const latestVersion = isVersioned ? asset.versions[0] : null; const isClickable = isVersioned; if (!latestVersion) return null; // Should not happen for completed assets return ( isClickable && onSelectAsset(asset)} > ); })}
Proof Name Workfront # Channel Sub-Channel Overall Status Actions
{asset.assetName} Pending {asset.channel} {asset.subChannel} {asset.analysisProgress ? :
Preparing...
}
{asset.assetName} Failed {asset.channel} {asset.subChannel}
{asset.assetName} {isVersioned && ( V{latestVersion.version} )} {latestVersion.workfrontId} {asset.channel} {asset.subChannel}
); }; const AssetDetailView: React.FC<{ projectName: string; asset: any; onBack: () => void; onNewVersionUpload: (file: File) => void; isUploadingNewVersion: boolean; onFlagSubmit: (flagData: Omit) => void; onResolveSubmit: (resolveData: Omit) => void; }> = ({ projectName, asset, onBack, onNewVersionUpload, isUploadingNewVersion, onFlagSubmit, onResolveSubmit }) => { const getInitialVersionIndex = () => { if (asset.initialVersion && asset.versions) { const index = asset.versions.findIndex((v: any) => v.version === asset.initialVersion); return index > -1 ? index : 0; } return 0; // Default to the latest version (index 0) }; const [selectedVersionIndex, setSelectedVersionIndex] = useState(getInitialVersionIndex); 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 asset versions change (e.g., new upload), reset to show the latest version. setSelectedVersionIndex(0); }, [asset.versions]); const versions = asset.versions || []; const selectedVersion = versions[selectedVersionIndex]; const handleFlagSubmitWrapper = (agentName: string, comments: string) => { onFlagSubmit({ projectName, assetName: asset.assetName, version: selectedVersion.version, agentFlagged: agentName, comments, }); }; const handleResolveSubmitWrapper = (agentName: string, issueText: string, reason: string) => { onResolveSubmit({ projectName, assetName: asset.assetName, version: selectedVersion.version, agent: agentName, issue: issueText, resolution: reason, }); }; if (!selectedVersion) { return (

Error: This proof has no versions to display.

); } return (

{asset.assetName}

Detailed AI feedback report.

Proof Preview

Version History

{versions.map((version: any, index: number) => { const isActive = index === selectedVersionIndex; return ( ); })}
) }; interface ProjectsProps { selectedProject: string | null; selectedAsset: any | null; onSelectProject: (projectName: string) => void; onSelectAsset: (asset: any) => void; onBackToProjectsList: () => void; onBackToProjectDetails: () => void; projects: typeof initialProjects; projectAssets: typeof initialProjectAssets; onAddNewProject: (projectData: { name: string; workfrontId: string; clientLead: string; }) => void; onAssetUpload: (projectName: string, file: File, assetName: string, channel: string, subChannel: string) => void; dropdownOptions: DropdownOptions; onRetryAnalysis: (projectName: string, tempId: string) => void; onProjectStatusChange: (projectName: string, newStatus: 'In Progress' | 'Completed') => void; onDeleteAsset: (projectName: string, assetName: string) => void; onFlagSubmit: (flagData: Omit) => void; onResolveSubmit: (resolveData: Omit) => void; } export const Projects: React.FC = ({ selectedProject, selectedAsset, onSelectProject, onSelectAsset, onBackToProjectsList, onBackToProjectDetails, projects, projectAssets, onAddNewProject, onAssetUpload, dropdownOptions, onRetryAnalysis, onProjectStatusChange, onDeleteAsset, onFlagSubmit, onResolveSubmit, }) => { const [isModalOpen, setIsModalOpen] = useState(false); if (selectedProject && selectedAsset) { const handleNewVersionUpload = (file: File) => { onAssetUpload( selectedProject, file, selectedAsset.assetName, selectedAsset.channel, selectedAsset.subChannel ); }; const isUploadingNewVersion = projectAssets[selectedProject]?.some( asset => (asset.status === 'analyzing' || asset.status === 'loading') && asset.assetName === selectedAsset.assetName ); return ; } if (selectedProject) { return onAssetUpload(selectedProject, file, assetName, channel, subChannel)} dropdownOptions={dropdownOptions} onRetryAnalysis={onRetryAnalysis} onDeleteAsset={onDeleteAsset} />; } return ( <> setIsModalOpen(false)} onAddProject={onAddNewProject} /> setIsModalOpen(true)} onProjectStatusChange={onProjectStatusChange} /> ); };