Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions components/report-template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ export const ReportTemplate: React.FC<ReportTemplateProps> = ({
chatTitle,
aiSummary
}) => {
const normalizeMarkdown = (content: string): string => {
return content
.replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2
.trim()
}
Comment on lines +26 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid normalizing inside fenced markdown blocks.

Line 28 collapses all 3+ newlines before markdown parsing, including inside fenced/preformatted blocks. That can corrupt report content such as code, logs, or intentionally spaced examples rendered through the downstream ReactMarkdown calls.

Proposed fix
 const normalizeMarkdown = (content: string): string => {
-  return content
-    .replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2
-    .trim()
+  let inFence = false
+  let blankCount = 0
+
+  return content
+    .trim()
+    .split('\n')
+    .filter(line => {
+      if (/^\s*(```|~~~)/.test(line)) {
+        inFence = !inFence
+      }
+
+      if (!inFence && !line.trim()) {
+        blankCount += 1
+        return blankCount <= 1
+      }
+
+      blankCount = 0
+      return true
+    })
+    .join('\n')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const normalizeMarkdown = (content: string): string => {
return content
.replace(/\n{3,}/g, '\n\n') // Collapse 3+ newlines to 2
.trim()
}
const normalizeMarkdown = (content: string): string => {
let inFence = false
let blankCount = 0
return content
.trim()
.split('\n')
.filter(line => {
if (/^\s*(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/report-template.tsx` around lines 26 - 30, The normalization in
normalizeMarkdown is collapsing all repeated newlines before parsing, which also
alters fenced/preformatted markdown content and can corrupt code, logs, or
intentionally spaced blocks. Update normalizeMarkdown in report-template.tsx to
track whether the parser is inside a fenced code block and only collapse extra
blank lines when outside fences, keeping the existing downstream ReactMarkdown
rendering intact.


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 <p>{children}</p>
}
}

const renderMessageContent = (content: any): string => {
if (typeof content === 'string') {
return content
Expand Down Expand Up @@ -113,9 +131,9 @@ export const ReportTemplate: React.FC<ReportTemplateProps> = ({
<h2 className="text-2xl font-black text-[#003366] uppercase tracking-tight">Intelligence Executive Summary</h2>
</div>
<div className="bg-slate-50 p-10 rounded-2xl border-l-8 border-[#003366] shadow-inner">
<div className="prose prose-slate prose-lg max-w-none text-slate-800 font-medium leading-relaxed">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{aiSummary}
<div className="prose prose-slate prose-lg prose-p:my-1 max-w-none text-slate-800 font-medium leading-relaxed">
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MarkdownComponents}>
{normalizeMarkdown(aiSummary)}
</ReactMarkdown>
</div>
</div>
Expand All @@ -128,7 +146,7 @@ export const ReportTemplate: React.FC<ReportTemplateProps> = ({
<span className="text-4xl font-black text-slate-200">01</span>
<h2 className="text-2xl font-black text-[#003366] uppercase tracking-tight">Executive Visualization</h2>
</div>
<div className="border-[12px] border-slate-50 rounded-2xl overflow-hidden shadow-2xl">
<div data-pdf-nosplit className="border-[12px] border-slate-50 rounded-2xl overflow-hidden shadow-2xl">
<img src={mapSnapshot} alt="Map Snapshot" className="w-full h-auto block" crossOrigin="anonymous" />
<div className="bg-slate-50 px-6 py-4 text-xs text-slate-500 text-center font-bold tracking-wide border-t border-slate-100">
ORBITAL PERSPECTIVE REF: {Math.random().toString(16).substring(2, 10).toUpperCase()}
Expand Down Expand Up @@ -167,14 +185,14 @@ export const ReportTemplate: React.FC<ReportTemplateProps> = ({
)
} else if (message.type === 'response') {
return (
<div key={index} className="prose prose-slate prose-lg max-w-none break-inside-avoid">
<div key={index} className="prose prose-slate prose-lg prose-p:my-1 max-w-none break-inside-avoid">
<div className="flex items-center gap-3 mb-6">
<div className="w-1.5 h-6 bg-emerald-500"></div>
<p className="text-xs font-black text-emerald-700 uppercase tracking-[0.2em] m-0">Strategic Output</p>
</div>
<div className="text-slate-700 leading-relaxed font-medium">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{contentString}
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MarkdownComponents}>
{normalizeMarkdown(contentString)}
</ReactMarkdown>
</div>
</div>
Expand All @@ -190,12 +208,14 @@ export const ReportTemplate: React.FC<ReportTemplateProps> = ({
</div>
{result.summary && (
<div className="text-slate-800 text-lg font-bold leading-snug bg-white p-8 rounded-2xl border border-slate-100 shadow-sm">
{result.summary}
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MarkdownComponents}>
{normalizeMarkdown(result.summary)}
</ReactMarkdown>
</div>
)}
<div className="flex flex-col items-center gap-12">
{result.mapboxImage && (
<div className="space-y-4 w-full max-w-2xl">
<div data-pdf-nosplit className="space-y-4 w-full max-w-2xl">
<div className="flex items-center justify-center gap-2">
<div className="w-2 h-2 rounded-full bg-indigo-500"></div>
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">Spectral RGB Analysis View</p>
Expand All @@ -204,7 +224,7 @@ export const ReportTemplate: React.FC<ReportTemplateProps> = ({
</div>
)}
{result.googleImage && (
<div className="space-y-4 w-full max-w-2xl">
<div data-pdf-nosplit className="space-y-4 w-full max-w-2xl">
<div className="flex items-center justify-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">High-Resolution Panchromatic Satellite</p>
Expand Down
104 changes: 68 additions & 36 deletions lib/utils/report-generator.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -12,73 +12,105 @@ 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)
})
})
),
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)
Comment on lines +35 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the mutated DOM style in a finally block.

If measurement or range derivation throws after Lines 36-37, the report DOM keeps the temporary inline width/position. Wrap the mutation in try/finally and remove the style attribute when it was originally absent.

Proposed fix
-const originalStyle = element.getAttribute('style') || ''
-element.style.width = `${templateWidth}px`
-element.style.position = 'relative'
+const originalStyle = element.getAttribute('style')
+let protectedRanges: Array<{ top: number; bottom: number }> = []
 
-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)
+try {
+  element.style.width = `${templateWidth}px`
+  element.style.position = 'relative'
+
+  const templateRect = element.getBoundingClientRect()
+  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)
+} finally {
+  if (originalStyle === null) {
+    element.removeAttribute('style')
+  } else {
+    element.setAttribute('style', originalStyle)
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 originalStyle = element.getAttribute('style')
let protectedRanges: Array<{ top: number; bottom: number }> = []
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
try {
element.style.width = `${templateWidth}px`
element.style.position = 'relative'
const templateRect = element.getBoundingClientRect()
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)
} finally {
if (originalStyle === null) {
element.removeAttribute('style')
} else {
element.setAttribute('style', originalStyle)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/utils/report-generator.ts` around lines 35 - 61, The DOM style mutation
in report generation is not guaranteed to be restored if measurement or
protected range collection fails. In report-generator.ts, wrap the
`element.style.width` and `element.style.position` mutation around the
`getBoundingClientRect`/`querySelectorAll` logic in a `try/finally`, and restore
the original inline style in the `finally` block; if `originalStyle` was empty,
remove the `style` attribute instead of setting it back.


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
Comment on lines +100 to +105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t let the 10px buffer split protected elements.

A no-split element that starts within 10px of currentPosition and extends past nextPossibleCut is still split, even when elementHeight <= pdfHeight. Use the loop’s 0.5 progress threshold instead of a 10px layout buffer.

Proposed fix
-// Only move the cut if the element starts after our current position (with small buffer)
+// Only move the cut if it advances the cursor and the element fits on a single page.
 // and fits on a single page.
-if (straddle && straddle.top > currentPosition + 10) {
+if (straddle && straddle.top > currentPosition + 0.5) {
   const elementHeight = straddle.bottom - straddle.top
   if (elementHeight <= pdfHeight) {
     effectiveCut = straddle.top
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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
// Only move the cut if it advances the cursor and the element fits on a single page.
// and fits on a single page.
if (straddle && straddle.top > currentPosition + 0.5) {
const elementHeight = straddle.bottom - straddle.top
if (elementHeight <= pdfHeight) {
effectiveCut = straddle.top
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/utils/report-generator.ts` around lines 100 - 105, The no-split handling
in report generation is still using the 10px layout buffer, which can split
protected elements that start close to the current position. Update the cut
logic in report-generator’s straddle handling to rely on the loop’s 0.5 progress
threshold instead of `currentPosition + 10`, while keeping the existing
`elementHeight <= pdfHeight` guard, so elements near the next cut are only moved
when they actually belong to the next page.

}
}
} 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`)
Expand Down