From 17495d429121025f4017d38744d5f2e73a490105 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 25 Jan 2026 08:19:28 -0600 Subject: [PATCH] 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 --- frontend/components/FeedbackReport.tsx | 76 ++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/frontend/components/FeedbackReport.tsx b/frontend/components/FeedbackReport.tsx index 1de2908..955a3a2 100755 --- a/frontend/components/FeedbackReport.tsx +++ b/frontend/components/FeedbackReport.tsx @@ -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 and with newlines + .replace(/<\/li>/gi, '\n') + .replace(/<\/ul>/gi, '\n') + // Remove opening tags + .replace(/]*>/gi, '') + .replace(/]*>/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 && ( +

{introLines.join(' ')}

+ )} + {bulletLines.length > 0 && ( +
    + {bulletLines.map((item, index) => ( +
  • {item}
  • + ))} +
+ )} + + ); +}; const RagStatusBadge: React.FC<{ status: RagStatus; isLarge?: boolean }> = ({ status, isLarge = false }) => { let colorClasses = ''; @@ -301,7 +367,9 @@ const SubReviewCard: React.FC<{ -

{review.feedback}

+
+ {formatFeedbackText(review.feedback)} +
{currentStatus !== 'Error' && issues.length > 0 && (
@@ -406,9 +474,9 @@ const LeadAgentSummary: React.FC<{ status: OverallStatus, summary: string, onFla
-

- {summary} -

+
+ {formatFeedbackText(summary)} +