modcomms/frontend/App.tsx
michael b119951f93 Fix retry button for failed proofs and hook up download asset button
- Add GET /files/{storage_key:path} endpoint to serve stored files
- Add getFile() method to apiService to fetch files from backend
- Update convertProofToFrontend() to preserve fileStorageKey
- Update handleRetryAnalysis() to fetch file from backend when not in memory
- Update handleDownload() to download original file instead of thumbnail

After page refresh, the retry button now fetches the original file from
backend storage using the fileStorageKey, allowing failed proofs to be
reprocessed. The Download Asset button also now downloads the original
uploaded file rather than the preview thumbnail.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 07:01:58 -06:00

808 lines
31 KiB
TypeScript
Executable file

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 { getUserInfo } from './services/authService';
import apiService from './services/apiService';
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 } 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[]>>;
// Brand guidelines for campaign creation
brandGuidelines: 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 [isLoadingData, setIsLoadingData] = useState(true);
// Initialize API service with MSAL instance for authenticated requests
useEffect(() => {
if (msalInstance) {
apiService.setMsalInstance(msalInstance);
}
}, [msalInstance]);
// Dropdown options now loaded from API
const [dropdownOptions, setDropdownOptions] = useState<DropdownOptions>({
campaigns: [],
channels: {},
brandGuidelines: []
});
// Load dropdown options from API when authenticated
useEffect(() => {
const loadDropdownOptions = async () => {
if (!isAuthenticated) return;
try {
const options = await apiService.getDropdownOptions();
console.log('[DEBUG App.tsx] Loaded dropdown options from API');
console.log('[DEBUG App.tsx] options.channels:', JSON.stringify(options.channels, null, 2));
console.log('[DEBUG App.tsx] Social.Meta proof types:', options.channels?.Social?.Meta);
setDropdownOptions({
campaigns: options.campaigns || [],
channels: options.channels || {},
brandGuidelines: options.brand_guidelines || []
});
} catch (error) {
console.error('[DEBUG App.tsx] Failed to load dropdown options:', error);
// Fall back to default options if API fails
setDropdownOptions({
campaigns: [],
channels: {
"Social": { "Meta": ["In-feed 1x1", "In-feed 4x5"] },
"Display": { "Banner": ["300x600", "300x250"] }
},
brandGuidelines: ["Barclays", "Barclaycard"]
});
}
};
loadDropdownOptions();
}, [isAuthenticated]);
// Campaigns and proofs now loaded from API instead of localStorage
const [campaigns, setCampaigns] = useState<any[]>([]);
const [campaignProofs, setCampaignProofs] = useState<Record<string, any[]>>({});
// Load campaigns from API when authenticated
useEffect(() => {
const loadCampaigns = async () => {
if (!isAuthenticated) return;
setIsLoadingData(true);
try {
const response = await apiService.getCampaigns();
setCampaigns(response.map(c => apiService.convertCampaignToFrontend(c)));
} catch (error) {
console.error('Failed to load campaigns:', error);
setError('Failed to load campaigns. Please try again.');
} finally {
setIsLoadingData(false);
}
};
loadCampaigns();
}, [isAuthenticated]);
// Audit items now loaded from API instead of localStorage
const [flaggedItems, setFlaggedItems] = useState<FlaggedItem[]>([]);
const [resolvedItems, setResolvedItems] = useState<ResolvedItem[]>([]);
const [errorItems, setErrorItems] = useState<ErrorItem[]>([]);
// Load audit items from API when authenticated
useEffect(() => {
const loadAuditItems = async () => {
if (!isAuthenticated) return;
try {
const [flagged, resolved, errors] = await Promise.all([
apiService.getFlaggedItems(),
apiService.getResolvedItems(),
apiService.getErrorItems(),
]);
setFlaggedItems(flagged.map(i => apiService.convertFlaggedItemToFrontend(i)));
setResolvedItems(resolved.map(i => apiService.convertResolvedItemToFrontend(i)));
setErrorItems(errors.map(i => apiService.convertErrorItemToFrontend(i)));
} catch (error) {
console.error('Failed to load audit items:', error);
}
};
loadAuditItems();
}, [isAuthenticated]);
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]);
const handleAddNewCampaign = async (campaignData: { name: string; workfrontId: string; clientLead: string; agencyLead: string; brandGuidelines: string; }) => {
try {
const response = await apiService.createCampaign({
name: campaignData.name,
workfront_id: campaignData.workfrontId,
client_lead: campaignData.clientLead,
agency_lead: campaignData.agencyLead,
brand_guidelines: campaignData.brandGuidelines,
});
const newCampaign = apiService.convertCampaignToFrontend(response);
setCampaigns(prev => [...prev, newCampaign]);
setCampaignProofs(prev => ({ ...prev, [newCampaign.name]: [] }));
} catch (error) {
console.error('Error creating campaign:', error);
setError('Failed to create campaign. Please try again.');
}
};
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 handleProofUploadForCampaign = async (
campaignName: string,
file: File,
proofName: string,
channel: string,
subChannel: string,
proofType?: string
) => {
setError(null);
// Find the campaign to get its ID for API persistence
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found. Please refresh and try again.');
return;
}
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 {
// Pass campaign context to persist proof in database
const result = await analyzeProof(file, handleAgentUpdate, msalInstance, {
campaignId: campaign._id,
proofName,
channel,
subChannel,
proofType,
});
const feedback = result.review;
// Refresh proofs from API to get the persisted data
// This ensures we have the correct IDs and data from the database
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[campaignName]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (refreshError) {
console.error('Failed to refresh proofs after analysis:', refreshError);
// Fallback: remove the temp placeholder since analysis succeeded
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.tempId !== tempId)
}));
}
// Refresh campaigns list to get updated proof count
try {
const campaignsResponse = await apiService.getCampaigns();
setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c)));
} catch (refreshError) {
console.error('Failed to refresh campaigns after analysis:', refreshError);
}
// If analysis resulted in error, log it (error items are now tracked on backend)
if (feedback.overallStatus === 'Analysis Error') {
console.warn('Proof analysis resulted in error status:', feedback.leadAgentSummary);
}
} catch (err) {
console.error("Failed to upload and analyze proof:", err);
setError("Failed to upload and analyze proof. Please try again.");
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].map(proof =>
proof.tempId === tempId ? { ...proof, status: 'error' } : proof
)
}));
}
};
const handleRetryAnalysis = async (campaignName: string, tempId: string) => {
const proofToRetry = campaignProofs[campaignName]?.find(proof => proof.tempId === tempId);
if (!proofToRetry) {
console.error("Proof to retry not found");
return;
}
// Find the campaign to get its ID for API persistence
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found. Please refresh and try again.');
return;
}
let file = proofToRetry.file;
// Fetch from backend if not in memory
if (!file) {
const storageKey = proofToRetry.fileStorageKey || proofToRetry.versions?.[0]?.fileStorageKey;
if (!storageKey) {
setError('Cannot retry: original file is not available.');
return;
}
try {
file = await apiService.getFile(storageKey);
} catch (err) {
console.error("Failed to fetch file for retry:", err);
setError('Failed to retrieve original file for retry.');
return;
}
}
const { 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 {
// Pass campaign context to persist proof in database
const result = await analyzeProof(file, handleAgentUpdateForRetry, msalInstance, {
campaignId: campaign._id,
proofName,
channel,
subChannel,
proofType,
});
// Refresh proofs from API to get the persisted data
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[campaignName]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (refreshError) {
console.error('Failed to refresh proofs after retry:', refreshError);
// Remove the temp placeholder since analysis succeeded
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.tempId !== tempId)
}));
}
// Refresh campaigns list to get updated proof count
try {
const campaignsResponse = await apiService.getCampaigns();
setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c)));
} catch (refreshError) {
console.error('Failed to refresh campaigns after retry:', refreshError);
}
if (result.review.overallStatus === 'Analysis Error') {
console.warn('Retry analysis resulted in error status:', result.review.leadAgentSummary);
}
} catch (err) {
console.error("Failed to retry proof analysis:", err);
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].map(proof =>
proof.tempId === tempId ? { ...proof, status: 'error' } : proof
)
}));
}
};
const handleCampaignStatusChange = async (campaignName: string, newStatus: 'In Progress' | 'Completed') => {
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found.');
return;
}
const previousStatus = campaign.status;
const previousLastModified = campaign.lastModified;
// Optimistically update local state immediately
setCampaigns(prev => prev.map(p =>
p.name === campaignName ? { ...p, status: newStatus, lastModified: new Date().toISOString() } : p
));
try {
await apiService.updateCampaign(campaign._id, { status: newStatus });
} catch (error) {
console.error('Error updating campaign status:', error);
setError('Failed to update campaign status.');
// Rollback on error
setCampaigns(prev => prev.map(p =>
p.name === campaignName ? { ...p, status: previousStatus, lastModified: previousLastModified } : p
));
}
};
const handleDeleteProof = async (campaignName: string, proofName: string) => {
// Find the proof to get its ID
const proofToDelete = campaignProofs[campaignName]?.find(p => p.proofName === proofName);
if (!proofToDelete?._id) {
// If no _id, it might be a temp proof that hasn't been saved to DB yet
// Just remove it from local state
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.proofName !== proofName)
}));
return;
}
try {
await apiService.deleteProof(proofToDelete._id);
// Update local state after successful deletion
setCampaignProofs(prevProofs => ({
...prevProofs,
[campaignName]: prevProofs[campaignName].filter(p => p.proofName !== proofName)
}));
// Refresh campaigns to get updated proof count
const campaignsResponse = await apiService.getCampaigns();
setCampaigns(campaignsResponse.map(c => apiService.convertCampaignToFrontend(c)));
} catch (error) {
console.error('Error deleting proof:', error);
setError('Failed to delete proof.');
}
};
// --- SETTINGS HANDLERS (NOW USE API) ---
// Helper to refresh dropdown options from API
const refreshDropdownOptions = async () => {
try {
const options = await apiService.getDropdownOptions();
setDropdownOptions({
campaigns: options.campaigns || [],
channels: options.channels || {},
brandGuidelines: options.brand_guidelines || []
});
} catch (error) {
console.error('Failed to refresh dropdown options:', error);
}
};
// Campaign options are not stored in dropdown_options table - keeping local for now
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 = async (channel: string) => {
try {
await apiService.addChannel(channel);
// Optimistically update local state
setDropdownOptions(prev => ({
...prev,
channels: { ...prev.channels, [channel]: {} }
}));
} catch (error) {
console.error('Error adding channel:', error);
setError('Failed to add channel.');
}
};
const handleRemoveChannel = async (channel: string) => {
try {
await apiService.deleteChannel(channel);
// Optimistically update local state
setDropdownOptions(prev => {
const newChannels = { ...prev.channels };
delete newChannels[channel];
return { ...prev, channels: newChannels };
});
} catch (error) {
console.error('Error removing channel:', error);
setError('Failed to remove channel.');
}
};
const handleAddSubChannel = async (channel: string, subChannel: string) => {
try {
await apiService.addSubChannel(channel, subChannel);
// Optimistically update local state
setDropdownOptions(prev => ({
...prev,
channels: {
...prev.channels,
[channel]: { ...prev.channels[channel], [subChannel]: [] }
}
}));
} catch (error) {
console.error('Error adding sub-channel:', error);
setError('Failed to add sub-channel.');
}
};
const handleRemoveSubChannel = async (channel: string, subChannel: string) => {
try {
await apiService.deleteSubChannel(channel, subChannel);
// Optimistically update local state
setDropdownOptions(prev => {
const channelData = { ...prev.channels[channel] };
delete channelData[subChannel];
return { ...prev, channels: { ...prev.channels, [channel]: channelData } };
});
} catch (error) {
console.error('Error removing sub-channel:', error);
setError('Failed to remove sub-channel.');
}
};
const handleAddProofType = async (channel: string, subChannel: string, proofType: string) => {
try {
await apiService.addProofType(channel, subChannel, proofType);
// Optimistically update local state
setDropdownOptions(prev => {
const channelData = prev.channels[channel] || {};
const currentTypes = channelData[subChannel] || [];
return {
...prev,
channels: {
...prev.channels,
[channel]: {
...channelData,
[subChannel]: [...currentTypes, proofType].sort()
}
}
};
});
} catch (error) {
console.error('Error adding proof type:', error);
setError('Failed to add proof type.');
}
};
const handleRemoveProofType = async (channel: string, subChannel: string, proofType: string) => {
try {
await apiService.deleteProofType(channel, subChannel, proofType);
// Optimistically update local state
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)
}
}
};
});
} catch (error) {
console.error('Error removing proof type:', error);
setError('Failed to remove proof type.');
}
};
const handleNavigate = (view: View) => {
setCurrentView(view);
setSelectedCampaign(null); // Reset campaign on any main navigation
setSelectedProof(null);
};
const handleSelectCampaign = async (campaignName: string) => {
setSelectedCampaign(campaignName);
// Find the campaign to get its ID for API calls
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
console.error('Campaign not found or missing ID:', campaignName);
return;
}
// Load proofs from API if not already cached
if (!campaignProofs[campaignName]) {
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[campaignName]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (error) {
console.error('Failed to load proofs for campaign:', campaignName, error);
setError('Failed to load proofs. Please try again.');
}
}
};
const handleSelectProof = (proof: any) => {
setSelectedProof(proof);
};
const handleBackToCampaignsList = () => {
setSelectedCampaign(null);
setSelectedProof(null);
};
const handleBackToCampaignDetails = () => {
setSelectedProof(null);
};
const handleFlagSubmit = async (flagData: Omit<FlaggedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
// Find the proof to get its ID
const proof = campaignProofs[flagData.campaignName]?.find(p => p.proofName === flagData.proofName);
if (!proof?._id) {
setError('Proof not found. Unable to submit flag.');
return;
}
try {
const response = await apiService.flagProofVersion(
proof._id,
flagData.version,
{ agent_flagged: flagData.agentFlagged, comments: flagData.comments }
);
const newFlag = apiService.convertFlaggedItemToFrontend(response);
setFlaggedItems(prev => [newFlag, ...prev]);
} catch (error) {
console.error('Error flagging proof version:', error);
setError('Failed to submit flag. Please try again.');
}
};
const handleResolveSubmit = async (resolveData: Omit<ResolvedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => {
// Find the proof to get its ID
const proof = campaignProofs[resolveData.campaignName]?.find(p => p.proofName === resolveData.proofName);
if (!proof?._id) {
setError('Proof not found. Unable to submit resolution.');
return;
}
try {
const response = await apiService.resolveProofVersion(
proof._id,
resolveData.version,
{ agent: resolveData.agent, issue: resolveData.issue, resolution: resolveData.resolution }
);
const newResolution = apiService.convertResolvedItemToFrontend(response);
setResolvedItems(prev => [newResolution, ...prev]);
} catch (error) {
console.error('Error resolving proof version:', error);
setError('Failed to submit resolution. Please try again.');
}
};
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';
// Get user info from MSAL for sidebar display
const userInfo = getUserInfo(msalInstance);
return (
<div className={`flex h-screen ${mainBgColor} font-sans text-gray-800 overflow-hidden`}>
<Sidebar
activeItem={currentView}
onNavigate={(view) => handleNavigate(view as View)}
userName={userInfo?.name}
userEmail={userInfo?.email}
/>
<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;