From b733bb0e4e412c871eadc09186cd0cffa3a3072e Mon Sep 17 00:00:00 2001 From: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> Date: Fri, 26 Jun 2026 07:31:11 +0000 Subject: [PATCH 1/2] feat: implement boundary-aware PDF slicing and tighten report template prose - Add data-pdf-nosplit markers to key images in Section 01 and 02 of the report template. - Implement a boundary-aware slicing loop in generatePDFReport that prevents splitting protected elements across pages. - Normalize Markdown content by collapsing redundant newlines and trimming whitespace. - Implement a custom p renderer for ReactMarkdown to skip empty paragraphs. - Tighten paragraph spacing with prose-p:my-1 Tailwind utilities. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- components/report-template.tsx | 40 +++++++++---- lib/utils/report-generator.ts | 104 +++++++++++++++++++++------------ 2 files changed, 98 insertions(+), 46 deletions(-) diff --git a/components/report-template.tsx b/components/report-template.tsx index b3ac6977..8a723cbf 100644 --- a/components/report-template.tsx +++ b/components/report-template.tsx @@ -23,6 +23,24 @@ export const ReportTemplate: React.FC = ({ chatTitle, aiSummary }) => { + const normalizeMarkdown = (content: string): string => { + return content + .replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2 + .trim() + } + + const MarkdownComponents = { + p: ({ children }: { children: React.ReactNode }) => { + if (!children || (typeof children === 'string' && !children.trim())) { + return null + } + if (Array.isArray(children) && children.every(child => typeof child === 'string' && !child.trim())) { + return null + } + return

{children}

+ } + } + const renderMessageContent = (content: any): string => { if (typeof content === 'string') { return content @@ -113,9 +131,9 @@ export const ReportTemplate: React.FC = ({

Intelligence Executive Summary

-
- - {aiSummary} +
+ + {normalizeMarkdown(aiSummary)}
@@ -128,7 +146,7 @@ export const ReportTemplate: React.FC = ({ 01

Executive Visualization

-
+
Map Snapshot
ORBITAL PERSPECTIVE REF: {Math.random().toString(16).substring(2, 10).toUpperCase()} @@ -167,14 +185,14 @@ export const ReportTemplate: React.FC = ({ ) } else if (message.type === 'response') { return ( -
+

Strategic Output

- - {contentString} + + {normalizeMarkdown(contentString)}
@@ -190,12 +208,14 @@ export const ReportTemplate: React.FC = ({
{result.summary && (
- {result.summary} + + {normalizeMarkdown(result.summary)} +
)}
{result.mapboxImage && ( -
+

Spectral RGB Analysis View

@@ -204,7 +224,7 @@ export const ReportTemplate: React.FC = ({
)} {result.googleImage && ( -
+

High-Resolution Panchromatic Satellite

diff --git a/lib/utils/report-generator.ts b/lib/utils/report-generator.ts index f8db66d0..0def08c3 100644 --- a/lib/utils/report-generator.ts +++ b/lib/utils/report-generator.ts @@ -1,8 +1,8 @@ export const generatePDFReport = async (elementId: string, fileName: string) => { const element = document.getElementById(elementId) if (!element) { - console.error(`Element with id ${elementId} not found in the DOM. Full document structure:`, document.body.innerHTML.substring(0, 500)) - throw new Error(`Element with id ${elementId} not found. Please try again.`) + console.error(`Element with id ${elementId} not found in the DOM.`) + throw new Error(`Element with id ${elementId} not found.`) } try { @@ -12,17 +12,15 @@ export const generatePDFReport = async (elementId: string, fileName: string) => ]) const images = Array.from(element.getElementsByTagName('img')) - const imageLoadTimeout = 10000 // 10 seconds timeout for high-res images + const imageLoadTimeout = 10000 - // Wait for images to load, but don't block forever await Promise.race([ Promise.all( images.map(img => { if (img.complete) return Promise.resolve() return new Promise((resolve) => { img.onload = resolve - img.onerror = resolve // Continue even if one image fails - // Force a check after a small delay for data URLs + img.onerror = resolve if (img.src.startsWith('data:')) setTimeout(resolve, 500) }) }) @@ -30,55 +28,89 @@ export const generatePDFReport = async (elementId: string, fileName: string) => new Promise(resolve => setTimeout(resolve, imageLoadTimeout)) ]) + const templateWidth = 800 // The width we force for PDF generation + + // To get accurate positions for the 800px width used in the PDF, + // we temporarily set the element width. + const originalStyle = element.getAttribute('style') || '' + element.style.width = `${templateWidth}px` + element.style.position = 'relative' + + const templateRect = element.getBoundingClientRect() + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'px', + format: 'a4' + }) + + const pdfWidth = pdf.internal.pageSize.getWidth() + const pdfHeight = pdf.internal.pageSize.getHeight() + const pdfScale = pdfWidth / templateWidth + + const protectedRanges = Array.from(element.querySelectorAll('[data-pdf-nosplit]')) + .map(el => { + const rect = el.getBoundingClientRect() + return { + top: (rect.top - templateRect.top) * pdfScale, + bottom: (rect.bottom - templateRect.top) * pdfScale + } + }) + .sort((a, b) => a.top - b.top) + + // Restore original style + element.setAttribute('style', originalStyle) + const canvas = await html2canvas(element, { - scale: 3, // Increased scale for ultra-sharp text and images + scale: 3, useCORS: true, logging: false, allowTaint: true, backgroundColor: '#ffffff', imageTimeout: 15000, onclone: (clonedDoc) => { - // Ensure the cloned element is visible for html2canvas const clonedElement = clonedDoc.getElementById(elementId) if (clonedElement) { clonedElement.style.position = 'static' clonedElement.style.left = '0' clonedElement.style.visibility = 'visible' - clonedElement.style.width = '800px' // Fix width for consistent scaling + clonedElement.style.width = `${templateWidth}px` } } }) - const imgData = canvas.toDataURL('image/png') // Use PNG for lossless quality and sharper text + const imgData = canvas.toDataURL('image/png') + const imgProps = pdf.getImageProperties(imgData) + const scaledHeight = pdfWidth * (imgProps.height / imgProps.width) - // A4 dimensions in px at 72 DPI are roughly 595 x 842 - // But we use the internal pageSize values for flexibility - const pdf = new jsPDF({ - orientation: 'portrait', - unit: 'px', - format: 'a4' - }) + let currentPosition = 0 - const pdfWidth = pdf.internal.pageSize.getWidth() - const pdfHeight = pdf.internal.pageSize.getHeight() + while (currentPosition < scaledHeight - 0.5) { + if (currentPosition > 0) { + pdf.addPage() + } - const imgProps = pdf.getImageProperties(imgData) - const ratio = imgProps.height / imgProps.width - const scaledHeight = pdfWidth * ratio - - let heightLeft = scaledHeight - let position = 0 - - // Add first page - pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, scaledHeight, undefined, 'FAST') - heightLeft -= pdfHeight - - // Add subsequent pages if content overflows - while (heightLeft > 0) { - position = heightLeft - scaledHeight - pdf.addPage() - pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, scaledHeight, undefined, 'FAST') - heightLeft -= pdfHeight + let nextPossibleCut = currentPosition + pdfHeight + let effectiveCut = nextPossibleCut + + if (nextPossibleCut < scaledHeight) { + const straddle = protectedRanges.find(range => + nextPossibleCut > range.top && nextPossibleCut < range.bottom + ) + + // Only move the cut if the element starts after our current position (with small buffer) + // and fits on a single page. + if (straddle && straddle.top > currentPosition + 10) { + const elementHeight = straddle.bottom - straddle.top + if (elementHeight <= pdfHeight) { + effectiveCut = straddle.top + } + } + } else { + effectiveCut = scaledHeight + } + + pdf.addImage(imgData, 'PNG', 0, -currentPosition, pdfWidth, scaledHeight, undefined, 'FAST') + currentPosition = effectiveCut } pdf.save(`${fileName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_report.pdf`) From ce6e2cc56dcec0c764cf6455ead8b6163a32a576 Mon Sep 17 00:00:00 2001 From: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:03:49 +0000 Subject: [PATCH 2/2] fix: resolve TypeScript error in custom paragraph renderer - Update `MarkdownComponents` in `components/report-template.tsx` to make `children` optional. - This ensures compatibility with `react-markdown`'s `Components` type and resolves the build failure. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- components/report-template.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/report-template.tsx b/components/report-template.tsx index 8a723cbf..d494c694 100644 --- a/components/report-template.tsx +++ b/components/report-template.tsx @@ -30,7 +30,7 @@ export const ReportTemplate: React.FC = ({ } const MarkdownComponents = { - p: ({ children }: { children: React.ReactNode }) => { + p: ({ children }: { children?: React.ReactNode }) => { if (!children || (typeof children === 'string' && !children.trim())) { return null }