Add categorized issue display for revision analysis

When analyzing proof revisions (version > 1), SubReviewCard now displays
issues in three distinct categories: Resolved Issues (green, collapsed by
default), Outstanding Issues (amber), and New Issues (red). Each section
is collapsible with count badges. Original mode (version 1) maintains
backward compatibility with the single "Actionable Issues" list.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-25 10:39:42 -06:00
parent bf1d689f42
commit e5a841716e

View file

@ -241,9 +241,18 @@ const FlagIssueModal: React.FC<{
);
};
const SubReviewCard: React.FC<{
title: string;
review: SubReview;
// 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;
}> = ({ title, review, onFlag, onResolve }) => {
@ -251,15 +260,39 @@ const SubReviewCard: React.FC<{
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(() => {
setIssues(review.issues.map(text => ({ text, status: 'actionable', reason: undefined })));
if (hasRevisionData(review)) {
// Revision mode: populate from outstandingIssues and newIssues
const outstandingIssues: IssueState[] = (review.outstandingIssues || []).map(text => ({
text,
status: 'actionable',
reason: undefined,
category: 'outstanding' as const
}));
const newIssues: IssueState[] = (review.newIssues || []).map(text => ({
text,
status: 'actionable',
reason: undefined,
category: 'new' as const
}));
setIssues([...outstandingIssues, ...newIssues]);
} else {
// Original mode: use review.issues
setIssues(review.issues.map(text => ({ text, status: 'actionable', reason: undefined })));
}
setCurrentStatus(review.ragStatus);
}, [review]);
@ -371,7 +404,211 @@ const SubReviewCard: React.FC<{
{formatFeedbackText(review.feedback)}
</div>
{currentStatus !== 'Error' && issues.length > 0 && (
{/* 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-brand-accent hover:text-brand-dark-blue bg-brand-light-blue/10 hover:bg-brand-light-blue/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-brand-dark-blue 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-brand-accent hover:text-brand-dark-blue bg-brand-light-blue/10 hover:bg-brand-light-blue/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-brand-dark-blue 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 actionable issues found.</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 uppercase tracking-wider mb-3 ${issueIconColor}`}>Actionable Issues</h5>
<ul className="space-y-3">
@ -418,7 +655,7 @@ const SubReviewCard: React.FC<{
</ul>
</div>
)}
{currentStatus !== 'Error' && issues.length === 0 && (
{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 actionable issues found.</span>