modcomms/frontend/components/PDFReport.tsx
Vadym Samoilenko 3f0e774ccb Replace logo with v6 SVG across Sidebar and PDF Report
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:27:51 +01:00

230 lines
No EOL
13 KiB
TypeScript
Executable file

import React from 'react';
import type { AgentReview, RagStatus } from '../types';
import { LegalIcon } from './icons/LegalIcon';
import { BrandIcon } from './icons/BrandIcon';
import { ChannelIcon } from './icons/ChannelIcon';
interface PDFReportProps {
campaignName: string;
proofs: any[];
}
/**
* Renders inline markdown bold (**text**) as <strong> elements for PDF output.
*/
const renderBoldMarkdownForPDF = (text: string): React.ReactNode[] => {
const parts: React.ReactNode[] = [];
const regex = /\*\*(.+?)\*\*/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
parts.push(<strong key={match.index}>{match[1]}</strong>);
lastIndex = regex.lastIndex;
}
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return parts;
};
const formatFeedbackTextForPDF = (text: string): React.ReactNode => {
if (!text) return null;
// Normalize HTML tags and bullet characters
let normalizedText = text
.replace(/<\/li>/gi, '\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<ul[^>]*>/gi, '')
.replace(/<li[^>]*>/gi, '• ')
.replace(/<[^>]+>/g, '')
.replace(/\s*•\s*/g, '\n• ')
.replace(/\n{2,}/g, '\n')
.trim();
const lines = normalizedText.split('\n').filter(line => line.trim());
// Each bullet is an array of lines (to support multi-line Issue/Recommendation)
const bulletGroups: string[][] = [];
const introLines: string[] = [];
lines.forEach(line => {
const trimmed = line.trim();
if (trimmed.startsWith('•')) {
bulletGroups.push([trimmed.replace(/^•\s*/, '').trim()]);
} else if (trimmed) {
if (bulletGroups.length === 0) {
introLines.push(trimmed);
} else {
// Continuation line within the current bullet
bulletGroups[bulletGroups.length - 1].push(trimmed);
}
}
});
return (
<>
{introLines.length > 0 && (
<p style={{ margin: '0 0 8px 0' }}>{renderBoldMarkdownForPDF(introLines.join(' '))}</p>
)}
{bulletGroups.length > 0 && (
<ul style={{ margin: 0, paddingLeft: '20px', listStyleType: 'disc', listStylePosition: 'outside' }}>
{bulletGroups.map((group, index) => (
<li key={index} style={{ marginBottom: '8px', lineHeight: '1.5', breakInside: 'avoid' }}>
{group.map((line, lineIdx) => (
<React.Fragment key={lineIdx}>
{lineIdx > 0 && <br />}
{renderBoldMarkdownForPDF(line)}
</React.Fragment>
))}
</li>
))}
</ul>
)}
</>
);
};
const RagStatusBadge: React.FC<{ status: RagStatus }> = ({ status }) => {
let bgColor = '#E5E7EB'; // Gray for Error
let textColor = '#1F2937';
switch (status) {
case 'Red': bgColor = '#FEE2E2'; textColor = '#991B1B'; break;
case 'Amber': bgColor = '#FEF3C7'; textColor = '#92400E'; break;
case 'Green': bgColor = '#D1FAE5'; textColor = '#065F46'; break;
}
return (
<span style={{ backgroundColor: bgColor, borderRadius: '9999px', display: 'inline-block', minWidth: '50px', paddingLeft: '12px', paddingRight: '12px', paddingTop: '4px', paddingBottom: '4px', lineHeight: '1.2', textAlign: 'center', color: textColor, fontSize: '12px', fontWeight: 'bold' }}>
{status}
</span>
);
};
export const PDFReport: React.FC<PDFReportProps> = ({ campaignName, proofs }) => {
const logoUrl = `${window.location.origin}${import.meta.env.BASE_URL}BAR-ModComms-logo-v6.svg`;
const today = new Date().toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
});
const isCampaignReport = proofs.length > 1;
const singleProofName = !isCampaignReport && proofs.length > 0 ? proofs[0].proofName : '';
return (
<div style={{ width: '210mm', fontFamily: 'Arial, sans-serif', color: '#333333', background: '#FFFFFF' }}>
{/* --- Cover Page --- */}
<div style={{ width: '210mm', height: '297mm', display: 'flex', flexDirection: 'column', padding: '20mm', boxSizing: 'border-box', borderBottom: '1px solid #e5e5e5' }}>
<div style={{ display: 'flex', alignItems: 'center', paddingBottom: '10mm', borderBottom: '1px solid #e5e5e5' }}>
<div style={{ backgroundColor: '#000000', padding: '10px 16px', borderRadius: '6px', display: 'inline-flex' }}>
<img src={logoUrl} alt="Mod Comms AI — In partnership with Barclays" style={{ height: '60px', width: 'auto' }} />
</div>
</div>
<div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
<h1 style={{ fontSize: '42px', color: '#001f5a', margin: '0 0 10px 0' }}>AI Compliance & Brand Report</h1>
<p style={{ fontSize: '18px', color: '#555', marginTop: '0' }}>
{isCampaignReport ? 'Campaign-Level Summary' : 'Single Proof Analysis'}
</p>
</div>
<div style={{ borderTop: '1px solid #e5e5e5', paddingTop: '10mm' }}>
<p style={{ margin: 0, fontSize: '14px' }}><strong>Campaign:</strong> {campaignName}</p>
{!isCampaignReport && <p style={{ margin: '5px 0 0 0', fontSize: '14px' }}><strong>Proof:</strong> {singleProofName}</p>}
<p style={{ margin: '5px 0 0 0', fontSize: '14px' }}><strong>Export Date:</strong> {today}</p>
</div>
</div>
{/* --- Table of Contents for Campaign Report --- */}
{isCampaignReport && (
<div style={{ width: '210mm', minHeight: '297mm', padding: '20mm', boxSizing: 'border-box', pageBreakBefore: 'always', borderBottom: '1px solid #e5e5e5' }}>
<h2 style={{ fontSize: '28px', color: '#001f5a', borderBottom: '2px solid #00a3e0', paddingBottom: '8px', marginBottom: '20px' }}>
Table of Contents
</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
{proofs.map((proof, index) => (
<li key={index} style={{ fontSize: '16px', padding: '10px 0', borderBottom: '1px dotted #ccc' }}>
{proof.proofName} - V{proof.versions[0]?.version || 1}
</li>
))}
</ul>
</div>
)}
{/* --- Proof Report Pages --- */}
{proofs.map((proof) => {
const version = proof.versions[0];
if (!version) return null;
const feedback: AgentReview = version.feedback;
const agentReviews = [
{ title: 'Legal Agent', review: feedback.legalAgentReview, icon: <LegalIcon style={{height: '24px', width: '24px'}} /> },
{ title: 'Brand Agent', review: feedback.brandAgentReview, icon: <BrandIcon style={{height: '24px', width: '24px'}} /> },
{ title: 'Channel Best Practices Agent', review: feedback.channelBestPracticesAgentReview, icon: <ChannelIcon style={{height: '24px', width: '24px'}} /> },
{ title: 'Channel Tech Specs Agent', review: feedback.channelTechSpecsAgentReview, icon: <ChannelIcon style={{height: '24px', width: '24px'}} /> },
];
return (
<div key={proof.proofName} style={{ width: '210mm', minHeight: '297mm', padding: '15mm', boxSizing: 'border-box', pageBreakBefore: 'always' }}>
{/* Proof Header */}
<div style={{ paddingBottom: '8px', borderBottom: '2px solid #00a3e0', marginBottom: '10mm' }}>
<h2 style={{ fontSize: '24px', color: '#001f5a', margin: 0 }}>{proof.proofName} - V{version.version}</h2>
<p style={{ fontSize: '14px', color: '#555', margin: '4px 0 0 0' }}>
{version.workfrontId} &bull; {proof.channel} / {proof.subChannel}
</p>
</div>
{/* Preview & Summary */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10mm', marginBottom: '10mm', breakInside: 'avoid' }}>
<div style={{ border: '1px solid #ccc', padding: '4px', background: '#f8f8f8' }}>
<img src={version.proofPreviewUrl} alt="Proof Preview" style={{ width: '100%', objectFit: 'contain' }} />
</div>
<div style={{ backgroundColor: '#f4f6f8', padding: '6mm', borderRadius: '4px', borderLeft: `4px solid ${feedback.overallStatus === 'Passed' ? '#10B981' : '#EF4444'}`}}>
<h3 style={{ fontSize: '18px', margin: '0 0 8px 0', color: '#001f5a' }}>Overall Summary</h3>
<p style={{ fontSize: '14px', margin: '0 0 10px 0', fontWeight: 'bold' }}>Status: {feedback.overallStatus}</p>
{feedback.overallStatus === 'Requires Manual Legal Review' && (
<div style={{ backgroundColor: '#F3E8FF', border: '1px solid #D8B4FE', borderRadius: '4px', padding: '8px', marginBottom: '10px'}}>
<p style={{fontSize: '13px', margin: 0, fontWeight: 'bold', color: '#6B21A8'}}>Financial Promotion Detected:</p>
<p style={{fontSize: '12px', margin: '4px 0 0 0', color: '#6B21A8', fontStyle: 'italic'}}>"{feedback.financialPromotionReason}"</p>
</div>
)}
<div style={{ fontSize: '14px', lineHeight: 1.5, margin: 0 }}>{formatFeedbackTextForPDF(feedback.leadAgentSummary)}</div>
</div>
</div>
{/* Detailed Agent Feedback */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm', breakInside: 'avoid' }}>
{agentReviews.map(({ title, review, icon }) => (
<div key={title} style={{ border: '1px solid #e5e5e5', borderRadius: '4px', padding: '5mm', backgroundColor: '#fff', breakInside: 'avoid', pageBreakInside: 'avoid' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<h4 style={{ fontSize: '16px', fontWeight: 'bold', color: '#001f5a', margin: 0, display: 'flex', alignItems: 'center', gap: '8px'}}>
<span style={{color: '#0070c0'}}>{icon}</span> {title}
</h4>
<RagStatusBadge status={review.ragStatus} />
</div>
<div style={{ fontSize: '13px', lineHeight: 1.5, margin: '0 0 10px 0', borderTop: '1px solid #eee', paddingTop: '10px' }}>{formatFeedbackTextForPDF(review.feedback)}</div>
{review.issues && review.issues.length > 0 && (
<div>
<h5 style={{ fontSize: '13px', fontWeight: 'bold', margin: '0 0 5px 0' }}>Key Actions:</h5>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: 1.5, listStyleType: 'disc', listStylePosition: 'outside' }}>
{review.issues.map((issue, i) => <li key={i} style={{ marginBottom: '4px', lineHeight: '1.5', breakInside: 'avoid' }}>{issue}</li>)}
</ul>
</div>
)}
</div>
))}
</div>
</div>
);
})}
</div>
);
};