ACE / components /ResultsTable.tsx
Severian's picture
Update components/ResultsTable.tsx
02b914b verified
raw
history blame
35.3 kB
import React, { useState, useMemo, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import { ResultRow, DetailedQaReport, QaSectionResult } from '../types';
import { DownloadIcon, CheckCircleIcon, XCircleIcon, EyeIcon } from './Icons';
import jsPDF from 'jspdf';
import { Document, Packer, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, WidthType, AlignmentType, BorderStyle } from 'docx';
// This tells TypeScript that `Papa` is available on the global window object.
declare const Papa: any;
interface ResultsTableProps {
results: ResultRow[];
}
type SortConfig = {
key: keyof ResultRow;
direction: 'ascending' | 'descending';
} | null;
const QAReportModal: React.FC<{ report: DetailedQaReport; onClose: () => void }> = ({ report, onClose }) => {
const Section: React.FC<{ title: string; data: QaSectionResult }> = ({ title, data }) => (
<div className="bg-gray-900/70 p-4 rounded-lg mb-4 border border-gray-700">
<h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-3">
{data.pass
? <CheckCircleIcon className="w-6 h-6 text-green-400 flex-shrink-0" />
: <XCircleIcon className="w-6 h-6 text-red-400 flex-shrink-0" />
}
{title}
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 text-sm pl-9">
<p><span className="font-semibold text-gray-400">Grade:</span> <span className="text-gray-200">{data.grade}</span></p>
<p><span className="font-semibold text-gray-400">Pass:</span> <span className={`font-medium ${data.pass ? 'text-green-400' : 'text-red-400'}`}>{data.pass ? 'Yes' : 'No'}</span></p>
</div>
<div className="pl-9 mt-3 text-sm">
<p className="font-semibold text-gray-400">Errors:</p>
<ul className="list-disc list-inside text-gray-300 pl-2 mt-1 space-y-1">
{data.errors.map((err, i) => <li key={i}>{err}</li>)}
</ul>
</div>
<div className="pl-9 mt-3 text-sm">
<p className="font-semibold text-gray-400">Content:</p>
<div className="text-gray-300 text-xs mt-1 bg-gray-950/50 p-3 rounded-md border border-gray-600/50 overflow-auto max-h-48">
<div className="prose prose-sm prose-invert max-w-none">
<ReactMarkdown
components={{
// Custom styling for markdown elements
p: ({ children }) => <p className="mb-2 last:mb-0 text-gray-300">{children}</p>,
ul: ({ children }) => <ul className="list-disc ml-4 mb-2 text-gray-300">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal ml-4 mb-2 text-gray-300">{children}</ol>,
li: ({ children }) => <li className="mb-1 text-gray-300">{children}</li>,
code: ({ children }) => <code className="bg-gray-800 px-1 py-0.5 rounded text-blue-300 text-xs">{children}</code>,
strong: ({ children }) => <strong className="font-semibold text-white">{children}</strong>,
em: ({ children }) => <em className="italic text-gray-200">{children}</em>,
h1: ({ children }) => <h1 className="text-lg font-bold mb-2 text-white">{children}</h1>,
h2: ({ children }) => <h2 className="text-base font-semibold mb-2 text-white">{children}</h2>,
h3: ({ children }) => <h3 className="text-sm font-semibold mb-1 text-white">{children}</h3>,
}}
>
{data.corrected}
</ReactMarkdown>
</div>
</div>
</div>
</div>
);
return (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50" onClick={onClose}>
<div className="bg-gray-800 rounded-lg shadow-xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-4 pb-4 border-b border-gray-700">
<h3 className="text-2xl font-bold text-white">QA Report Details</h3>
<button onClick={onClose} className="text-gray-400 hover:text-white text-3xl font-bold">&times;</button>
</div>
<div className="space-y-2">
<Section title="Title" data={report.title} />
<Section title="Meta Description" data={report.meta} />
<Section title="H1" data={report.h1} />
<Section title="Copy" data={report.copy} />
</div>
</div>
</div>
);
};
const ResultsTable: React.FC<ResultsTableProps> = ({ results }) => {
const [sortConfig, setSortConfig] = useState<SortConfig>(null);
const [selectedReport, setSelectedReport] = useState<DetailedQaReport | null>(null);
const [showDownloadMenu, setShowDownloadMenu] = useState(false);
const downloadMenuRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (downloadMenuRef.current && !downloadMenuRef.current.contains(event.target as Node)) {
setShowDownloadMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleDownloadCSV = () => {
if (results.length === 0) return;
const csvData = results.map(row => {
const report = row.detailedQaReport;
return {
'URL': row.URL,
'Page': row.Page,
'Keywords': row.Keywords,
'Original Title': row.Recommended_Title,
'Original H1': row.Recommended_H1,
'Original Copy': row.Copy,
'Internal Links': row.Internal_Links,
'Generated Title': row.generatedTitle,
'Generated H1': row.generatedH1,
'Generated Meta': row.generatedMeta,
'Generated Copy': row.generatedCopy,
'Overall Pass': row.overallPass,
'Overall Grade': row.overallGrade,
'Title Pass': report?.title.pass,
'Title Grade': report?.title.grade,
'Title Errors': report?.title.errors.join('; '),
'Meta Pass': report?.meta.pass,
'Meta Grade': report?.meta.grade,
'Meta Errors': report?.meta.errors.join('; '),
'H1 Pass': report?.h1.pass,
'H1 Grade': report?.h1.grade,
'H1 Errors': report?.h1.errors.join('; '),
'Copy Pass': report?.copy.pass,
'Copy Grade': report?.copy.grade,
'Copy Errors': report?.copy.errors.join('; '),
'QA Full Report': row.qaReport,
};
});
const csv = Papa.unparse(csvData);
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'ace_copywriting_results.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setShowDownloadMenu(false);
};
const handleDownloadPDF = () => {
if (results.length === 0) return;
const pdf = new jsPDF();
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const margin = 20;
const maxLineWidth = pageWidth - (margin * 2);
let currentY = margin;
const lineHeight = 6;
const sectionSpacing = 10;
// Helper function to add text with word wrapping and automatic page breaks
const addWrappedText = (text: string, x: number, y: number, maxWidth: number, fontSize: number = 12): number => {
pdf.setFontSize(fontSize);
const lines = pdf.splitTextToSize(text, maxWidth);
let currentLineY = y;
for (let i = 0; i < lines.length; i++) {
// Check if we need a new page for this line
if (currentLineY + lineHeight > pageHeight - margin) {
pdf.addPage();
currentLineY = margin;
}
pdf.text(lines[i], x, currentLineY);
currentLineY += lineHeight;
}
return currentLineY;
};
// Helper function to ensure minimum space on page for section headers
const ensureMinSpace = (minHeight: number = 40): number => {
if (currentY + minHeight > pageHeight - margin) {
pdf.addPage();
return margin;
}
return currentY;
};
// Title
pdf.setFontSize(20);
pdf.setFont('helvetica', 'bold');
pdf.text('ACE Copywriting Pipeline Results', margin, currentY);
currentY += 15;
// Summary
pdf.setFontSize(12);
pdf.setFont('helvetica', 'normal');
const totalResults = results.length;
const passedResults = results.filter(r => r.overallPass).length;
const summaryText = `Generated ${totalResults} results | ${passedResults} passed QA | ${totalResults - passedResults} failed QA`;
currentY = addWrappedText(summaryText, margin, currentY, maxLineWidth);
currentY += sectionSpacing;
// Results
results.forEach((row, index) => {
// Estimate space needed for this entry based on content length
const estimatedHeight = 50 + // Base height for headers and metadata
Math.ceil((row.generatedTitle?.length || 0) / 80) * 6 + // Title
Math.ceil((row.generatedH1?.length || 0) / 80) * 6 + // H1
Math.ceil((row.generatedMeta?.length || 0) / 80) * 6 + // Meta
Math.ceil((row.generatedCopy?.length || 0) / 100) * 5; // Copy (smaller font)
// Ensure minimum space for section header
currentY = ensureMinSpace(40);
// Page/URL Header
pdf.setFontSize(14);
pdf.setFont('helvetica', 'bold');
currentY = addWrappedText(`${index + 1}. ${row.Page || 'Page'} (${row.URL})`, margin, currentY, maxLineWidth, 14);
currentY += 5;
// Keywords
pdf.setFontSize(10);
pdf.setFont('helvetica', 'normal');
currentY = addWrappedText(`Keywords: ${row.Keywords}`, margin, currentY, maxLineWidth, 10);
currentY += 3;
// Overall QA Status
pdf.setFont('helvetica', 'bold');
if (row.overallPass) {
pdf.setTextColor(0, 128, 0);
} else {
pdf.setTextColor(255, 0, 0);
}
currentY = addWrappedText(`Overall QA: ${row.overallPass ? 'PASS' : 'FAIL'} (${row.overallGrade})`, margin, currentY, maxLineWidth, 10);
pdf.setTextColor(0, 0, 0);
currentY += 5;
// Generated Content
pdf.setFont('helvetica', 'bold');
currentY = addWrappedText('Generated Title:', margin, currentY, maxLineWidth, 10);
pdf.setFont('helvetica', 'normal');
currentY = addWrappedText(row.generatedTitle || '', margin, currentY, maxLineWidth, 9);
currentY += 3;
pdf.setFont('helvetica', 'bold');
currentY = addWrappedText('Generated H1:', margin, currentY, maxLineWidth, 10);
pdf.setFont('helvetica', 'normal');
currentY = addWrappedText(row.generatedH1 || '', margin, currentY, maxLineWidth, 9);
currentY += 3;
pdf.setFont('helvetica', 'bold');
currentY = addWrappedText('Generated Meta:', margin, currentY, maxLineWidth, 10);
pdf.setFont('helvetica', 'normal');
currentY = addWrappedText(row.generatedMeta || '', margin, currentY, maxLineWidth, 9);
currentY += 3;
// Generated Copy (full content)
pdf.setFont('helvetica', 'bold');
currentY = addWrappedText('Generated Copy:', margin, currentY, maxLineWidth, 10);
pdf.setFont('helvetica', 'normal');
// Add the full copy content with proper spacing
if (row.generatedCopy) {
currentY = addWrappedText(row.generatedCopy, margin, currentY, maxLineWidth, 9);
}
// Add QA Details if available
if (row.detailedQaReport) {
currentY = ensureMinSpace(60); // Ensure space for QA header + at least one section
currentY += 5;
pdf.setFontSize(12);
pdf.setFont('helvetica', 'bold');
currentY = addWrappedText('QA Report Details:', margin + 5, currentY, maxLineWidth - 5, 12);
currentY += 2;
const addQaSection = (title: string, section: QaSectionResult) => {
currentY = ensureMinSpace(30);
pdf.setFontSize(10);
pdf.setFont('helvetica', 'bold');
if (section.pass) {
pdf.setTextColor(0, 128, 0); // Green for PASS
} else {
pdf.setTextColor(255, 0, 0); // Red for FAIL
}
currentY = addWrappedText(`${title}: ${section.pass ? 'PASS' : 'FAIL'} (Grade: ${section.grade})`, margin + 5, currentY, maxLineWidth - 5, 10);
pdf.setTextColor(0, 0, 0); // Reset color
pdf.setFont('helvetica', 'normal');
currentY = addWrappedText(`Errors: ${section.errors.join(', ')}`, margin + 10, currentY, maxLineWidth - 10, 8);
currentY = ensureMinSpace(20);
pdf.setFont('helvetica', 'italic');
currentY = addWrappedText(`Correction/Analysis: ${section.corrected}`, margin + 10, currentY, maxLineWidth - 10, 8);
currentY += 4;
};
addQaSection('Title', row.detailedQaReport.title);
addQaSection('Meta', row.detailedQaReport.meta);
addQaSection('H1', row.detailedQaReport.h1);
addQaSection('Copy', row.detailedQaReport.copy);
}
currentY += sectionSpacing * 2; // Extra spacing between entries
});
// Footer on last page
pdf.setFontSize(8);
pdf.setTextColor(128, 128, 128);
pdf.text('Generated by ACE Copywriting Pipeline', margin, pageHeight - 10);
// Save the PDF
pdf.save('ace_copywriting_results.pdf');
setShowDownloadMenu(false);
};
const handleDownloadJSON = () => {
if (results.length === 0) return;
const jsonData = {
metadata: {
generatedAt: new Date().toISOString(),
totalResults: results.length,
passedResults: results.filter(r => r.overallPass).length,
failedResults: results.filter(r => !r.overallPass).length
},
results: results.map(row => ({
url: row.URL,
page: row.Page,
keywords: row.Keywords,
original: {
title: row.Recommended_Title,
h1: row.Recommended_H1,
copy: row.Copy,
internalLinks: row.Internal_Links
},
generated: {
title: row.generatedTitle,
h1: row.generatedH1,
meta: row.generatedMeta,
copy: row.generatedCopy
},
qa: {
overallPass: row.overallPass,
overallGrade: row.overallGrade,
sections: {
title: {
pass: row.detailedQaReport?.title.pass,
grade: row.detailedQaReport?.title.grade,
errors: row.detailedQaReport?.title.errors
},
meta: {
pass: row.detailedQaReport?.meta.pass,
grade: row.detailedQaReport?.meta.grade,
errors: row.detailedQaReport?.meta.errors
},
h1: {
pass: row.detailedQaReport?.h1.pass,
grade: row.detailedQaReport?.h1.grade,
errors: row.detailedQaReport?.h1.errors
},
copy: {
pass: row.detailedQaReport?.copy.pass,
grade: row.detailedQaReport?.copy.grade,
errors: row.detailedQaReport?.copy.errors
}
},
fullReport: row.qaReport
}
}))
};
const jsonString = JSON.stringify(jsonData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'ace_copywriting_results.json');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setShowDownloadMenu(false);
};
const handleDownloadDOCX = async () => {
if (results.length === 0) return;
// Helper function to clean HTML tags and format text
const cleanText = (text: string): string => {
if (!text) return '';
return text
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove markdown bold
.replace(/\*([^*]+)\*/g, '$1') // Remove markdown italic
.replace(/---/g, '') // Remove markdown separators
.replace(/\n\s*\n/g, '\n') // Remove extra line breaks
.trim();
};
// Helper function to split text into paragraphs
const splitIntoParagraphs = (text: string): string[] => {
const cleaned = cleanText(text);
return cleaned
.split(/\n+/)
.map(p => p.trim())
.filter(p => p.length > 0);
};
const children: any[] = [];
// Title
children.push(
new Paragraph({
text: "ACE Copywriting Pipeline Results",
heading: HeadingLevel.HEADING_1,
alignment: AlignmentType.CENTER,
spacing: { after: 400 }
})
);
// Summary
const totalResults = results.length;
const passedResults = results.filter(r => r.overallPass).length;
const summaryText = `Generated ${totalResults} results | ${passedResults} passed QA | ${totalResults - passedResults} failed QA`;
children.push(
new Paragraph({
text: summaryText,
spacing: { after: 400 }
})
);
// Results
results.forEach((row, index) => {
// Page/URL Header
children.push(
new Paragraph({
text: `${index + 1}. ${row.Page || 'Page'} (${row.URL})`,
heading: HeadingLevel.HEADING_2,
spacing: { before: 400, after: 200 }
})
);
// Keywords
children.push(
new Paragraph({
text: `Keywords: ${row.Keywords}`,
spacing: { after: 200 }
})
);
// Overall QA Status
children.push(
new Paragraph({
children: [
new TextRun({
text: `Overall QA: ${row.overallPass ? 'PASS' : 'FAIL'} (${row.overallGrade})`,
color: row.overallPass ? '008000' : 'FF0000',
bold: true
})
],
spacing: { after: 200 }
})
);
// Generated Content
children.push(
new Paragraph({
text: "Generated Title:",
heading: HeadingLevel.HEADING_3,
spacing: { before: 300, after: 100 }
}),
new Paragraph({
text: cleanText(row.generatedTitle || ''),
spacing: { after: 200 }
}),
new Paragraph({
text: "Generated H1:",
heading: HeadingLevel.HEADING_3,
spacing: { before: 300, after: 100 }
}),
new Paragraph({
text: cleanText(row.generatedH1 || ''),
spacing: { after: 200 }
}),
new Paragraph({
text: "Generated Meta:",
heading: HeadingLevel.HEADING_3,
spacing: { before: 300, after: 100 }
}),
new Paragraph({
text: cleanText(row.generatedMeta || ''),
spacing: { after: 200 }
}),
new Paragraph({
text: "Generated Copy:",
heading: HeadingLevel.HEADING_3,
spacing: { before: 300, after: 100 }
})
);
// Handle generated copy with proper paragraph breaks
if (row.generatedCopy) {
const copyParagraphs = splitIntoParagraphs(row.generatedCopy);
copyParagraphs.forEach(paragraph => {
children.push(
new Paragraph({
text: paragraph,
spacing: { after: 150 }
})
);
});
}
// QA Details if available
if (row.detailedQaReport) {
children.push(
new Paragraph({
text: "QA Report Details:",
heading: HeadingLevel.HEADING_3,
spacing: { before: 400, after: 200 }
})
);
const addQaSection = (title: string, section: QaSectionResult) => {
children.push(
new Paragraph({
children: [
new TextRun({
text: `${title}: ${section.pass ? 'PASS' : 'FAIL'} (Grade: ${section.grade})`,
color: section.pass ? '008000' : 'FF0000',
bold: true
})
],
heading: HeadingLevel.HEADING_4,
spacing: { before: 300, after: 100 }
}),
new Paragraph({
text: `Errors: ${section.errors.join(', ')}`,
spacing: { after: 100 }
})
);
// Handle correction/analysis with proper formatting
if (section.corrected && section.corrected.trim()) {
const correctedText = cleanText(section.corrected);
if (correctedText && correctedText !== 'Content analysis not available.') {
children.push(
new Paragraph({
children: [
new TextRun({
text: `Correction/Analysis: ${correctedText}`,
italics: true
})
],
spacing: { after: 200 }
})
);
}
}
};
addQaSection('Title', row.detailedQaReport.title);
addQaSection('Meta', row.detailedQaReport.meta);
addQaSection('H1', row.detailedQaReport.h1);
addQaSection('Copy', row.detailedQaReport.copy);
}
// Add spacing between entries
children.push(
new Paragraph({
text: "",
spacing: { after: 400 }
})
);
});
// Footer
children.push(
new Paragraph({
children: [
new TextRun({
text: "Generated by ACE Copywriting Pipeline",
color: '808080',
size: 16
})
],
alignment: AlignmentType.CENTER,
spacing: { before: 400 }
})
);
const doc = new Document({
sections: [{
properties: {},
children: children
}]
});
const blob = await Packer.toBlob(doc);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', 'ace_copywriting_results.docx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
setShowDownloadMenu(false);
};
const sortedResults = useMemo(() => {
let sortableItems = [...results];
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
const key = sortConfig.key;
const valA = a[key as keyof typeof a];
const valB = b[key as keyof typeof b];
if (typeof valA === 'boolean' && typeof valB === 'boolean') {
if (valA === valB) return 0;
return sortConfig.direction === 'ascending' ? (valA ? -1 : 1) : (valA ? 1 : -1);
}
if (valA < valB) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (valA > valB) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
}
return sortableItems;
}, [results, sortConfig]);
const requestSort = (key: keyof ResultRow) => {
let direction: 'ascending' | 'descending' = 'ascending';
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({ key, direction });
};
const getSortIndicator = (key: keyof ResultRow) => {
if (!sortConfig || sortConfig.key !== key) {
return ' ↕';
}
return sortConfig.direction === 'ascending' ? ' ▲' : ' ▼';
};
if (results.length === 0) {
return (
<div className="bg-gray-800 rounded-xl shadow-lg p-6 text-center">
<h2 className="text-2xl font-semibold text-white">Generated Content</h2>
<p className="mt-4 text-gray-400">No results to display yet. Process a file to see the output here.</p>
</div>
)
}
return (
<>
<div className="bg-gray-800 rounded-xl shadow-lg p-6 w-full">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-semibold text-white">Generated Content</h2>
<div className="relative" ref={downloadMenuRef}>
<button
onClick={() => setShowDownloadMenu(!showDownloadMenu)}
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200"
>
<DownloadIcon className="w-5 h-5"/>
Download
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showDownloadMenu && (
<div className="absolute right-0 mt-2 w-48 bg-gray-700 rounded-lg shadow-lg z-10 border border-gray-600">
<div className="py-1">
<button
onClick={handleDownloadCSV}
className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
>
📊 Download CSV
</button>
<button
onClick={handleDownloadPDF}
className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
>
📄 Download PDF
</button>
<button
onClick={handleDownloadJSON}
className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
>
💾 Download JSON
</button>
<button
onClick={handleDownloadDOCX}
className="w-full text-left px-4 py-2 text-white hover:bg-gray-600 flex items-center gap-2"
>
📝 Download DOCX
</button>
</div>
</div>
)}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left text-gray-300">
<thead className="text-xs text-gray-400 uppercase bg-gray-700">
<tr>
<th scope="col" className="px-6 py-3 cursor-pointer" onClick={() => requestSort('URL')}>URL{getSortIndicator('URL')}</th>
<th scope="col" className="px-6 py-3">Generated Title</th>
<th scope="col" className="px-6 py-3">Generated H1</th>
<th scope="col" className="px-6 py-3 cursor-pointer" onClick={() => requestSort('overallPass')}>Overall Pass{getSortIndicator('overallPass')}</th>
<th scope="col" className="px-6 py-3 text-center">Details</th>
</tr>
</thead>
<tbody>
{sortedResults
.filter((row, index, arr) => arr.findIndex(r => r.id === row.id) === index) // Remove any runtime duplicates
.map((row) => (
<tr key={row.id} className="bg-gray-800 border-b border-gray-700 hover:bg-gray-700/50">
<td className="px-6 py-4 font-medium text-white max-w-xs truncate" title={row.URL}>{row.URL}</td>
<td className="px-6 py-4 max-w-xs truncate" title={row.generatedTitle}>{row.generatedTitle}</td>
<td className="px-6 py-4 max-w-xs truncate" title={row.generatedH1}>{row.generatedH1}</td>
<td className="px-6 py-4">
<div className="flex justify-center">
{row.overallPass
? <CheckCircleIcon className="w-6 h-6 text-green-400" />
: <XCircleIcon className="w-6 h-6 text-red-400" />
}
</div>
</td>
<td className="px-6 py-4 text-center">
<button
onClick={() => {
console.log('Opening QA modal with data:', row.detailedQaReport);
setSelectedReport(row.detailedQaReport || null);
}}
disabled={!row.detailedQaReport}
className="flex items-center justify-center mx-auto gap-1 text-blue-400 hover:text-blue-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-colors"
>
<EyeIcon className="w-5 h-5" /> View
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{selectedReport && <QAReportModal report={selectedReport} onClose={() => setSelectedReport(null)} />}
</>
);
};
export default ResultsTable;