import React, { useState, useEffect } from 'react'; import { useIsAuthenticated, useMsal } from '@azure/msal-react'; import { InteractionStatus } from '@azure/msal-browser'; import { Hero } from './components/Hero'; import { analyzeProof } from './services/geminiService'; import type { AgentReview, AgentName, FlaggedItem, ResolvedItem, ErrorItem } from './types'; import { AGENT_NAMES } from './constants'; import { Sidebar } from './components/Sidebar'; import { ChecksOverview } from './components/ChecksOverview'; import { Analytics } from './components/Analytics'; import { Profile } from './components/Profile'; import { CopyGenAI } from './components/CopyGenAI'; import { Settings } from './components/Settings'; import { Campaigns, initialCampaigns, initialCampaignProofs } from './components/Campaigns'; import { Auditing } from './components/Auditing'; import { Login } from './components/Login'; import { WIPReviewer } from './components/WIPReviewer'; type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing'; export interface DropdownOptions { campaigns: string[]; // Hierarchy: Channel -> SubChannel -> ProofType[] channels: Record>; } const App: React.FC = () => { // MSAL authentication state const isAuthenticated = useIsAuthenticated(); const { instance: msalInstance, inProgress } = useMsal(); const [currentView, setCurrentView] = useState('Home'); const [selectedCampaign, setSelectedCampaign] = useState(null); const [selectedProof, setSelectedProof] = useState(null); const [error, setError] = useState(null); const [dropdownOptions, setDropdownOptions] = useState(() => { try { const storedOptions = localStorage.getItem('barclays_modcomms_dropdown_options_v3'); if (storedOptions) { return JSON.parse(storedOptions); } } catch (error) { console.error('Error reading dropdown options from localStorage', error); } return { campaigns: ["Barclays Q4 Social"], channels: { "Social": { "Meta": ["In-feed 1x1", "In-feed 4x5", "Reels static 9x16", "Stories Static 9x16", "In-feed 1x1 video", "9x16 reels", "9x16 stories"] }, "YouTube - (online video)": { "Video": ["1x1", "16x9"] }, "Google - Performance Max": { "Video": ["1x1", "9x16", "16x9"], "Static image": ["1080x1080", "4x5 (1080x1350)", "1.91x1 (1200x628)", "Logo 1x1 (1080x1080)"] }, "Display": { "Banner": ["300x600", "160x600", "970x250", "300x250"] }, ".co.uk Banner": { "Web Banner Design + Static": ["720x540", "1316x740"] }, "Ad Copy": { "Copy Document": ["Text"] } } }; }); useEffect(() => { try { localStorage.setItem('barclays_modcomms_dropdown_options_v3', JSON.stringify(dropdownOptions)); } catch (error) { console.error('Error saving dropdown options to localStorage', error); } }, [dropdownOptions]); const [campaigns, setCampaigns] = useState(() => { try { const storedCampaigns = localStorage.getItem('barclays_modcomms_campaigns_v3'); return storedCampaigns ? JSON.parse(storedCampaigns) : initialCampaigns; } catch (error) { console.error('Error reading campaigns from localStorage', error); return initialCampaigns; } }); const [campaignProofs, setCampaignProofs] = useState(() => { try { const storedProofs = localStorage.getItem('barclays_modcomms_campaign_proofs_v3'); return storedProofs ? JSON.parse(storedProofs) : initialCampaignProofs; } catch (error) { console.error('Error reading campaign proofs from localStorage', error); return initialCampaignProofs; } }); const [flaggedItems, setFlaggedItems] = useState(() => { try { const storedFlags = localStorage.getItem('barclays_modcomms_flagged_items_v3'); return storedFlags ? JSON.parse(storedFlags) : []; } catch (error) { console.error('Error reading flagged items from localStorage', error); return []; } }); const [resolvedItems, setResolvedItems] = useState(() => { try { const storedResolutions = localStorage.getItem('barclays_modcomms_resolved_items_v3'); return storedResolutions ? JSON.parse(storedResolutions) : []; } catch (error) { console.error('Error reading resolved items from localStorage', error); return []; } }); const [errorItems, setErrorItems] = useState(() => { try { const storedErrors = localStorage.getItem('barclays_modcomms_error_items_v3'); return storedErrors ? JSON.parse(storedErrors) : []; } catch (error) { console.error('Error reading error items from localStorage', error); return []; } }); useEffect(() => { try { if (!localStorage.getItem('barclays_modcomms_campaigns_v3')) { localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(initialCampaigns)); } if (!localStorage.getItem('barclays_modcomms_campaign_proofs_v3')) { localStorage.setItem('barclays_modcomms_campaign_proofs_v3', JSON.stringify(initialCampaignProofs)); } } catch (error) { console.error('Error saving initial data to localStorage', error); } }, []); useEffect(() => { // Keep selectedProof in sync with the master list in campaignProofs. // This ensures that when a new version is added, the detail view refreshes. if (selectedCampaign && selectedProof && campaignProofs[selectedCampaign]) { const currentCampaignProofs = campaignProofs[selectedCampaign]; const freshProof = currentCampaignProofs.find(a => !a.tempId && a.proofName === selectedProof.proofName); // A simple stringify check to prevent re-renders if the object is the same. if (freshProof && JSON.stringify(freshProof) !== JSON.stringify(selectedProof)) { setSelectedProof(freshProof); } } }, [campaignProofs, selectedCampaign, selectedProof]); // New function to handle saving campaign proofs to avoid QuotaExceededError const saveCampaignProofs = (proofs: any) => { try { // Deep clone to avoid mutating the state object used by React const proofsToSave = JSON.parse(JSON.stringify(proofs)); // Iterate over all campaigns and their proofs for (const campaignName in proofsToSave) { if (Object.prototype.hasOwnProperty.call(proofsToSave, campaignName)) { proofsToSave[campaignName].forEach((proof: any) => { // For temporary placeholder proofs, remove the file object and progress // as it cannot be stringified and is not needed for persistence. if (proof.tempId) { if ('file' in proof) delete proof.file; if ('analysisProgress' in proof) delete proof.analysisProgress; } // For versioned proofs, remove large non-SVG preview URLs if (proof.versions) { proof.versions.forEach((version: any) => { if (version.proofPreviewUrl && !version.proofPreviewUrl.startsWith('data:image/svg+xml')) { delete version.proofPreviewUrl; } }); } }); } } localStorage.setItem('barclays_modcomms_campaign_proofs_v3', JSON.stringify(proofsToSave)); } catch (error) { if ((error as DOMException).name === 'QuotaExceededError') { console.error('LocalStorage quota exceeded. Could not save campaign proofs. The app might not persist data correctly across sessions until storage is cleared.', error); setError('Could not save campaign changes, storage is full.'); } else { console.error('Error saving campaign proofs to localStorage', error); } } }; const handleAddNewCampaign = (campaignData: { name: string; workfrontId: string; clientLead: string; brandGuidelines: string; }) => { const newCampaign = { ...campaignData, agency: "OLIVER Agency", agencyLead: "Steve O'Donoghue", proofs: 0, status: 'In Progress', lastModified: new Date().toISOString().split('T')[0], }; const updatedCampaigns = [...campaigns, newCampaign]; const updatedCampaignProofs = { ...campaignProofs, [newCampaign.name]: [] }; setCampaigns(updatedCampaigns); setCampaignProofs(updatedCampaignProofs); try { localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns)); saveCampaignProofs(updatedCampaignProofs); } catch (error) { console.error('Error saving new campaign to localStorage', error); } }; const fileToDataUrl = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); }); }; const handleAddNewError = (errorData: Omit) => { const newError: ErrorItem = { ...errorData, id: `err_${Date.now()}`, timestamp: new Date().toISOString(), submitter: "Steve O'Donoghue", // Hardcoded for prototype submitAgency: "OLIVER Agency", // Hardcoded for prototype }; setErrorItems(prevItems => { const updatedErrors = [newError, ...prevItems]; try { localStorage.setItem('barclays_modcomms_error_items_v3', JSON.stringify(updatedErrors)); } catch (error) { console.error('Error saving error items to localStorage', error); setError('Could not save the analysis error log due to storage limitations.'); } return updatedErrors; }); }; const handleProofUploadForCampaign = async ( campaignName: string, file: File, proofName: string, channel: string, subChannel: string, proofType?: string ) => { setError(null); const tempId = `temp_${Date.now()}`; const newProofPlaceholder = { tempId, proofName, channel, subChannel, proofType, status: 'analyzing', analysisProgress: { completed: 0, total: AGENT_NAMES.length + 1 }, file, versions: [], }; setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: [newProofPlaceholder, ...(prevProofs[campaignName] || [])] })); const handleAgentUpdate = (agentName: AgentName | 'Summary') => { setCampaignProofs(prevProofs => { const currentProofs = prevProofs[campaignName] || []; const updatedProofs = currentProofs.map(proof => { if (proof.tempId === tempId && proof.status === 'analyzing') { const newCompleted = (proof.analysisProgress?.completed ?? 0) + 1; return { ...proof, analysisProgress: { ...proof.analysisProgress, completed: newCompleted } }; } return proof; }); return { ...prevProofs, [campaignName]: updatedProofs }; }); }; try { const feedback = await analyzeProof(file, handleAgentUpdate, msalInstance); const previewUrl = await fileToDataUrl(file); if (feedback.overallStatus === 'Analysis Error') { const currentCampaignProofsList = campaignProofs[campaignName] || []; const existingProof = currentCampaignProofsList.find(a => a.proofName === proofName && !a.tempId); const version = existingProof ? (existingProof.versions[0]?.version || 0) + 1 : 1; handleAddNewError({ campaignName, proofName, version, errorSummary: feedback.leadAgentSummary, }); } setCampaignProofs(prevCampaignProofs => { const currentCampaignProofsList = prevCampaignProofs[campaignName] || []; const existingProof = currentCampaignProofsList.find(a => a.proofName === proofName && !a.tempId); if (existingProof) { // UPDATE PROOF (NEW VERSION) const latestVersionNumber = existingProof.versions[0]?.version || 0; const newVersionNumber = latestVersionNumber + 1; const baseWorkfrontId = existingProof.versions.length > 0 ? existingProof.versions[existingProof.versions.length - 1].workfrontId.split('-V')[0] : `#WF_${Math.floor(10000 + Math.random() * 90000)}`; const newVersion = { version: newVersionNumber, timestamp: new Date().toISOString().split('T')[0], workfrontId: `${baseWorkfrontId}-V${newVersionNumber}`, proofPreviewUrl: previewUrl, feedback: feedback, overallStatus: feedback.overallStatus, }; const updatedProof = { ...existingProof, overallStatus: feedback.overallStatus, versions: [newVersion, ...existingProof.versions] }; const updatedProofsList = currentCampaignProofsList .filter(proof => proof.tempId !== tempId) .map(proof => proof.proofName === proofName ? updatedProof : proof); const finalCampaignProofs = { ...prevCampaignProofs, [campaignName]: updatedProofsList }; setCampaigns(prevCampaigns => { const updatedCampaigns = prevCampaigns.map(p => p.name === campaignName ? { ...p, lastModified: new Date().toISOString().split('T')[0] } : p ); localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns)); return updatedCampaigns; }); saveCampaignProofs(finalCampaignProofs); return finalCampaignProofs; } else { // CREATE NEW PROOF const newWorkfrontId = `#WF_${Math.floor(10000 + Math.random() * 90000)}`; const newProofWithVersion = { proofName, channel, subChannel, proofType, status: 'completed', overallStatus: feedback.overallStatus, versions: [ { version: 1, timestamp: new Date().toISOString().split('T')[0], workfrontId: `${newWorkfrontId}-V1`, proofPreviewUrl: previewUrl, feedback: feedback, overallStatus: feedback.overallStatus, } ] }; const updatedProofsList = currentCampaignProofsList.map(proof => proof.tempId === tempId ? newProofWithVersion : proof ); const finalCampaignProofs = { ...prevCampaignProofs, [campaignName]: updatedProofsList }; setCampaigns(prevCampaigns => { const updatedCampaigns = prevCampaigns.map(p => p.name === campaignName ? { ...p, proofs: p.proofs + 1, lastModified: new Date().toISOString().split('T')[0] } : p ); localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns)); return updatedCampaigns; }); saveCampaignProofs(finalCampaignProofs); return finalCampaignProofs; } }); } catch (err) { console.error("Failed to upload and analyze proof:", err); setError("Failed to upload and analyze proof. Please try again."); setCampaignProofs(prevProofs => { const updatedProofs = { ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => proof.tempId === tempId ? { ...proof, status: 'error' } : proof ) }; saveCampaignProofs(updatedProofs); return updatedProofs; }); } }; const handleRetryAnalysis = async (campaignName: string, tempId: string) => { const proofToRetry = campaignProofs[campaignName]?.find(proof => proof.tempId === tempId); if (!proofToRetry || !proofToRetry.file) { console.error("Proof to retry not found or file is missing"); return; } const { file, proofName, channel, subChannel, proofType } = proofToRetry; setCampaignProofs(prevProofs => ({ ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => proof.tempId === tempId ? { ...proof, status: 'analyzing', analysisProgress: { completed: 0, total: AGENT_NAMES.length + 1 } } : proof ) })); const handleAgentUpdateForRetry = (agentName: AgentName | 'Summary') => { setCampaignProofs(prevProofs => { const currentProofs = prevProofs[campaignName] || []; const updatedProofs = currentProofs.map(proof => { if (proof.tempId === tempId && proof.status === 'analyzing') { const newCompleted = (proof.analysisProgress?.completed ?? 0) + 1; return { ...proof, analysisProgress: { ...proof.analysisProgress, completed: newCompleted } }; } return proof; }); return { ...prevProofs, [campaignName]: updatedProofs }; }); }; try { const feedback = await analyzeProof(file, handleAgentUpdateForRetry, msalInstance); const previewUrl = await fileToDataUrl(file); const newWorkfrontId = `#WF_${Math.floor(10000 + Math.random() * 90000)}`; if (feedback.overallStatus === 'Analysis Error') { handleAddNewError({ campaignName: campaignName, proofName: proofName, version: 1, // Retry always creates a V1 errorSummary: feedback.leadAgentSummary, }); } const newProofWithVersion = { proofName, channel, subChannel, proofType, status: 'completed', overallStatus: feedback.overallStatus, versions: [ { version: 1, timestamp: new Date().toISOString().split('T')[0], workfrontId: `${newWorkfrontId}-V1`, proofPreviewUrl: previewUrl, feedback: feedback, overallStatus: feedback.overallStatus, } ] }; const updatedCampaigns = campaigns.map(p => p.name === campaignName ? { ...p, proofs: p.proofs + 1, lastModified: new Date().toISOString().split('T')[0] } : p ); setCampaigns(updatedCampaigns); localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns)); setCampaignProofs(prevProofs => { const updatedProofs = { ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => proof.tempId === tempId ? newProofWithVersion : proof ) }; saveCampaignProofs(updatedProofs); return updatedProofs; }); } catch (err) { console.error("Failed to retry proof analysis:", err); setCampaignProofs(prevProofs => { const updatedProofs = { ...prevProofs, [campaignName]: prevProofs[campaignName].map(proof => proof.tempId === tempId ? { ...proof, status: 'error' } : proof ) }; saveCampaignProofs(updatedProofs); return updatedProofs; }); } }; const handleCampaignStatusChange = (campaignName: string, newStatus: 'In Progress' | 'Completed') => { const updatedCampaigns = campaigns.map(p => p.name === campaignName ? { ...p, status: newStatus, lastModified: new Date().toISOString().split('T')[0] } : p ); setCampaigns(updatedCampaigns); try { localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns)); } catch (error) { console.error('Error saving campaign status to localStorage', error); } }; const handleDeleteProof = (campaignName: string, proofName: string) => { // Update campaign proofs setCampaignProofs(prevProofs => { const updatedProofsForCampaign = prevProofs[campaignName].filter( proof => proof.proofName !== proofName ); const finalProofs = { ...prevProofs, [campaignName]: updatedProofsForCampaign }; saveCampaignProofs(finalProofs); return finalProofs; }); // Update campaigns list (proof count) setCampaigns(prevCampaigns => { const updatedCampaigns = prevCampaigns.map(p => p.name === campaignName ? { ...p, proofs: p.proofs > 0 ? p.proofs - 1 : 0, lastModified: new Date().toISOString().split('T')[0] } : p ); localStorage.setItem('barclays_modcomms_campaigns_v3', JSON.stringify(updatedCampaigns)); return updatedCampaigns; }); }; // --- SETTINGS HANDLERS (UPDATED) --- const handleAddCampaignOption = (value: string) => { setDropdownOptions(prev => { if (!prev.campaigns.includes(value)) { return { ...prev, campaigns: [...prev.campaigns, value].sort() }; } return prev; }); }; const handleRemoveCampaignOption = (value: string) => { setDropdownOptions(prev => ({ ...prev, campaigns: prev.campaigns.filter(c => c !== value) })); }; const handleAddChannel = (channel: string) => { setDropdownOptions(prev => { if (prev.channels[channel]) return prev; return { ...prev, channels: { ...prev.channels, [channel]: {} } }; }); }; const handleRemoveChannel = (channel: string) => { setDropdownOptions(prev => { const newChannels = { ...prev.channels }; delete newChannels[channel]; return { ...prev, channels: newChannels }; }); }; const handleAddSubChannel = (channel: string, subChannel: string) => { setDropdownOptions(prev => { const channelData = prev.channels[channel] || {}; if (channelData[subChannel]) return prev; return { ...prev, channels: { ...prev.channels, [channel]: { ...channelData, [subChannel]: [] } } }; }); }; const handleRemoveSubChannel = (channel: string, subChannel: string) => { setDropdownOptions(prev => { const channelData = { ...prev.channels[channel] }; delete channelData[subChannel]; return { ...prev, channels: { ...prev.channels, [channel]: channelData } }; }); }; const handleAddProofType = (channel: string, subChannel: string, proofType: string) => { setDropdownOptions(prev => { const channelData = prev.channels[channel]; if (!channelData) return prev; const currentTypes = channelData[subChannel] || []; if (currentTypes.includes(proofType)) return prev; return { ...prev, channels: { ...prev.channels, [channel]: { ...channelData, [subChannel]: [...currentTypes, proofType].sort() } } }; }); }; const handleRemoveProofType = (channel: string, subChannel: string, proofType: string) => { setDropdownOptions(prev => { const channelData = prev.channels[channel]; if (!channelData) return prev; const currentTypes = channelData[subChannel] || []; return { ...prev, channels: { ...prev.channels, [channel]: { ...channelData, [subChannel]: currentTypes.filter(t => t !== proofType) } } }; }); }; const handleNavigate = (view: View) => { setCurrentView(view); setSelectedCampaign(null); // Reset campaign on any main navigation setSelectedProof(null); }; const handleSelectCampaign = (campaignName: string) => { setSelectedCampaign(campaignName); }; const handleSelectProof = (proof: any) => { setSelectedProof(proof); }; const handleBackToCampaignsList = () => { setSelectedCampaign(null); setSelectedProof(null); }; const handleBackToCampaignDetails = () => { setSelectedProof(null); }; const handleFlagSubmit = (flagData: Omit) => { const newFlag: FlaggedItem = { ...flagData, id: `flag_${Date.now()}`, timestamp: new Date().toISOString(), submitter: "Steve O'Donoghue", // Hardcoded for prototype submitAgency: "OLIVER Agency", // Hardcoded for prototype }; const updatedFlags = [newFlag, ...flaggedItems]; setFlaggedItems(updatedFlags); try { localStorage.setItem('barclays_modcomms_flagged_items_v3', JSON.stringify(updatedFlags)); } catch (error) { console.error('Error saving flagged items to localStorage', error); setError('Could not save your feedback due to storage limitations.'); } }; const handleResolveSubmit = (resolveData: Omit) => { const newResolution: ResolvedItem = { ...resolveData, id: `res_${Date.now()}`, timestamp: new Date().toISOString(), submitter: "Steve O'Donoghue", // Hardcoded for prototype submitAgency: "OLIVER Agency", // Hardcoded for prototype }; const updatedResolutions = [newResolution, ...resolvedItems]; setResolvedItems(updatedResolutions); try { localStorage.setItem('barclays_modcomms_resolved_items_v3', JSON.stringify(updatedResolutions)); } catch (error) { console.error('Error saving resolved items to localStorage', error); setError('Could not save your resolution due to storage limitations.'); } }; const handleNavigateToAuditedItem = (item: { campaignName: string; proofName: string; version: number }) => { const proofToSelect = campaignProofs[item.campaignName]?.find(a => a.proofName === item.proofName); if (proofToSelect) { const versionExists = proofToSelect.versions.some((v: any) => v.version === item.version); if (versionExists) { setSelectedCampaign(item.campaignName); // Add a temporary property to the proof object to indicate which version to show. setSelectedProof({ ...proofToSelect, initialVersion: item.version }); setCurrentView('Campaigns'); } else { setError(`Version ${item.version} not found for proof ${item.proofName}. It may have been deleted.`); } } else { setError(`Proof ${item.proofName} not found in campaign ${item.campaignName}. It may have been deleted.`); } }; const handleLogout = async () => { try { await msalInstance.logoutPopup({ postLogoutRedirectUri: window.location.origin, }); } catch (error) { console.error('Logout failed:', error); } }; const renderContent = () => { switch (currentView) { case 'Analytics': return ; case 'Profile': return ; case 'CopyGenAI': return ; case 'Campaigns': return ; case 'WIP Reviewer': return ; case 'Auditing': return ; case 'Settings': return ; case 'Home': default: return ( <> handleNavigate('Campaigns')} /> ); } }; // Show loading spinner during MSAL authentication interactions if (inProgress !== InteractionStatus.None) { return (

Authenticating...

); } if (!isAuthenticated) { return ; } // Determine background color based on view to avoid grey bar on Home view const mainBgColor = currentView === 'Home' ? 'bg-white' : 'bg-brand-gray'; return (
handleNavigate(view as View)} />
{renderContent()}
); }; export default App;