Spaces:
Sleeping
Sleeping
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">×</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; |