feat: add PDF export for complete lab reports (#6)#8
feat: add PDF export for complete lab reports (#6)#8mahek395 wants to merge 2 commits intoOpenLake:mainfrom
Conversation
- Add @react-pdf/renderer for client-side PDF generation - Cover page with IIT Bhilai branding and student metadata - Renders sections, observation tables, formulas, charts, results - ExportPDFWidget drop-in component with metadata dialog - Auto-captures Chart.js plots via toBase64Image() - ExportButtonClient wrapper for Next.js server component compatibility Closes OpenLake#6
|
@mahek395 is attempting to deploy a commit to the OpenLake_Website Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 56 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThis PR implements a complete lab report PDF export feature. It adds an "Export as PDF" button to experiment pages that opens a dialog for collecting student metadata, generates styled PDFs containing experiment data (theory, diagrams, observations, calculations, plots), and downloads them client-side using the Changes
Sequence DiagramsequenceDiagram
participant User
participant ExportButtonClient
participant ExportMetaDialog
participant useLabReportExport
participant labReportPDF
participant Browser
User->>ExportButtonClient: Page loads with experiment
ExportButtonClient->>useLabReportExport: Initialize hook with experiment
User->>ExportMetaDialog: Click "Export as PDF" button
ExportMetaDialog->>useLabReportExport: openDialog()
ExportMetaDialog->>ExportMetaDialog: Display modal, collect metadata
User->>ExportMetaDialog: Fill student info & click export
ExportMetaDialog->>useLabReportExport: exportPDF(experiment, meta)
useLabReportExport->>useLabReportExport: Set status = "generating"
useLabReportExport->>labReportPDF: generateLabReportPDF(experiment, meta, charts)
labReportPDF->>labReportPDF: Capture Chart.js images, render document
labReportPDF->>labReportPDF: Convert to Blob via pdf().toBlob()
labReportPDF-->>useLabReportExport: Return Blob
useLabReportExport->>Browser: Trigger download
useLabReportExport->>useLabReportExport: Set status = "done"
ExportMetaDialog->>ExportMetaDialog: Show success, auto-close after delay
Browser->>User: PDF downloaded
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…nLake#2) - Replace local timeout variables with useRef - Clear pending timeouts in useEffect cleanup - Prevents state update on unmounted component warning Closes OpenLake#2
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/lab/`[slug]/experiment/[experimentId]/page.js:
- Around line 33-35: ExportButtonClient's chart registry (registerChart) isn't
reachable by PlotPanel, so move or expose the hook where PlotPanel can call it:
lift the export hook that provides registerChart up into the ExperimentLayout
(or a parent that wraps children) and pass registerChart down to children that
create charts (e.g., pass as a prop into PlotPanel or provide it via React
context), or alternatively change ExportButtonClient to accept a registerChart
prop from its parent; update PlotPanel to call registerChart with its local
chartRef so the registry is populated for PDF export.
In `@components/experiment/ExportPDFButton.jsx`:
- Around line 331-448: The modal lacks proper dialog semantics and form label
associations: add role="dialog" and aria-modal="true" to the element rendered
with style S.dialog, give the dialog a unique id and set aria-labelledby to the
dialog title element and aria-describedby to the subtitle/optional-note as
appropriate; ensure the overlay container is not announced (role="presentation"
or aria-hidden) and that focus management/trapping is handled when
opening/closing via closeDialog. Also change each label/input pair to use
explicit for/id pairs (give inputs unique ids and updateMeta remains the
onChange handler) so labels are programmatically associated with their inputs
and assistive tech can read field names before exportPDF is invoked.
In `@lib/labReportPDF.js`:
- Around line 475-485: The current switch branch in lib/labReportPDF.js for
"formula"/"equation"/"calculation" writes block.content as plain text; instead,
convert block.content to KaTeX-rendered SVG and render that SVG in the PDF
output (preserving the surrounding View/label layout and styles.formulaBox and
styles.formulaLabel). Use the project's existing KaTeX renderer utility (the
same helper used on the experiment page — e.g., renderMathToSVG or
KaTeXRenderer) to produce SVG markup, and insert it via the PDF SVG component
used elsewhere (e.g., SvgXml/Svg) rather than a <Text
style={styles.formulaText}> node so math is typeset correctly. Ensure
block.label handling remains unchanged.
- Around line 630-642: CoverPage currently destructures semester and labGroup
from experiment but those values come from meta; update the destructuring so
semester and labGroup are read from meta (e.g., include semester = "" and
labGroup = "" in the meta destructure) and remove them from the experiment
destructure so the component uses the user-entered values; ensure defaults
(empty strings) and existing defaults like date remain in meta destructuring and
references in the function use these meta-provided variables.
- Around line 728-757: The component LabReportDocument currently treats sections
as an array and accesses section.blocks, but getExperiment() yields sections as
an object keyed by id with each value having a content array; update the code in
LabReportDocument to normalize sections (e.g., convert to an array via
Object.values or a small guard like Array.isArray check) and iterate over that
normalized list, and replace usages of section.blocks with section.content (or
the actual content array name provided by the schema) when rendering LabSection
so it works for both shapes.
- Around line 821-829: The loop in LabSection "Precautions" calls renderBlock
with the entire precautions array when an item p is a string, causing the full
list to be rendered repeatedly; change the branch inside the map so that when
typeof p === "string" you pass a single-item list (e.g., { type: "list",
content: [p] }) or otherwise pass p as-is to renderBlock (identify the map and
the call to renderBlock in lib/labReportPDF.js and update that conditional to
use [p] instead of precautions).
- Around line 548-566: The image/circuit/diagram/graph/plot branch currently
only uses block.src/block.base64, causing blocks with block.assetId to be
dropped; update generateLabReportPDF to resolve asset-backed blocks before
rendering: for each block with block.assetId look up the matching asset in
experiment.assets (or use the existing asset resolver used by ContentBlock),
populate block.src or block.base64 from the asset (or skip/null if not found),
then proceed to render the Image as before; reference symbols:
generateLabReportPDF, block.assetId, experiment.assets, and the image rendering
branch in lib/labReportPDF.js.
- Around line 589-595: The LabSection component currently sets wrap={false} on
the top-level View (function LabSection, styles.sectionContainer) which prevents
sections from paginating; remove the wrap={false} prop from that View so
sections (e.g., Calculations, Graphs & Plots, Observations) can break across
pages and flow naturally across page boundaries.
In `@lib/useLabReportExport.js`:
- Around line 75-80: The auto-close setTimeout in the useLabReportExport flow is
never cleared and can dismiss a reopened dialog; store the timeout id (e.g.,
autoCloseTimerId or autoCloseTimerRef) when calling setTimeout inside the code
path that calls setStatus("done") and setIsDialogOpen(false), and call
clearTimeout(autoCloseTimerId) whenever the dialog lifecycle changes (on dialog
close, before opening a new dialog, and in the hook's cleanup/unmount). Update
functions that setIsDialogOpen or setStatus to clear any existing timer before
creating a new one so the stale callback cannot fire and accidentally close a
new dialog session.
- Line 5: The static import of generateLabReportPDF causes the heavy
`@react-pdf/renderer` to bundle unnecessarily; change to a dynamic import inside
the exportPDF function: remove the top-level "import { generateLabReportPDF }
from \"./labReportPDF\"", and inside exportPDF() do const { generateLabReportPDF
} = await import("./labReportPDF"); then call generateLabReportPDF(...) as
before so the PDF builder and renderer are only loaded when exportPDF runs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3d32af9c-5a26-4424-92a4-11795a5d8d0d
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
app/lab/[slug]/experiment/[experimentId]/page.jscomponents/experiment/ExportButtonClient.jsxcomponents/experiment/ExportPDFButton.jsxlib/labReportPDF.jslib/useLabReportExport.jspackage.json
| <ExperimentLayout experiment={experiment} fullExperimentId={fullExperimentId}> | ||
| <ExportButtonClient experiment={experiment} /> | ||
| {SECTION_ORDER.map((sectionKey) => { |
There was a problem hiding this comment.
Expose registerChart to the components that actually create charts.
Mounting the export widget here is not enough for plot capture. components/experiment/PlotPanel.js still owns a local chartRef and never sees the hook's registerChart, so the registry stays empty and PDFs won't include generated plots.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/lab/`[slug]/experiment/[experimentId]/page.js around lines 33 - 35,
ExportButtonClient's chart registry (registerChart) isn't reachable by
PlotPanel, so move or expose the hook where PlotPanel can call it: lift the
export hook that provides registerChart up into the ExperimentLayout (or a
parent that wraps children) and pass registerChart down to children that create
charts (e.g., pass as a prop into PlotPanel or provide it via React context), or
alternatively change ExportButtonClient to accept a registerChart prop from its
parent; update PlotPanel to call registerChart with its local chartRef so the
registry is populated for PDF export.
| return ( | ||
| <> | ||
| <style>{spinnerKeyframes}</style> | ||
| {/* Overlay */} | ||
| <div style={S.overlay} onClick={closeDialog}> | ||
| {/* Dialog — stop propagation so clicking inside doesn't close */} | ||
| <div style={S.dialog} onClick={(e) => e.stopPropagation()}> | ||
| {/* Header */} | ||
| <div style={S.dialogHeader}> | ||
| <div style={S.dialogTitleGroup}> | ||
| <div style={S.dialogTitle}>Export Lab Report</div> | ||
| <div style={S.dialogSubtitle}> | ||
| Fill in your details to personalise the PDF (optional) | ||
| </div> | ||
| </div> | ||
| <button style={S.closeBtn} onClick={closeDialog} aria-label="Close"> | ||
| <CloseIcon /> | ||
| </button> | ||
| </div> | ||
|
|
||
| {/* Experiment info */} | ||
| <div style={S.infoStrip}> | ||
| <div style={S.infoStripLabel}>Experiment</div> | ||
| <div style={S.infoStripTitle}>{experimentTitle}</div> | ||
| </div> | ||
|
|
||
| {/* Form fields */} | ||
| <div style={S.fieldRow}> | ||
| <div style={S.fieldRowItem}> | ||
| <label style={S.label}>Student Name</label> | ||
| <input | ||
| style={S.input} | ||
| type="text" | ||
| placeholder="e.g. Rahul Sharma" | ||
| value={meta.studentName} | ||
| onChange={updateMeta("studentName")} | ||
| disabled={isGenerating || isDone} | ||
| /> | ||
| </div> | ||
| <div style={S.fieldRowItem}> | ||
| <label style={S.label}>Roll Number</label> | ||
| <input | ||
| style={S.input} | ||
| type="text" | ||
| placeholder="e.g. 12340100" | ||
| value={meta.rollNumber} | ||
| onChange={updateMeta("rollNumber")} | ||
| disabled={isGenerating || isDone} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div style={S.fieldRow}> | ||
| <div style={S.fieldRowItem}> | ||
| <label style={S.label}>Semester</label> | ||
| <input | ||
| style={S.input} | ||
| type="text" | ||
| placeholder="e.g. VI (2025–26)" | ||
| value={meta.semester} | ||
| onChange={updateMeta("semester")} | ||
| disabled={isGenerating || isDone} | ||
| /> | ||
| </div> | ||
| <div style={S.fieldRowItem}> | ||
| <label style={S.label}>Lab Group</label> | ||
| <input | ||
| style={S.input} | ||
| type="text" | ||
| placeholder="e.g. B1" | ||
| value={meta.labGroup} | ||
| onChange={updateMeta("labGroup")} | ||
| disabled={isGenerating || isDone} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div style={S.fieldGroup}> | ||
| <label style={S.label}>Instructor / TA</label> | ||
| <input | ||
| style={S.input} | ||
| type="text" | ||
| placeholder="e.g. Dr. Priya Gupta" | ||
| value={meta.instructor} | ||
| onChange={updateMeta("instructor")} | ||
| disabled={isGenerating || isDone} | ||
| /> | ||
| </div> | ||
|
|
||
| <p style={S.optionalNote}>All fields are optional.</p> | ||
|
|
||
| {/* Feedback */} | ||
| {isError && errorMsg && ( | ||
| <div style={S.errorBox}>⚠ {errorMsg}</div> | ||
| )} | ||
| {isDone && ( | ||
| <div style={S.successBox}>✓ PDF downloaded successfully!</div> | ||
| )} | ||
|
|
||
| {/* Actions */} | ||
| <div style={S.actionRow}> | ||
| <button | ||
| style={S.cancelBtn} | ||
| onClick={closeDialog} | ||
| disabled={isGenerating} | ||
| > | ||
| Cancel | ||
| </button> | ||
| <button | ||
| style={exportBtnStyle} | ||
| onClick={exportPDF} | ||
| disabled={isGenerating || isDone} | ||
| > | ||
| {exportBtnContent} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Add dialog semantics and real label associations.
The modal has no role="dialog"/aria-modal, and the labels are not bound to their inputs. Screen-reader users will land in an unnamed overlay with unnamed fields, which makes the export flow difficult to complete.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/experiment/ExportPDFButton.jsx` around lines 331 - 448, The modal
lacks proper dialog semantics and form label associations: add role="dialog" and
aria-modal="true" to the element rendered with style S.dialog, give the dialog a
unique id and set aria-labelledby to the dialog title element and
aria-describedby to the subtitle/optional-note as appropriate; ensure the
overlay container is not announced (role="presentation" or aria-hidden) and that
focus management/trapping is handled when opening/closing via closeDialog. Also
change each label/input pair to use explicit for/id pairs (give inputs unique
ids and updateMeta remains the onChange handler) so labels are programmatically
associated with their inputs and assistive tech can read field names before
exportPDF is invoked.
| case "formula": | ||
| case "equation": | ||
| case "calculation": | ||
| return ( | ||
| <View key={index} style={styles.formulaBox}> | ||
| <Text style={styles.formulaText}>{block.content}</Text> | ||
| {block.label && ( | ||
| <Text style={styles.formulaLabel}>{block.label}</Text> | ||
| )} | ||
| </View> | ||
| ); |
There was a problem hiding this comment.
Render formulas as KaTeX/SVG instead of raw text.
The linked requirement calls out reusing the existing KaTeX rendering for PDF export. Writing block.content directly into <Text> will print raw TeX/plain strings and loses the math layout the experiment page already has.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/labReportPDF.js` around lines 475 - 485, The current switch branch in
lib/labReportPDF.js for "formula"/"equation"/"calculation" writes block.content
as plain text; instead, convert block.content to KaTeX-rendered SVG and render
that SVG in the PDF output (preserving the surrounding View/label layout and
styles.formulaBox and styles.formulaLabel). Use the project's existing KaTeX
renderer utility (the same helper used on the experiment page — e.g.,
renderMathToSVG or KaTeXRenderer) to produce SVG markup, and insert it via the
PDF SVG component used elsewhere (e.g., SvgXml/Svg) rather than a <Text
style={styles.formulaText}> node so math is typeset correctly. Ensure
block.label handling remains unchanged.
| case "image": | ||
| case "circuit": | ||
| case "diagram": | ||
| case "graph": | ||
| case "plot": | ||
| if (!block.src && !block.base64) return null; | ||
| return ( | ||
| <View key={index} style={styles.imageContainer}> | ||
| <View style={styles.imageBox}> | ||
| <Image | ||
| src={block.base64 || block.src} | ||
| style={{ maxWidth: 380, maxHeight: 260 }} | ||
| /> | ||
| </View> | ||
| {block.caption && ( | ||
| <Text style={styles.imageCaption}>{block.caption}</Text> | ||
| )} | ||
| </View> | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | head -20Repository: OpenLake/bhilaee-labs
Length of output: 558
🏁 Script executed:
find . -name "labReportPDF.js" -o -name "ContentBlock*" 2>/dev/nullRepository: OpenLake/bhilaee-labs
Length of output: 126
🏁 Script executed:
wc -l lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 88
🏁 Script executed:
sed -n '540,580p' lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 1123
🏁 Script executed:
cat -n components/experiment/ContentBlock.jsRepository: OpenLake/bhilaee-labs
Length of output: 13139
🏁 Script executed:
rg "block\.(asset|src|base64)" lib/labReportPDF.js -A 2 -B 2Repository: OpenLake/bhilaee-labs
Length of output: 400
🏁 Script executed:
grep -n "labReportPDF\|renderBlockPDF\|function.*PDF" lib/labReportPDF.js | head -20Repository: OpenLake/bhilaee-labs
Length of output: 234
🏁 Script executed:
rg "labReportPDF|renderBlockPDF" --type js -B 3 -A 3Repository: OpenLake/bhilaee-labs
Length of output: 1133
🏁 Script executed:
rg "block\.assetId" --type jsRepository: OpenLake/bhilaee-labs
Length of output: 516
🏁 Script executed:
sed -n '904,950p' lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 1285
🏁 Script executed:
rg "generateLabReportPDF" -A 10 lib/useLabReportExport.jsRepository: OpenLake/bhilaee-labs
Length of output: 1082
🏁 Script executed:
rg "experiment\.assets\|experiment\.blocks" --type js -B 2 -A 2Repository: OpenLake/bhilaee-labs
Length of output: 47
🏁 Script executed:
sed -n '520,600p' lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 2371
🏁 Script executed:
rg "renderBlock|case \"image\"" lib/labReportPDF.js -B 10 -A 15Repository: OpenLake/bhilaee-labs
Length of output: 4322
🏁 Script executed:
rg "experiment\." lib/labReportPDF.js | head -20Repository: OpenLake/bhilaee-labs
Length of output: 156
🏁 Script executed:
rg "assetId" --type js --type json -B 2 -A 2Repository: OpenLake/bhilaee-labs
Length of output: 50378
🏁 Script executed:
rg "block.*assetId\|assetId.*block" --type js -B 3 -A 3Repository: OpenLake/bhilaee-labs
Length of output: 47
Resolve asset-backed images before handing blocks to <Image>.
The web path renders blocks through ContentBlock with experiment.assets, but this PDF path only accepts block.src or block.base64. Diagram/image blocks with block.assetId (as used across sensor-lab, machines-lab, instrumentation-lab, and power-system-lab experiments) will silently disappear from exports. The generateLabReportPDF function needs to either accept and resolve assets, or blocks must be pre-resolved before rendering.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/labReportPDF.js` around lines 548 - 566, The
image/circuit/diagram/graph/plot branch currently only uses
block.src/block.base64, causing blocks with block.assetId to be dropped; update
generateLabReportPDF to resolve asset-backed blocks before rendering: for each
block with block.assetId look up the matching asset in experiment.assets (or use
the existing asset resolver used by ContentBlock), populate block.src or
block.base64 from the asset (or skip/null if not found), then proceed to render
the Image as before; reference symbols: generateLabReportPDF, block.assetId,
experiment.assets, and the image rendering branch in lib/labReportPDF.js.
| function LabSection({ title, blocks = [], children }) { | ||
| return ( | ||
| <View style={styles.sectionContainer} wrap={false}> | ||
| {title && <Text style={styles.sectionHeading}>{title}</Text>} | ||
| {blocks.map((block, i) => renderBlock(block, i))} | ||
| {children} | ||
| </View> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's verify the file exists and check the specific lines
wc -l lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 88
🏁 Script executed:
# Read the relevant section to confirm the code
sed -n '585,600p' lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 517
🏁 Script executed:
# Search for other uses of wrap={false} to understand the pattern
rg "wrap\s*=\s*\{false\}" lib/labReportPDF.js -n -A 2 -B 2Repository: OpenLake/bhilaee-labs
Length of output: 331
🏁 Script executed:
# Check the imports to confirm this is `@react-pdf/renderer`
head -30 lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 1033
🏁 Script executed:
# Check how LabSection is used throughout the file
rg "LabSection" lib/labReportPDF.js -n -B 2 -A 2Repository: OpenLake/bhilaee-labs
Length of output: 2605
🏁 Script executed:
# Look for any other sections that might have wrap={false} to see if this is consistent
rg "<View" lib/labReportPDF.js -n | head -30Repository: OpenLake/bhilaee-labs
Length of output: 1525
🏁 Script executed:
# Check the styles.sectionContainer to understand the styling
sed -n '30,150p' lib/labReportPDF.js | rg "sectionContainer" -B 5 -A 5Repository: OpenLake/bhilaee-labs
Length of output: 47
🏁 Script executed:
# Search for sectionContainer style definition
rg "sectionContainer" lib/labReportPDF.js -B 3 -A 3Repository: OpenLake/bhilaee-labs
Length of output: 485
🏁 Script executed:
# Look at the styles object more carefully
sed -n '30,250p' lib/labReportPDF.js | grep -A 10 "sectionContainer"Repository: OpenLake/bhilaee-labs
Length of output: 310
🏁 Script executed:
# Get a broader view of the styles
sed -n '30,250p' lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 4737
🏁 Script executed:
# Let's search for any documentation or comments about wrap behavior in the file
rg "wrap" lib/labReportPDF.js -B 2 -A 2Repository: OpenLake/bhilaee-labs
Length of output: 477
🏁 Script executed:
# Check if there are other uses of wrap={true} or comments about pagination
rg "pagination\|page\s*break\|overflow" lib/labReportPDF.js -iRepository: OpenLake/bhilaee-labs
Length of output: 47
🌐 Web query:
@react-pdf/renderer View wrap property behavior pagination
💡 Result:
In @react-pdf/renderer, pagination (“page wrapping”) is controlled at two levels: the <Page wrap> prop (turns the pagination engine on/off for that page) and each element’s own wrap prop (whether that element is allowed to be split across pages). (react-pdf.org)
1) <Page wrap>: enables/disables pagination
wrapon<Page />istrueby default. Whentrue, react-pdf will automatically generate additional pages (“subpages”) when content overflows. (react-pdf.org)- If you set
<Page wrap={false}>, the pagination step returns only that single page (no splitting happens). (github.com)
2) <View wrap>: can this View be split across pages?
wrapon<View />is alsotrueby default. (react-pdf.org)- With
wrap={true}(default), aViewmay be split when it crosses the page boundary, and part of it can continue on the next page. (github.com) - With
wrap={false}, theViewis treated as unbreakable:- if it doesn’t fit in the remaining space at the bottom of the page, it gets moved to the next page as a whole. (react-pdf.org)
- if it’s taller than the available page content area and
wrap={false}, react-pdf warns (“can’t wrap… bigger than available page height”), keeps that node on the current page, and pushes the rest of the nodes to the next page. (github.com)
3) Some node types never wrap (even if you don’t set wrap={false})
Internally, the layout engine forces wrap=false for Svg, Note, Image, Canvas nodes. (github.com)
Citations:
- 1: https://react-pdf.org/advanced
- 2: https://react-pdf.org/components
- 3: https://github.com/diegomura/react-pdf/blob/ee5c96b8/packages/layout/src/steps/resolvePagination.ts
- 4: https://react-pdf.org/components
- 5: https://github.com/diegomura/react-pdf/blob/ee5c96b8/packages/layout/src/steps/resolvePagination.ts
- 6: https://react-pdf.org/advanced
- 7: https://github.com/diegomura/react-pdf/blob/ee5c96b8/packages/layout/src/steps/resolvePagination.ts
- 8: https://github.com/diegomura/react-pdf/blob/ee5c96b8/packages/layout/src/node/getWrap.ts
Remove wrap={false} to allow sections to paginate across pages.
wrap={false} forces each section to remain unbreakable. Large sections like Calculations, Graphs & Plots, or Observations cannot be split across pages—they either overflow with warnings or get pushed awkwardly to the next page as a whole unit. Removing this constraint allows sections to flow naturally across page boundaries.
♻️ Suggested change
function LabSection({ title, blocks = [], children }) {
return (
- <View style={styles.sectionContainer} wrap={false}>
+ <View style={styles.sectionContainer}>
{title && <Text style={styles.sectionHeading}>{title}</Text>}
{blocks.map((block, i) => renderBlock(block, i))}
{children}
</View>
);📝 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.
| function LabSection({ title, blocks = [], children }) { | |
| return ( | |
| <View style={styles.sectionContainer} wrap={false}> | |
| {title && <Text style={styles.sectionHeading}>{title}</Text>} | |
| {blocks.map((block, i) => renderBlock(block, i))} | |
| {children} | |
| </View> | |
| function LabSection({ title, blocks = [], children }) { | |
| return ( | |
| <View style={styles.sectionContainer}> | |
| {title && <Text style={styles.sectionHeading}>{title}</Text>} | |
| {blocks.map((block, i) => renderBlock(block, i))} | |
| {children} | |
| </View> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/labReportPDF.js` around lines 589 - 595, The LabSection component
currently sets wrap={false} on the top-level View (function LabSection,
styles.sectionContainer) which prevents sections from paginating; remove the
wrap={false} prop from that View so sections (e.g., Calculations, Graphs &
Plots, Observations) can break across pages and flow naturally across page
boundaries.
| function CoverPage({ experiment, meta }) { | ||
| const { | ||
| title = "Experiment", | ||
| course = "", | ||
| semester = "", | ||
| labGroup = "", | ||
| } = experiment; | ||
| const { | ||
| studentName = "", | ||
| rollNumber = "", | ||
| date = new Date().toLocaleDateString("en-IN"), | ||
| instructor = "", | ||
| } = meta || {}; |
There was a problem hiding this comment.
Read semester and labGroup from the dialog metadata.
The dialog stores those fields in meta, but the cover page destructures them from experiment. As written, user-entered semester/group values are dropped from the PDF.
♻️ Suggested change
function CoverPage({ experiment, meta }) {
- const {
- title = "Experiment",
- course = "",
- semester = "",
- labGroup = "",
- } = experiment;
+ const { title = "Experiment", course = "" } = experiment;
const {
studentName = "",
rollNumber = "",
date = new Date().toLocaleDateString("en-IN"),
instructor = "",
+ semester = "",
+ labGroup = "",
} = meta || {};📝 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.
| function CoverPage({ experiment, meta }) { | |
| const { | |
| title = "Experiment", | |
| course = "", | |
| semester = "", | |
| labGroup = "", | |
| } = experiment; | |
| const { | |
| studentName = "", | |
| rollNumber = "", | |
| date = new Date().toLocaleDateString("en-IN"), | |
| instructor = "", | |
| } = meta || {}; | |
| function CoverPage({ experiment, meta }) { | |
| const { title = "Experiment", course = "" } = experiment; | |
| const { | |
| studentName = "", | |
| rollNumber = "", | |
| date = new Date().toLocaleDateString("en-IN"), | |
| instructor = "", | |
| semester = "", | |
| labGroup = "", | |
| } = meta || {}; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/labReportPDF.js` around lines 630 - 642, CoverPage currently destructures
semester and labGroup from experiment but those values come from meta; update
the destructuring so semester and labGroup are read from meta (e.g., include
semester = "" and labGroup = "" in the meta destructure) and remove them from
the experiment destructure so the component uses the user-entered values; ensure
defaults (empty strings) and existing defaults like date remain in meta
destructuring and references in the function use these meta-provided variables.
| export function LabReportDocument({ experiment, meta }) { | ||
| const { | ||
| title = "Experiment", | ||
| sections = [], | ||
| observations = [], | ||
| calculations = [], | ||
| results = [], | ||
| chartImages = [], | ||
| circuitImages = [], | ||
| precautions = [], | ||
| sources = [], | ||
| } = experiment; | ||
|
|
||
| return ( | ||
| <Document | ||
| title={`Lab Report — ${title}`} | ||
| author={meta?.studentName || "IIT Bhilai Student"} | ||
| subject={`${title} — IIT Bhilai Virtual Lab`} | ||
| creator="IIT Bhilai Virtual Lab" | ||
| producer="@react-pdf/renderer" | ||
| > | ||
| {/* Cover Page */} | ||
| <CoverPage experiment={experiment} meta={meta} /> | ||
|
|
||
| {/* Content Pages */} | ||
| <ReportPage experimentTitle={title}> | ||
| {/* Render standard sections from the experiment's `sections` array */} | ||
| {sections.map((section, si) => ( | ||
| <LabSection key={si} title={section.title} blocks={section.blocks} /> | ||
| ))} |
There was a problem hiding this comment.
Use the actual experiment.sections shape here.
getExperiment() produces sections as an object keyed by section id with a content array. sections.map(...) will throw on normal experiments, and section.blocks is not a field the current schema provides.
♻️ Suggested change
const {
title = "Experiment",
- sections = [],
+ sections = {},
observations = [],
calculations = [],
results = [],
chartImages = [],
@@
- {/* Render standard sections from the experiment's `sections` array */}
- {sections.map((section, si) => (
- <LabSection key={si} title={section.title} blocks={section.blocks} />
- ))}
+ {/* Render standard sections from the experiment's `sections` object */}
+ {Object.entries(sections).map(([sectionKey, section]) =>
+ section?.isApplicable === false ? null : (
+ <LabSection
+ key={sectionKey}
+ title={section.title}
+ blocks={section.content}
+ />
+ )
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/labReportPDF.js` around lines 728 - 757, The component LabReportDocument
currently treats sections as an array and accesses section.blocks, but
getExperiment() yields sections as an object keyed by id with each value having
a content array; update the code in LabReportDocument to normalize sections
(e.g., convert to an array via Object.values or a small guard like Array.isArray
check) and iterate over that normalized list, and replace usages of
section.blocks with section.content (or the actual content array name provided
by the schema) when rendering LabSection so it works for both shapes.
| {precautions.length > 0 && ( | ||
| <LabSection title="Precautions"> | ||
| {precautions.map((p, pi) => | ||
| renderBlock( | ||
| typeof p === "string" | ||
| ? { type: "list", content: precautions } | ||
| : p, | ||
| pi | ||
| ) |
There was a problem hiding this comment.
Don't render the full precautions list once per entry.
When precautions is an array of strings, each iteration passes the entire array back into renderBlock({ type: "list", content: precautions }, pi), so the same list is emitted repeatedly.
♻️ Suggested change
{precautions.length > 0 && (
<LabSection title="Precautions">
- {precautions.map((p, pi) =>
- renderBlock(
- typeof p === "string"
- ? { type: "list", content: precautions }
- : p,
- pi
- )
- )}
+ {typeof precautions[0] === "string"
+ ? renderBlock({ type: "list", content: precautions }, 0)
+ : precautions.map((p, pi) => renderBlock(p, pi))}
</LabSection>
)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/labReportPDF.js` around lines 821 - 829, The loop in LabSection
"Precautions" calls renderBlock with the entire precautions array when an item p
is a string, causing the full list to be rendered repeatedly; change the branch
inside the map so that when typeof p === "string" you pass a single-item list
(e.g., { type: "list", content: [p] }) or otherwise pass p as-is to renderBlock
(identify the map and the call to renderBlock in lib/labReportPDF.js and update
that conditional to use [p] instead of precautions).
| // React hook that manages PDF export state, metadata collection, and chart capture | ||
|
|
||
| import { useState, useRef, useCallback } from "react"; | ||
| import { generateLabReportPDF } from "./labReportPDF"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/useLabReportExport.jsRepository: OpenLake/bhilaee-labs
Length of output: 3584
🏁 Script executed:
# Search for other usages of generateLabReportPDF to ensure no other code depends on the static import
rg "generateLabReportPDF" --type js --type jsx --type ts --type tsxRepository: OpenLake/bhilaee-labs
Length of output: 92
🏁 Script executed:
# Verify labReportPDF.js exists
fd "labReportPDF" -type fRepository: OpenLake/bhilaee-labs
Length of output: 235
🏁 Script executed:
# Search for all usages of generateLabReportPDF in the repository
rg "generateLabReportPDF"Repository: OpenLake/bhilaee-labs
Length of output: 611
🏁 Script executed:
# Check if labReportPDF.js exists
find . -name "labReportPDF.js" -o -name "labReportPDF.ts" -o -name "labReportPDF.jsx" -o -name "labReportPDF.tsx"Repository: OpenLake/bhilaee-labs
Length of output: 86
🏁 Script executed:
# Examine labReportPDF.js to understand the function and any dependencies
head -50 lib/labReportPDF.jsRepository: OpenLake/bhilaee-labs
Length of output: 1511
🏁 Script executed:
# Check if generateLabReportPDF is imported anywhere else besides useLabReportExport.js
rg "from.*labReportPDF" --no-headingRepository: OpenLake/bhilaee-labs
Length of output: 225
🏁 Script executed:
# Look at where useLabReportExport is used to understand the consumption pattern
rg "useLabReportExport|from.*useLabReportExport"Repository: OpenLake/bhilaee-labs
Length of output: 1104
Lazy-load the PDF builder on demand.
This static import puts lib/labReportPDF.js and @react-pdf/renderer on the default experiment-page bundle even when the user never exports anything. Move it inside exportPDF() so the heavy renderer only loads on click.
♻️ Suggested change
-import { generateLabReportPDF } from "./labReportPDF";
// ...
const exportPDF = useCallback(async () => {
setStatus("generating");
try {
+ const { generateLabReportPDF } = await import("./labReportPDF");
await generateLabReportPDF(
experiment,
meta,
chartRegistryRef.current
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/useLabReportExport.js` at line 5, The static import of
generateLabReportPDF causes the heavy `@react-pdf/renderer` to bundle
unnecessarily; change to a dynamic import inside the exportPDF function: remove
the top-level "import { generateLabReportPDF } from \"./labReportPDF\"", and
inside exportPDF() do const { generateLabReportPDF } = await
import("./labReportPDF"); then call generateLabReportPDF(...) as before so the
PDF builder and renderer are only loaded when exportPDF runs.
| setStatus("done"); | ||
| // Auto-close after brief success state | ||
| setTimeout(() => { | ||
| setIsDialogOpen(false); | ||
| setStatus("idle"); | ||
| }, 1800); |
There was a problem hiding this comment.
Clear the success auto-close timer when the dialog lifecycle changes.
The timer is never cancelled. If the user closes and reopens the dialog within 1.8s, the stale callback can still fire and dismiss the new session unexpectedly.
♻️ Suggested change
// Registry of live Chart.js instances
const chartRegistryRef = useRef({});
+ const autoCloseTimerRef = useRef(null);
const openDialog = useCallback(() => {
+ clearTimeout(autoCloseTimerRef.current);
setStatus("idle");
setErrorMsg("");
setIsDialogOpen(true);
}, []);
const closeDialog = useCallback(() => {
+ clearTimeout(autoCloseTimerRef.current);
setIsDialogOpen(false);
setStatus("idle");
}, []);
const exportPDF = useCallback(async () => {
setStatus("generating");
try {
await generateLabReportPDF(
experiment,
meta,
chartRegistryRef.current
);
setStatus("done");
// Auto-close after brief success state
- setTimeout(() => {
+ clearTimeout(autoCloseTimerRef.current);
+ autoCloseTimerRef.current = setTimeout(() => {
setIsDialogOpen(false);
setStatus("idle");
}, 1800);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/useLabReportExport.js` around lines 75 - 80, The auto-close setTimeout in
the useLabReportExport flow is never cleared and can dismiss a reopened dialog;
store the timeout id (e.g., autoCloseTimerId or autoCloseTimerRef) when calling
setTimeout inside the code path that calls setStatus("done") and
setIsDialogOpen(false), and call clearTimeout(autoCloseTimerId) whenever the
dialog lifecycle changes (on dialog close, before opening a new dialog, and in
the hook's cleanup/unmount). Update functions that setIsDialogOpen or setStatus
to clear any existing timer before creating a new one so the stale callback
cannot fire and accidentally close a new dialog session.
Summary
Implements PDF export for complete lab reports (closes #6).
Changes
lib/labReportPDF.js— PDF document builder (@react-pdf/renderer)lib/useLabReportExport.js— hook for state, chart capture, downloadcomponents/experiment/ExportPDFButton.jsx— Export button + metadata dialogcomponents/experiment/ExportButtonClient.jsx— Client wrapper for Next.js server component compatibilityWhat the PDF includes
Testing
Screenshots
Summary by CodeRabbit
New Features
Dependencies
@react-pdf/rendererfor PDF generation.