Skip to content

feat: add PDF export for complete lab reports (#6)#8

Open
mahek395 wants to merge 2 commits intoOpenLake:mainfrom
mahek395:feature/pdf-export-lab-reports
Open

feat: add PDF export for complete lab reports (#6)#8
mahek395 wants to merge 2 commits intoOpenLake:mainfrom
mahek395:feature/pdf-export-lab-reports

Conversation

@mahek395
Copy link
Copy Markdown

@mahek395 mahek395 commented Apr 8, 2026

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, download
  • components/experiment/ExportPDFButton.jsx — Export button + metadata dialog
  • components/experiment/ExportButtonClient.jsx — Client wrapper for Next.js server component compatibility

What the PDF includes

  • Cover page (IIT Bhilai branding, student metadata)
  • Theory with formula blocks
  • Circuit diagram images
  • Observation tables
  • Calculations
  • Auto-captured Chart.js plots
  • Result & conclusion, precautions, references
  • Marks rubric + signature block

Testing

  • Tested with an experiment that has charts
  • PDF downloads correctly in Chrome
  • Tested with an experiment that has no charts
  • PDF downloads correctly in Firefox
  • Blank metadata fields are handled gracefully

Screenshots

image image

Summary by CodeRabbit

  • New Features

    • Added PDF export functionality for experiments with a dedicated export button and configurable metadata dialog.
    • Users can add optional student metadata (name, roll number, semester, lab group, instructor) to exported reports.
    • Exported PDFs feature formatted lab report layouts including experiment details, embedded charts, and signature blocks.
  • Dependencies

    • Added @react-pdf/renderer for PDF generation.

- 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
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 8, 2026

@mahek395 is attempting to deploy a commit to the OpenLake_Website Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Warning

Rate limit exceeded

@mahek395 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 56 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ffe7f64-61bd-40e1-b725-b5894161dc34

📥 Commits

Reviewing files that changed from the base of the PR and between 97e33c4 and 62e7901.

📒 Files selected for processing (3)
  • app/history/page.js
  • app/observations/page.js
  • app/starred/page.js

Walkthrough

This 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 @react-pdf/renderer library.

Changes

Cohort / File(s) Summary
Export UI Components
components/experiment/ExportButtonClient.jsx, components/experiment/ExportPDFButton.jsx
ExportButtonClient wraps the export hook and widget. ExportPDFButton includes a styled button, ExportMetaDialog for collecting student metadata (name, roll number, semester, lab group, instructor), and ExportPDFWidget composer. Dialog displays status feedback (generating spinner, success, error states) and error messaging.
PDF Generation Logic
lib/labReportPDF.js, lib/useLabReportExport.js
labReportPDF defines PDF layout (cover page, sections, tables, images, signature blocks) and renders experiment data to a Blob. useLabReportExport hook manages dialog state, export lifecycle (idle→generating→done/error), metadata collection, and Chart.js instance registration.
Integration & Dependencies
app/lab/[slug]/experiment/[experimentId]/page.js, package.json
Experiment page imports and renders ExportButtonClient with experiment prop. package.json adds @react-pdf/renderer ^4.4.0 dependency.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A PDF dream, so clean and neat,
Lab reports now have a rapid beat!
Charts and tables, formatted with care,
Downloaded fast, floating through the air! 📄✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature: adding PDF export functionality for complete lab reports, which is the primary objective of this changeset.
Linked Issues check ✅ Passed All coding requirements from issue #6 are met: Export button added, PDF includes all required content (title, theory, diagrams, tables, calculations, plots), client-side generation via @react-pdf/renderer implemented, KaTeX rendering handled, and Chart.js plots captured.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing PDF export functionality for lab reports as specified in issue #6; no unrelated code modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…nLake#2)

- Replace local timeout variables with useRef
- Clear pending timeouts in useEffect cleanup
- Prevents state update on unmounted component warning

Closes OpenLake#2
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between cd31052 and 97e33c4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • app/lab/[slug]/experiment/[experimentId]/page.js
  • components/experiment/ExportButtonClient.jsx
  • components/experiment/ExportPDFButton.jsx
  • lib/labReportPDF.js
  • lib/useLabReportExport.js
  • package.json

Comment on lines 33 to 35
<ExperimentLayout experiment={experiment} fullExperimentId={fullExperimentId}>
<ExportButtonClient experiment={experiment} />
{SECTION_ORDER.map((sectionKey) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +331 to +448
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment thread lib/labReportPDF.js
Comment on lines +475 to +485
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>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment thread lib/labReportPDF.js
Comment on lines +548 to +566
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>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | head -20

Repository: OpenLake/bhilaee-labs

Length of output: 558


🏁 Script executed:

find . -name "labReportPDF.js" -o -name "ContentBlock*" 2>/dev/null

Repository: OpenLake/bhilaee-labs

Length of output: 126


🏁 Script executed:

wc -l lib/labReportPDF.js

Repository: OpenLake/bhilaee-labs

Length of output: 88


🏁 Script executed:

sed -n '540,580p' lib/labReportPDF.js

Repository: OpenLake/bhilaee-labs

Length of output: 1123


🏁 Script executed:

cat -n components/experiment/ContentBlock.js

Repository: OpenLake/bhilaee-labs

Length of output: 13139


🏁 Script executed:

rg "block\.(asset|src|base64)" lib/labReportPDF.js -A 2 -B 2

Repository: OpenLake/bhilaee-labs

Length of output: 400


🏁 Script executed:

grep -n "labReportPDF\|renderBlockPDF\|function.*PDF" lib/labReportPDF.js | head -20

Repository: OpenLake/bhilaee-labs

Length of output: 234


🏁 Script executed:

rg "labReportPDF|renderBlockPDF" --type js -B 3 -A 3

Repository: OpenLake/bhilaee-labs

Length of output: 1133


🏁 Script executed:

rg "block\.assetId" --type js

Repository: OpenLake/bhilaee-labs

Length of output: 516


🏁 Script executed:

sed -n '904,950p' lib/labReportPDF.js

Repository: OpenLake/bhilaee-labs

Length of output: 1285


🏁 Script executed:

rg "generateLabReportPDF" -A 10 lib/useLabReportExport.js

Repository: OpenLake/bhilaee-labs

Length of output: 1082


🏁 Script executed:

rg "experiment\.assets\|experiment\.blocks" --type js -B 2 -A 2

Repository: OpenLake/bhilaee-labs

Length of output: 47


🏁 Script executed:

sed -n '520,600p' lib/labReportPDF.js

Repository: OpenLake/bhilaee-labs

Length of output: 2371


🏁 Script executed:

rg "renderBlock|case \"image\"" lib/labReportPDF.js -B 10 -A 15

Repository: OpenLake/bhilaee-labs

Length of output: 4322


🏁 Script executed:

rg "experiment\." lib/labReportPDF.js | head -20

Repository: OpenLake/bhilaee-labs

Length of output: 156


🏁 Script executed:

rg "assetId" --type js --type json -B 2 -A 2

Repository: OpenLake/bhilaee-labs

Length of output: 50378


🏁 Script executed:

rg "block.*assetId\|assetId.*block" --type js -B 3 -A 3

Repository: 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.

Comment thread lib/labReportPDF.js
Comment on lines +589 to +595
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file exists and check the specific lines
wc -l lib/labReportPDF.js

Repository: OpenLake/bhilaee-labs

Length of output: 88


🏁 Script executed:

# Read the relevant section to confirm the code
sed -n '585,600p' lib/labReportPDF.js

Repository: 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 2

Repository: OpenLake/bhilaee-labs

Length of output: 331


🏁 Script executed:

# Check the imports to confirm this is `@react-pdf/renderer`
head -30 lib/labReportPDF.js

Repository: 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 2

Repository: 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 -30

Repository: 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 5

Repository: OpenLake/bhilaee-labs

Length of output: 47


🏁 Script executed:

# Search for sectionContainer style definition
rg "sectionContainer" lib/labReportPDF.js -B 3 -A 3

Repository: 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.js

Repository: 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 2

Repository: 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 -i

Repository: 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

  • wrap on <Page /> is true by default. When true, 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?

  • wrap on <View /> is also true by default. (react-pdf.org)
  • With wrap={true} (default), a View may be split when it crosses the page boundary, and part of it can continue on the next page. (github.com)
  • With wrap={false}, the View is 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:


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.

Suggested change
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.

Comment thread lib/labReportPDF.js
Comment on lines +630 to +642
function CoverPage({ experiment, meta }) {
const {
title = "Experiment",
course = "",
semester = "",
labGroup = "",
} = experiment;
const {
studentName = "",
rollNumber = "",
date = new Date().toLocaleDateString("en-IN"),
instructor = "",
} = meta || {};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment thread lib/labReportPDF.js
Comment on lines +728 to +757
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} />
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Comment thread lib/labReportPDF.js
Comment on lines +821 to +829
{precautions.length > 0 && (
<LabSection title="Precautions">
{precautions.map((p, pi) =>
renderBlock(
typeof p === "string"
? { type: "list", content: precautions }
: p,
pi
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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).

Comment thread lib/useLabReportExport.js
// React hook that manages PDF export state, metadata collection, and chart capture

import { useState, useRef, useCallback } from "react";
import { generateLabReportPDF } from "./labReportPDF";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/useLabReportExport.js

Repository: 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 tsx

Repository: OpenLake/bhilaee-labs

Length of output: 92


🏁 Script executed:

# Verify labReportPDF.js exists
fd "labReportPDF" -type f

Repository: 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.js

Repository: OpenLake/bhilaee-labs

Length of output: 1511


🏁 Script executed:

# Check if generateLabReportPDF is imported anywhere else besides useLabReportExport.js
rg "from.*labReportPDF" --no-heading

Repository: 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.

Comment thread lib/useLabReportExport.js
Comment on lines +75 to +80
setStatus("done");
// Auto-close after brief success state
setTimeout(() => {
setIsDialogOpen(false);
setStatus("idle");
}, 1800);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add PDF export of complete lab reports including observations and calculations

1 participant