modcomms/frontend/components/FeedbackReport.tsx
Vadym Samoilenko 8317e01568 Add azure border to all modal containers per Oliver design
All modal inner containers now have border-2 border-oliver-azure
for consistent Oliver branding across:
- CreateCampaignModal, CreateProjectModal
- FeedbackReport (resolve + flag modals)
- UserManagement (confirmation + history modals)
- Campaigns (upload, delete confirmation, version history modals)
- Projects (upload, delete modals)
- Login (support contact modal)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 10:21:59 +00:00

921 lines
No EOL
50 KiB
TypeScript
Executable file

import React, { useState, useEffect } from 'react';
import type { AgentReview, SubReview, RagStatus, OverallStatus, FlaggedItem, ResolvedItem } from '../types';
import { CheckCircleIcon, ExclamationTriangleIcon, InformationCircleIcon } from './icons/StatusIcons';
import { FlagIcon } from './icons/FlagIcon';
import { XIcon } from './icons/XIcon';
import { BugIcon } from './icons/BugIcon';
import { ExportIcon } from './icons/ExportIcon';
import { LegalIcon } from './icons/LegalIcon';
/**
* Formats feedback text from Gemini API into properly structured React elements.
* Handles HTML tags, bullet characters, and newline-separated text.
*/
/**
* Renders inline markdown bold (**text**) as <strong> elements.
* Returns an array of React nodes with bold segments wrapped in <strong>.
*/
const renderBoldMarkdown = (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 formatFeedbackText = (text: string): React.ReactNode => {
if (!text) return null;
// First, handle HTML tags by converting them to a normalized format
let normalizedText = text
// Convert literal \n sequences to real newlines
.replace(/\\n/g, '\n')
// Replace </li> and </ul> with newlines
.replace(/<\/li>/gi, '\n')
.replace(/<\/ul>/gi, '\n')
// Remove opening tags
.replace(/<ul[^>]*>/gi, '')
.replace(/<li[^>]*>/gi, '• ')
// Remove any other HTML tags (but preserve ** markdown bold)
.replace(/<[^>]+>/g, '')
// Normalize whitespace around bullets
.replace(/\s*•\s*/g, '\n• ')
// Clean up multiple newlines
.replace(/\n{2,}/g, '\n')
.trim();
// Split into lines
const lines = normalizedText.split('\n').filter(line => line.trim());
// Separate intro text from bullet points
// 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('•')) {
// Start a new bullet group
bulletGroups.push([trimmed.replace(/^•\s*/, '').trim()]);
} else if (trimmed) {
if (bulletGroups.length === 0) {
// Haven't started bullets yet — it's intro text
introLines.push(trimmed);
} else {
// Continuation line within the current bullet
bulletGroups[bulletGroups.length - 1].push(trimmed);
}
}
});
return (
<>
{introLines.length > 0 && (
<p className="mb-3">{renderBoldMarkdown(introLines.join(' '))}</p>
)}
{bulletGroups.length > 0 && (
<ul className="list-disc list-inside">
{bulletGroups.map((group, index) => (
<React.Fragment key={index}>
{group.map((line, lineIdx) => (
<li key={lineIdx} className={lineIdx === 0 && index > 0 ? 'mt-3' : ''}>
{renderBoldMarkdown(line)}
</li>
))}
</React.Fragment>
))}
</ul>
)}
</>
);
};
const RagStatusBadge: React.FC<{ status: RagStatus; isLarge?: boolean }> = ({ status, isLarge = false }) => {
let colorClasses = '';
let iconColor = '';
switch (status) {
case 'Red':
colorClasses = 'bg-error-light border-error text-error';
iconColor = 'text-error';
break;
case 'Amber':
colorClasses = 'bg-warning-light border-warning text-oliver-black';
iconColor = 'text-warning';
break;
case 'Green':
colorClasses = 'bg-success-light border-success text-success';
iconColor = 'text-success';
break;
case 'Error':
colorClasses = 'bg-oliver-grey border-grey-300 text-oliver-black';
iconColor = 'text-oliver-black/60';
break;
}
const sizeClasses = isLarge ? 'px-4 py-1.5 text-sm rounded-[10px] border shadow-sm' : 'px-2.5 py-1 text-xs rounded-[10px] border shadow-sm';
return (
<div className={`inline-flex items-center font-bold tracking-wide ${sizeClasses} ${colorClasses} backdrop-blur-sm`}>
{status === 'Red' && <ExclamationTriangleIcon className={`h-4 w-4 mr-1.5 ${iconColor}`} />}
{status === 'Amber' && <InformationCircleIcon className={`h-4 w-4 mr-1.5 ${iconColor}`} />}
{status === 'Green' && <CheckCircleIcon className={`h-4 w-4 mr-1.5 ${iconColor}`} />}
{status === 'Error' && <BugIcon className={`h-4 w-4 mr-1.5 ${iconColor}`} />}
<span>{status}</span>
</div>
);
};
const ResolveIssueModal: React.FC<{
isOpen: boolean;
onClose: () => void;
onSubmit: (reason: string) => void;
issueText: string;
}> = ({ isOpen, onClose, onSubmit, issueText }) => {
if (!isOpen) return null;
const [reason, setReason] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (reason.trim()) {
onSubmit(reason);
}
};
return (
<div
className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center z-50 transition-all duration-300"
onClick={onClose}
>
<div
className="bg-white rounded-[10px] shadow-2xl p-8 w-full max-w-lg transform transition-all border-2 border-oliver-azure"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-2xl font-bold text-oliver-black mb-2">Resolve Issue</h3>
<p className="text-oliver-black mb-6">Please provide a reason for manually resolving this issue.</p>
<div className="my-6 p-4 bg-oliver-grey border border-grey-300 rounded-[10px] text-oliver-black italic text-sm">
"{issueText}"
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="resolution-reason" className="block text-sm font-bold text-oliver-black mb-2">Reason for resolution</label>
<textarea
id="resolution-reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
className="w-full p-4 border-2 border-oliver-azure rounded-[10px] focus:ring-2 focus:ring-oliver-azure/50 focus:border-oliver-azure transition-all bg-oliver-grey focus:bg-white resize-none text-oliver-black"
rows={4}
placeholder="e.g. 'Legal team has approved this exception via email...'"
required
/>
<div className="mt-8 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-6 py-2.5 rounded-full border-2 border-oliver-azure text-oliver-azure font-semibold hover:bg-oliver-azure hover:text-white transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-6 py-2.5 rounded-full bg-oliver-azure text-white font-bold shadow-lg shadow-oliver-azure/30 hover:bg-oliver-azure/90 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!reason.trim()}
>
Submit Resolution
</button>
</div>
</form>
</div>
</div>
);
};
const FlagIssueModal: React.FC<{
isOpen: boolean;
onClose: () => void;
onSubmit: (comments: string) => void;
agentName: string;
}> = ({ isOpen, onClose, onSubmit, agentName }) => {
if (!isOpen) return null;
const [comments, setComments] = useState('');
const [showSuccess, setShowSuccess] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(comments);
setShowSuccess(true);
setTimeout(() => {
setShowSuccess(false);
setComments('');
onClose();
}, 2000);
};
return (
<div
className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center z-50 transition-opacity duration-300"
onClick={showSuccess ? undefined : onClose}
>
<div
className="bg-white rounded-[10px] shadow-2xl p-8 w-full max-w-lg transform transition-all border-2 border-oliver-azure"
onClick={(e) => e.stopPropagation()}
>
{showSuccess ? (
<div className="text-center py-8">
<CheckCircleIcon className="h-12 w-12 text-success mx-auto mb-4" />
<h3 className="text-xl font-bold text-oliver-black mb-2">Flag Submitted</h3>
<p className="text-slate-500">Thank you for your feedback on the {agentName}'s review. This has been logged for auditing.</p>
</div>
) : (
<>
<div className="flex justify-between items-start mb-6">
<div>
<h3 className="text-2xl font-bold text-oliver-black">Flag Feedback</h3>
<p className="text-slate-500 text-sm mt-1">Reporting incorrect feedback from <span className="font-semibold text-oliver-azure">{agentName}</span></p>
</div>
<button onClick={onClose} className="p-2 rounded-full hover:bg-slate-100 text-slate-400 hover:text-slate-600 transition-colors">
<XIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="flag-comments" className="block text-sm font-bold text-slate-700 mb-2">Additional Comments</label>
<textarea
id="flag-comments"
value={comments}
onChange={(e) => setComments(e.target.value)}
className="w-full p-4 border border-slate-200 rounded-xl focus:ring-2 focus:ring-red-500/30 focus:border-red-500 transition-all bg-slate-50 focus:bg-white resize-none"
rows={5}
placeholder="Please explain why this feedback is incorrect..."
/>
<div className="mt-8 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 rounded-xl text-slate-600 font-semibold hover:bg-slate-100 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2.5 rounded-xl bg-red-600 text-white font-bold shadow-lg shadow-red-600/30 hover:bg-red-500 transition-all"
>
Submit Flag
</button>
</div>
</form>
</>
)}
</div>
</div>
);
};
// Helper function to detect revision mode (when revision data is present)
const hasRevisionData = (review: SubReview): boolean => {
return (
(review.resolvedIssues && review.resolvedIssues.length > 0) ||
(review.outstandingIssues && review.outstandingIssues.length > 0) ||
(review.newIssues && review.newIssues.length > 0)
);
};
const SubReviewCard: React.FC<{
title: string;
review: SubReview;
onFlag: () => void;
onResolve: (issueText: string, reason: string) => void;
isFlagged?: boolean;
resolvedItems?: ResolvedItem[];
}> = ({ title, review, onFlag, onResolve, isFlagged, resolvedItems = [] }) => {
interface IssueState {
text: string;
status: 'actionable' | 'resolved';
reason?: string;
category?: 'outstanding' | 'new'; // For revision mode
}
const [issues, setIssues] = useState<IssueState[]>([]);
const [currentStatus, setCurrentStatus] = useState<RagStatus>(review.ragStatus);
const [isModalOpen, setIsModalOpen] = useState(false);
const [activeIssueIndex, setActiveIssueIndex] = useState<number | null>(null);
// Section collapse states for revision mode
const [resolvedCollapsed, setResolvedCollapsed] = useState(true);
const [outstandingCollapsed, setOutstandingCollapsed] = useState(false);
const [newCollapsed, setNewCollapsed] = useState(false);
useEffect(() => {
const findResolution = (text: string): ResolvedItem | undefined =>
resolvedItems.find(r => r.issue === text);
if (hasRevisionData(review)) {
// Revision mode: populate from outstandingIssues and newIssues
const outstandingIssues: IssueState[] = (review.outstandingIssues || []).map(text => {
const match = findResolution(text);
return {
text,
status: match ? 'resolved' as const : 'actionable' as const,
reason: match?.resolution,
category: 'outstanding' as const
};
});
const newIssues: IssueState[] = (review.newIssues || []).map(text => {
const match = findResolution(text);
return {
text,
status: match ? 'resolved' as const : 'actionable' as const,
reason: match?.resolution,
category: 'new' as const
};
});
setIssues([...outstandingIssues, ...newIssues]);
} else {
// Original mode: use review.issues
setIssues(review.issues.map(text => {
const match = findResolution(text);
return {
text,
status: match ? 'resolved' as const : 'actionable' as const,
reason: match?.resolution
};
}));
}
setCurrentStatus(review.ragStatus);
}, [review, resolvedItems]);
useEffect(() => {
if (review.ragStatus === 'Error') {
setCurrentStatus('Error');
return;
}
if (issues.length > 0 && issues.every(issue => issue.status === 'resolved')) {
setCurrentStatus('Green');
} else if (issues.some(issue => issue.status === 'actionable')) {
setCurrentStatus(review.ragStatus);
}
}, [issues, review.ragStatus]);
const handleOpenModal = (index: number) => {
setActiveIssueIndex(index);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setActiveIssueIndex(null);
};
const handleResolve = (reason: string) => {
if (activeIssueIndex === null) return;
const issueText = issues[activeIssueIndex].text;
setIssues(currentIssues =>
currentIssues.map((issue, index) =>
index === activeIssueIndex
? { ...issue, status: 'resolved', reason }
: issue
)
);
onResolve(issueText, reason);
handleCloseModal();
};
const handleReopen = (indexToReopen: number) => {
setIssues(currentIssues =>
currentIssues.map((issue, index) =>
index === indexToReopen
? { ...issue, status: 'actionable', reason: undefined }
: issue
)
);
};
// Determine styles based on status
let containerStyles = 'bg-white border-slate-100';
let gradientOverlay = 'from-slate-50/50 to-white';
let headerColor = 'text-slate-800';
let issueIconColor = 'text-slate-400';
let issueIcon = <InformationCircleIcon className="h-5 w-5 text-slate-400 mt-0.5 flex-shrink-0" />;
if (currentStatus === 'Green') {
containerStyles = 'bg-white border-emerald-100 hover:border-emerald-200';
gradientOverlay = 'from-emerald-50/40 to-white';
headerColor = 'text-emerald-900';
} else if (currentStatus === 'Amber') {
containerStyles = 'bg-white border-amber-100 hover:border-amber-200';
gradientOverlay = 'from-amber-50/40 to-white';
headerColor = 'text-amber-900';
issueIconColor = 'text-amber-500';
issueIcon = <ExclamationTriangleIcon className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />;
} else if (currentStatus === 'Red') {
containerStyles = 'bg-white border-red-100 hover:border-red-200';
gradientOverlay = 'from-red-50/40 to-white';
headerColor = 'text-red-900';
issueIconColor = 'text-red-500';
issueIcon = <ExclamationTriangleIcon className="h-5 w-5 text-red-500 mt-0.5 flex-shrink-0" />;
}
return (
<>
<ResolveIssueModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onSubmit={handleResolve}
issueText={activeIssueIndex !== null ? issues[activeIssueIndex].text : ''}
/>
<div className={`relative rounded-2xl border p-6 shadow-sm transition-all duration-300 hover:shadow-md ${containerStyles} overflow-hidden group`}>
{/* Subtle gradient background */}
<div className={`absolute inset-0 bg-gradient-to-br ${gradientOverlay} pointer-events-none`}></div>
<div className="relative z-10">
<div className="flex justify-between items-center gap-4 mb-5">
<div className="flex items-center gap-3">
<h4 className={`text-lg font-bold ${headerColor}`}>{title}</h4>
</div>
<div className="flex items-center gap-3">
<RagStatusBadge status={currentStatus} />
<button
onClick={onFlag}
className={`p-1.5 rounded-lg transition-colors ${isFlagged ? 'text-red-500' : 'text-slate-400 hover:bg-red-50 hover:text-red-500'}`}
title={isFlagged ? "Flagged as incorrect" : "Flag as incorrect"}
aria-label={isFlagged ? "This feedback has been flagged" : "Flag this feedback as incorrect"}
>
<FlagIcon className="h-4 w-4" filled={isFlagged} />
</button>
</div>
</div>
<div className="text-slate-600 text-sm leading-relaxed mb-6">
{formatFeedbackText(review.feedback)}
</div>
{/* Revision mode: Show categorized sections */}
{currentStatus !== 'Error' && hasRevisionData(review) && (
<div className="space-y-4">
{/* Resolved Issues Section */}
{review.resolvedIssues && review.resolvedIssues.length > 0 && (
<div className="bg-emerald-50/50 rounded-xl border border-emerald-100 overflow-hidden">
<button
onClick={() => setResolvedCollapsed(!resolvedCollapsed)}
className="w-full flex items-center justify-between p-4 hover:bg-emerald-50/80 transition-colors"
>
<div className="flex items-center gap-2">
<CheckCircleIcon className="h-5 w-5 text-emerald-500" />
<h5 className="text-xs font-bold uppercase tracking-wider text-emerald-600">
Resolved Issues
</h5>
<span className="bg-emerald-100 text-emerald-700 text-xs font-semibold px-2 py-0.5 rounded-full">
{review.resolvedIssues.length}
</span>
</div>
<svg
className={`h-4 w-4 text-emerald-500 transition-transform ${resolvedCollapsed ? '' : 'rotate-180'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{!resolvedCollapsed && (
<ul className="px-4 pb-4 space-y-2">
{review.resolvedIssues.map((issueText, index) => (
<li key={`resolved-${index}`} className="flex items-start gap-3">
<CheckCircleIcon className="h-5 w-5 text-emerald-500 mt-0.5 flex-shrink-0" />
<span className="text-sm text-emerald-700 line-through opacity-75">
{issueText}
</span>
</li>
))}
</ul>
)}
</div>
)}
{/* Outstanding Issues Section */}
{issues.filter(i => i.category === 'outstanding').length > 0 && (
<div className="bg-amber-50/50 rounded-xl border border-amber-100 overflow-hidden">
<button
onClick={() => setOutstandingCollapsed(!outstandingCollapsed)}
className="w-full flex items-center justify-between p-4 hover:bg-amber-50/80 transition-colors"
>
<div className="flex items-center gap-2">
<ExclamationTriangleIcon className="h-5 w-5 text-amber-500" />
<h5 className="text-xs font-bold uppercase tracking-wider text-amber-600">
Outstanding Issues
</h5>
<span className="bg-amber-100 text-amber-700 text-xs font-semibold px-2 py-0.5 rounded-full">
{issues.filter(i => i.category === 'outstanding' && i.status === 'actionable').length}
</span>
</div>
<svg
className={`h-4 w-4 text-amber-500 transition-transform ${outstandingCollapsed ? '' : 'rotate-180'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{!outstandingCollapsed && (
<ul className="px-4 pb-4 space-y-3">
{issues.filter(i => i.category === 'outstanding').map((issue, idx) => {
const actualIndex = issues.findIndex((i, index) => i === issue && index >= 0);
return (
<li key={`outstanding-${idx}`} className="flex items-start gap-3 group/issue">
{issue.status === 'actionable' ? (
<ExclamationTriangleIcon className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />
) : (
<CheckCircleIcon className="h-5 w-5 text-emerald-500 mt-0.5 flex-shrink-0" />
)}
<div className="flex-grow pt-0.5">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
<span className={`text-sm ${issue.status === 'resolved' ? 'line-through text-slate-400' : 'text-amber-800'}`}>
{issue.text}
</span>
{issue.status === 'actionable' ? (
<button
onClick={() => handleOpenModal(actualIndex)}
className="flex-shrink-0 opacity-0 group-hover/issue:opacity-100 focus:opacity-100 text-xs font-semibold text-oliver-azure hover:text-oliver-black bg-oliver-azure/10 hover:bg-oliver-azure/20 px-2.5 py-1 rounded-lg transition-all"
>
Mark Resolved
</button>
) : (
<div className="flex items-center gap-2">
<span className="relative inline-block group/tooltip">
<InformationCircleIcon className="h-4 w-4 text-slate-300 cursor-help" />
<div className="absolute bottom-full right-0 mb-2 w-64 bg-slate-800 text-white text-xs rounded-lg py-2 px-3 opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-20 shadow-xl">
<span className="font-bold block mb-1">Reason for resolution:</span>
"{issue.reason}"
</div>
</span>
<button
onClick={() => handleReopen(actualIndex)}
className="text-xs font-semibold text-slate-400 hover:text-oliver-black bg-slate-100 hover:bg-slate-200 px-2.5 py-1 rounded-lg transition-all"
>
Re-open
</button>
</div>
)}
</div>
</div>
</li>
);
})}
</ul>
)}
</div>
)}
{/* New Issues Section */}
{issues.filter(i => i.category === 'new').length > 0 && (
<div className="bg-red-50/50 rounded-xl border border-red-100 overflow-hidden">
<button
onClick={() => setNewCollapsed(!newCollapsed)}
className="w-full flex items-center justify-between p-4 hover:bg-red-50/80 transition-colors"
>
<div className="flex items-center gap-2">
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
<h5 className="text-xs font-bold uppercase tracking-wider text-red-600">
New Issues
</h5>
<span className="bg-red-100 text-red-700 text-xs font-semibold px-2 py-0.5 rounded-full">
{issues.filter(i => i.category === 'new' && i.status === 'actionable').length}
</span>
</div>
<svg
className={`h-4 w-4 text-red-500 transition-transform ${newCollapsed ? '' : 'rotate-180'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{!newCollapsed && (
<ul className="px-4 pb-4 space-y-3">
{issues.filter(i => i.category === 'new').map((issue, idx) => {
const actualIndex = issues.findIndex((i, index) => i === issue && index >= 0);
return (
<li key={`new-${idx}`} className="flex items-start gap-3 group/issue">
{issue.status === 'actionable' ? (
<ExclamationTriangleIcon className="h-5 w-5 text-red-500 mt-0.5 flex-shrink-0" />
) : (
<CheckCircleIcon className="h-5 w-5 text-emerald-500 mt-0.5 flex-shrink-0" />
)}
<div className="flex-grow pt-0.5">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
<span className={`text-sm ${issue.status === 'resolved' ? 'line-through text-slate-400' : 'text-red-800'}`}>
{issue.text}
</span>
{issue.status === 'actionable' ? (
<button
onClick={() => handleOpenModal(actualIndex)}
className="flex-shrink-0 opacity-0 group-hover/issue:opacity-100 focus:opacity-100 text-xs font-semibold text-oliver-azure hover:text-oliver-black bg-oliver-azure/10 hover:bg-oliver-azure/20 px-2.5 py-1 rounded-lg transition-all"
>
Mark Resolved
</button>
) : (
<div className="flex items-center gap-2">
<span className="relative inline-block group/tooltip">
<InformationCircleIcon className="h-4 w-4 text-slate-300 cursor-help" />
<div className="absolute bottom-full right-0 mb-2 w-64 bg-slate-800 text-white text-xs rounded-lg py-2 px-3 opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-20 shadow-xl">
<span className="font-bold block mb-1">Reason for resolution:</span>
"{issue.reason}"
</div>
</span>
<button
onClick={() => handleReopen(actualIndex)}
className="text-xs font-semibold text-slate-400 hover:text-oliver-black bg-slate-100 hover:bg-slate-200 px-2.5 py-1 rounded-lg transition-all"
>
Re-open
</button>
</div>
)}
</div>
</div>
</li>
);
})}
</ul>
)}
</div>
)}
{/* No issues state for revision mode */}
{issues.length === 0 && (!review.resolvedIssues || review.resolvedIssues.length === 0) && (
<div className="flex items-center p-3 rounded-xl bg-emerald-50/50 border border-emerald-100 text-emerald-700 text-sm">
<CheckCircleIcon className="h-5 w-5 mr-2" />
<span className="font-semibold">No key actions identified.</span>
</div>
)}
</div>
)}
{/* Original mode: Show single list (backward compatible) */}
{currentStatus !== 'Error' && !hasRevisionData(review) && issues.length > 0 && (
<div className="bg-white/50 rounded-xl border border-slate-100 p-4">
<h5 className={`text-xs font-bold tracking-wider mb-3 ${issueIconColor}`}>Key Actions</h5>
<ul className="space-y-3">
{issues.map((issue, index) => (
<li key={index} className="flex items-start gap-3 group/issue">
{issue.status === 'actionable' ? (
issueIcon
) : (
<CheckCircleIcon className="h-5 w-5 text-emerald-500 mt-0.5 flex-shrink-0" />
)}
<div className="flex-grow pt-0.5">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
<span className={`text-sm ${issue.status === 'resolved' ? 'line-through text-slate-400' : 'text-slate-700'}`}>
{issue.text}
</span>
{issue.status === 'actionable' ? (
<button
onClick={() => handleOpenModal(index)}
className="flex-shrink-0 opacity-0 group-hover/issue:opacity-100 focus:opacity-100 text-xs font-semibold text-oliver-azure hover:text-oliver-black bg-oliver-azure/10 hover:bg-oliver-azure/20 px-2.5 py-1 rounded-lg transition-all"
>
Mark Resolved
</button>
) : (
<div className="flex items-center gap-2">
<span className="relative inline-block group/tooltip">
<InformationCircleIcon className="h-4 w-4 text-slate-300 cursor-help" />
<div className="absolute bottom-full right-0 mb-2 w-64 bg-slate-800 text-white text-xs rounded-lg py-2 px-3 opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-20 shadow-xl">
<span className="font-bold block mb-1">Reason for resolution:</span>
"{issue.reason}"
</div>
</span>
<button
onClick={() => handleReopen(index)}
className="text-xs font-semibold text-slate-400 hover:text-oliver-black bg-slate-100 hover:bg-slate-200 px-2.5 py-1 rounded-lg transition-all"
>
Re-open
</button>
</div>
)}
</div>
</div>
</li>
))}
</ul>
</div>
)}
{currentStatus !== 'Error' && !hasRevisionData(review) && issues.length === 0 && (
<div className="flex items-center p-3 rounded-xl bg-emerald-50/50 border border-emerald-100 text-emerald-700 text-sm">
<CheckCircleIcon className="h-5 w-5 mr-2" />
<span className="font-semibold">No key actions identified.</span>
</div>
)}
</div>
</div>
</>
);
};
const LeadAgentSummary: React.FC<{ status: OverallStatus, summary: string, onFlag: () => void; isFlagged?: boolean; }> = ({ status, summary, onFlag, isFlagged }) => {
const isPassed = status === 'Passed';
let themeStyles = 'from-sky-50 to-white border-sky-100 text-oliver-black';
let iconBg = 'bg-sky-100 text-oliver-azure';
let icon = <InformationCircleIcon className="h-8 w-8" />;
let blobColor = 'bg-sky-400';
if (status === 'Passed') {
themeStyles = 'from-emerald-50 to-white border-emerald-200 text-emerald-900';
iconBg = 'bg-emerald-100 text-emerald-600';
icon = <CheckCircleIcon className="h-8 w-8" />;
blobColor = 'bg-emerald-400';
} else if (status === 'Failed') {
themeStyles = 'from-rose-50 to-white border-rose-200 text-rose-900';
iconBg = 'bg-rose-100 text-rose-600';
icon = <ExclamationTriangleIcon className="h-8 w-8" />;
blobColor = 'bg-rose-400';
}
if (status === 'Requires Manual Legal Review') return null;
return (
<div className={`relative overflow-hidden rounded-2xl border p-8 shadow-lg transition-all duration-500 bg-gradient-to-br ${themeStyles}`}>
{/* Abstract decorative blob */}
<div className={`absolute -top-12 -right-12 w-48 h-48 rounded-full blur-3xl opacity-20 ${blobColor}`}></div>
<div className="relative z-10 flex items-start gap-6">
<div className={`flex-shrink-0 p-3 rounded-2xl ${iconBg} shadow-sm`}>
{icon}
</div>
<div className="flex-grow">
<div className="flex justify-between items-start">
<h3 className="text-2xl font-extrabold tracking-tight mb-3">
Overall Status: <span className="opacity-90">{status}</span>
</h3>
<button
onClick={onFlag}
className={`p-2 rounded-full transition-colors ${isFlagged ? 'text-red-500' : 'text-slate-400 hover:text-red-500 hover:bg-white/50'}`}
title={isFlagged ? "Flagged as incorrect" : "Flag as incorrect"}
>
<FlagIcon className="h-5 w-5" filled={isFlagged} />
</button>
</div>
<div className={`text-lg leading-relaxed ${isPassed ? 'text-emerald-800/80' : 'text-slate-700'}`}>
{formatFeedbackText(summary)}
</div>
</div>
</div>
</div>
);
};
const FinancialPromotionSummary: React.FC<{ reason: string; summary: string }> = ({ reason, summary }) => {
const [exportStatus, setExportStatus] = useState('Export for Legal');
const handleExport = () => {
const textToCopy = `## Financial Promotion Review Required ##\n\nReason for Flag:\n${reason}\n\n## AI Agent Summary ##\n${summary}`.trim();
navigator.clipboard.writeText(textToCopy).then(() => {
setExportStatus('Copied!');
setTimeout(() => setExportStatus('Export for Legal'), 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
setExportStatus('Failed');
setTimeout(() => setExportStatus('Export for Legal'), 2000);
});
};
return (
<div className="relative overflow-hidden rounded-2xl border border-purple-200 bg-gradient-to-br from-purple-50 to-white p-8 shadow-lg">
<div className="absolute -top-20 -right-20 w-64 h-64 rounded-full blur-3xl opacity-20 bg-purple-500"></div>
<div className="relative z-10 flex items-start gap-6">
<div className="flex-shrink-0 p-3 rounded-2xl bg-purple-100 text-purple-600 shadow-sm">
<LegalIcon className="h-8 w-8" />
</div>
<div className="flex-grow">
<h3 className="text-2xl font-extrabold text-purple-900 tracking-tight mb-2">
Financial Promotion Detected
</h3>
<p className="text-lg text-purple-800/80 mb-6 leading-relaxed">
This proof has been identified as a financial promotion and requires a separate, manual review from the Barclays legal team.
</p>
<div className="bg-white/60 backdrop-blur-sm border border-purple-100 rounded-xl p-4 mb-6 shadow-sm">
<p className="text-xs font-bold uppercase tracking-wider text-purple-500 mb-1">Reason Identified</p>
<p className="text-purple-900 font-medium italic">"{reason}"</p>
</div>
<button
onClick={handleExport}
className="flex items-center gap-2 bg-purple-600 text-white font-bold py-3 px-6 rounded-xl shadow-lg shadow-purple-600/20 hover:bg-purple-700 hover:shadow-purple-600/30 transition-all duration-300"
>
<ExportIcon className="h-5 w-5" />
{exportStatus}
</button>
</div>
</div>
</div>
);
};
export const FeedbackReport: React.FC<{
feedback: AgentReview;
onFlagSubmit: (agentName: string, comments: string) => void;
onResolveSubmit: (agentName: string, issueText: string, reason: string) => void;
flaggedItems?: FlaggedItem[];
resolvedItems?: ResolvedItem[];
proofName?: string;
version?: number;
}> = ({ feedback, onFlagSubmit, onResolveSubmit, flaggedItems = [], resolvedItems = [], proofName, version }) => {
const flaggedAgents = new Set(
flaggedItems
.filter(f => f.proofName === proofName && f.version === version)
.map(f => f.agentFlagged)
);
const filteredResolvedItems = resolvedItems.filter(
r => r.proofName === proofName && r.version === version
);
const [flagModalState, setFlagModalState] = useState<{ isOpen: boolean; agentName: string }>({
isOpen: false,
agentName: '',
});
const handleOpenFlagModal = (agentName: string) => {
setFlagModalState({ isOpen: true, agentName });
};
const handleCloseFlagModal = () => {
setFlagModalState({ isOpen: false, agentName: '' });
};
const handleSubmitFlag = (comments: string) => {
onFlagSubmit(flagModalState.agentName, comments);
};
const agentReviews = [
{ title: 'Legal Agent', review: feedback.legalAgentReview },
{ title: 'Brand Agent', review: feedback.brandAgentReview },
{ title: 'Channel Best Practices Agent', review: feedback.channelBestPracticesAgentReview },
{ title: 'Channel Tech Specs Agent', review: feedback.channelTechSpecsAgentReview },
];
const isFinancialPromotion = feedback.overallStatus === 'Requires Manual Legal Review';
return (
<div className="space-y-8">
<FlagIssueModal
isOpen={flagModalState.isOpen}
onClose={handleCloseFlagModal}
onSubmit={handleSubmitFlag}
agentName={flagModalState.agentName}
/>
{isFinancialPromotion ? (
<FinancialPromotionSummary
reason={feedback.financialPromotionReason || "No specific reason provided."}
summary={feedback.leadAgentSummary}
/>
) : (
<LeadAgentSummary
status={feedback.overallStatus}
summary={feedback.leadAgentSummary}
onFlag={() => handleOpenFlagModal('Lead Agent')}
isFlagged={flaggedAgents.has('Lead Agent')}
/>
)}
<div className="grid grid-cols-1 gap-6">
{agentReviews.map(({ title, review }) => (
<SubReviewCard
key={title}
title={`${title}`}
review={review}
onFlag={() => handleOpenFlagModal(title)}
onResolve={(issueText, reason) => onResolveSubmit(title, issueText, reason)}
isFlagged={flaggedAgents.has(title)}
resolvedItems={filteredResolvedItems.filter(r => r.agent === title)}
/>
))}
</div>
</div>
);
};