Fix feedback report formatting by parsing HTML and bullet text

Add formatFeedbackText() utility that converts raw HTML tags and
concatenated bullet points from Gemini API into properly formatted
React elements with proper line breaks and list styling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-25 08:19:28 -06:00
parent 907c3a520e
commit 17495d4291

View file

@ -7,6 +7,72 @@ 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.
*/
const formatFeedbackText = (text: string): React.ReactNode => {
if (!text) return null;
// First, handle HTML tags by converting them to a normalized format
let normalizedText = text
// 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
.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
const bulletLines: string[] = [];
const introLines: string[] = [];
lines.forEach(line => {
const trimmed = line.trim();
if (trimmed.startsWith('•')) {
// Remove the bullet character, we'll add it via CSS
bulletLines.push(trimmed.replace(/^•\s*/, '').trim());
} else if (trimmed) {
// If we haven't started collecting bullets yet, it's intro text
if (bulletLines.length === 0) {
introLines.push(trimmed);
} else {
// Text after bullets - treat as continuation of last bullet
const lastBullet = bulletLines.pop();
if (lastBullet) {
bulletLines.push(`${lastBullet} ${trimmed}`);
} else {
bulletLines.push(trimmed);
}
}
}
});
return (
<>
{introLines.length > 0 && (
<p className="mb-3">{introLines.join(' ')}</p>
)}
{bulletLines.length > 0 && (
<ul className="list-disc list-inside space-y-1">
{bulletLines.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)}
</>
);
};
const RagStatusBadge: React.FC<{ status: RagStatus; isLarge?: boolean }> = ({ status, isLarge = false }) => {
let colorClasses = '';
@ -301,7 +367,9 @@ const SubReviewCard: React.FC<{
</div>
</div>
<p className="text-slate-600 text-sm leading-relaxed mb-6">{review.feedback}</p>
<div className="text-slate-600 text-sm leading-relaxed mb-6">
{formatFeedbackText(review.feedback)}
</div>
{currentStatus !== 'Error' && issues.length > 0 && (
<div className="bg-white/50 rounded-xl border border-slate-100 p-4">
@ -406,9 +474,9 @@ const LeadAgentSummary: React.FC<{ status: OverallStatus, summary: string, onFla
<FlagIcon className="h-5 w-5" />
</button>
</div>
<p className={`text-lg leading-relaxed ${isPassed ? 'text-emerald-800/80' : 'text-slate-700'}`}>
{summary}
</p>
<div className={`text-lg leading-relaxed ${isPassed ? 'text-emerald-800/80' : 'text-slate-700'}`}>
{formatFeedbackText(summary)}
</div>
</div>
</div>
</div>