modcomms/frontend/App.tsx
michael 321a9ca820 Implement Microsoft MSAL SSO with PKCE flow
Frontend:
- Add @azure/msal-browser and @azure/msal-react packages
- Create authConfig.ts with MSAL configuration for PKCE flow
- Create authService.ts for token acquisition and user info
- Wrap App with MsalProvider in index.tsx
- Replace dummy login with real MSAL loginPopup() in Login.tsx
- Update App.tsx to use useIsAuthenticated/useMsal hooks
- Update Profile.tsx to display real user data from claims
- Update geminiService.ts to include access_token in WebSocket messages
- Update WIPReviewer.tsx to pass msalInstance for auth

Backend:
- Add python-jose and httpx dependencies for JWT verification
- Create auth_service.py with Azure AD JWKS fetching and token verification
- Create auth.py FastAPI dependency for protected REST endpoints
- Update main.py to verify tokens on WebSocket and protect /info endpoint
- Add AZURE_TENANT_ID, AZURE_CLIENT_ID, DISABLE_AUTH to config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 08:43:30 -06:00

835 lines
33 KiB
TypeScript

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<string, Record<string, string[]>>;
}
const App: React.FC = () => {
// MSAL authentication state
const isAuthenticated = useIsAuthenticated();
const { instance: msalInstance, inProgress } = useMsal();
const [currentView, setCurrentView] = useState<View>('Home');
const [selectedCampaign, setSelectedCampaign] = useState<string | null>(null);
const [selectedProof, setSelectedProof] = useState<any | null>(null);
const [error, setError] = useState<string | null>(null);
const [dropdownOptions, setDropdownOptions] = useState<DropdownOptions>(() => {
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<FlaggedItem[]>(() => {
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<ResolvedItem[]>(() => {
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<ErrorItem[]>(() => {
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<string> => {
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<ErrorItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
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<FlaggedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
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<ResolvedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
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 <Analytics />;
case 'Profile':
return <Profile onLogout={handleLogout} msalInstance={msalInstance} />;
case 'CopyGenAI':
return <CopyGenAI />;
case 'Campaigns':
return <Campaigns
selectedCampaign={selectedCampaign}
selectedProof={selectedProof}
onSelectCampaign={handleSelectCampaign}
onSelectProof={handleSelectProof}
onBackToCampaignsList={handleBackToCampaignsList}
onBackToCampaignDetails={handleBackToCampaignDetails}
campaigns={campaigns}
campaignProofs={campaignProofs}
onAddNewCampaign={handleAddNewCampaign}
onProofUpload={handleProofUploadForCampaign}
dropdownOptions={dropdownOptions}
onRetryAnalysis={handleRetryAnalysis}
onCampaignStatusChange={handleCampaignStatusChange}
onDeleteProof={handleDeleteProof}
onFlagSubmit={handleFlagSubmit}
onResolveSubmit={handleResolveSubmit}
/>;
case 'WIP Reviewer':
return <WIPReviewer dropdownOptions={dropdownOptions} msalInstance={msalInstance} />;
case 'Auditing':
return <Auditing
flaggedItems={flaggedItems}
resolvedItems={resolvedItems}
errorItems={errorItems}
onNavigate={handleNavigateToAuditedItem}
/>;
case 'Settings':
return <Settings
options={dropdownOptions}
onAddCampaign={handleAddCampaignOption}
onRemoveCampaign={handleRemoveCampaignOption}
onAddChannel={handleAddChannel}
onRemoveChannel={handleRemoveChannel}
onAddSubChannel={handleAddSubChannel}
onRemoveSubChannel={handleRemoveSubChannel}
onAddProofType={handleAddProofType}
onRemoveProofType={handleRemoveProofType}
onNavigate={handleNavigate}
/>;
case 'Home':
default:
return (
<>
<Hero onGetStarted={() => handleNavigate('Campaigns')} />
<ChecksOverview />
</>
);
}
};
// Show loading spinner during MSAL authentication interactions
if (inProgress !== InteractionStatus.None) {
return (
<div className="fixed inset-0 bg-[#0f172a] flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<svg className="animate-spin h-12 w-12 text-brand-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-white/80 text-sm">Authenticating...</p>
</div>
</div>
);
}
if (!isAuthenticated) {
return <Login />;
}
// Determine background color based on view to avoid grey bar on Home view
const mainBgColor = currentView === 'Home' ? 'bg-white' : 'bg-brand-gray';
return (
<div className={`flex h-screen ${mainBgColor} font-sans text-gray-800 overflow-hidden`}>
<Sidebar activeItem={currentView} onNavigate={(view) => handleNavigate(view as View)} />
<div className="flex-1 flex flex-col overflow-y-auto">
<main className="flex-1 flex flex-col min-h-full">
{renderContent()}
</main>
</div>
</div>
);
};
export default App;