Skip to content

enhance: improve embedded PQ display and add postMessage interactions with parent app#5065

Merged
rschlaefli merged 6 commits into
v3from
v3-embedded-pq
Apr 15, 2026
Merged

enhance: improve embedded PQ display and add postMessage interactions with parent app#5065
rschlaefli merged 6 commits into
v3from
v3-embedded-pq

Conversation

@rschlaefli

@rschlaefli rschlaefli commented Apr 14, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Embedded practice-quiz support with parent handshake and continuous quiz-state messages; embedded layout and evaluation toggle controls added.
  • Documentation

    • Added embed behavior guidelines, local-vs-production guidance, and step-by-step local testing procedures.
    • Added a local embed harness (static UI, server, and mock API) to verify handshake, payload stages, and debugging flows.

Use a parent-init handshake and restored completion state so embedded hosts receive stable quiz progress without wildcard postMessage targets.

Add a local embed harness and mock API so the iframe contract can be verified against the branch-local PWA instead of production, which is blocked by CSP frame-ancestors.
Copilot AI review requested due to automatic review settings April 14, 2026 14:43
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Apr 14, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds an iframe embedding mode for PracticeQuiz: threads an embedded prop through components, implements a parent-initiated postMessage handshake and versioned klicker:quiz-state updates with origin validation and localStorage-backed completion persistence, and provides a local embed harness (server, mock API, and UI) for end-to-end verification.

Changes

Cohort / File(s) Summary
Docs / Guidance
AGENTS.md, util/embedded.html
Added embed guidance (parent-initiated postMessage, avoid '*' targetOrigin, rely on ingress frame-ancestors) and replaced external embed with a redirect/link to the local embed harness.
PracticeQuiz Prop Threading
apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx, apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx
Added optional embedded?: boolean prop, passed through PracticeQuiz→ElementStack; adjusted layout/button classes and header/bookmark visibility when embedded.
InstanceHeader UI
apps/frontend-pwa/src/components/practiceQuiz/InstanceHeader.tsx
Added evaluationOpen?: boolean and onToggleEvaluation?: () => void props and a conditional toggle button wired to those props.
Embedded PostMessage Protocol
apps/frontend-pwa/src/pages/course/[courseId]/practiceQuizzes/[id].tsx
Implemented parent-initiated klicker:embed-init handling with origin recording, continuous/versioned klicker:quiz-state posts on progress changes, localStorage-based initial/completion state derivation, and helper type-guards.
Embed Harness: Server & Frontend
util/embed-harness/server.mjs, util/embed-harness/index.html, util/embed-harness/package.json, util/embed-harness/README.md
Added a small static server, an interactive HTML harness to send validated init messages and log klicker:quiz-state payloads, npm scripts, and a README with run/verification steps and localhost-vs-production guidance.
Embed Harness: Mock API
util/embed-harness/mock-api.mjs
Added a mock GraphQL HTTP server with strict origin-based CORS and handlers for GetPracticeQuiz, RespondToElementStack, Self, and related operation resolution.
Tests / Types
packages/graphql/test/helpers.ts
Exported a TestInitializationResult type and made testInitialization explicitly return Promise<TestInitializationResult> to type the returned user contexts.
Utility redirect
util/embedded.html
Replaced direct external embed with a redirect and link to the local embed harness.

Sequence Diagram(s)

sequenceDiagram
    participant Parent as Embed Harness (Parent)
    participant IFrame as PracticeQuiz (iframe)
    participant Storage as localStorage

    Parent->>IFrame: Load iframe with embed=true
    Parent->>IFrame: PostMessage "klicker:embed-init" (strict targetOrigin)
    IFrame->>IFrame: Validate init message & record parentOrigin

    IFrame->>Storage: Read pq-${quizId} completion state

    loop quiz progress updates
        IFrame->>IFrame: Update currentIx / isCompleted
        IFrame->>Parent: PostMessage "klicker:quiz-state" (versioned payload) -> targetOrigin = parentOrigin
        Parent->>Parent: Log & display latest payload
    end

    IFrame->>Storage: Persist completion when finished
    IFrame->>Parent: PostMessage final "klicker:quiz-state" (completed=true)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: enhancing embedded practice quiz display and adding postMessage communication with parent applications.

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


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.

@dosubot dosubot Bot added the enhancement label Apr 14, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copilot AI left a comment

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the embedded practice-quiz integration by introducing a parent-initiated postMessage handshake (no wildcard target origins), emitting a versioned klicker:quiz-state payload, and adding a local embed-harness to verify the contract against a branch-local PWA.

Changes:

  • Added embedded messaging handshake + versioned quiz-state payload, plus completion restoration from persisted progress.
  • Threaded embedded mode through the practice-quiz flow and adjusted embedded CTA layout/spacing.
  • Added util/embed-harness/ (static page + local server + mock GraphQL API + docs) and redirected util/embedded.html to it.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
util/embedded.html Redirects the older utility page to the new local embed harness.
util/embed-harness/server.mjs Adds a tiny local HTTP server to serve the harness HTML.
util/embed-harness/package.json Adds scripts to run the harness and mock API.
util/embed-harness/mock-api.mjs Adds a local mock GraphQL server for embed-flow verification.
util/embed-harness/index.html Adds a static parent harness page to load an iframe and inspect postMessage traffic.
util/embed-harness/README.md Documents how to run/verify the local harness against the branch-local PWA.
packages/graphql/test/helpers.ts Adds an explicit return type to fix TS2742 during type-check.
apps/frontend-pwa/src/pages/course/[courseId]/practiceQuizzes/[id].tsx Implements embedded handshake + state emission + completion restoration; documents the protocol.
apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx Threads embedded prop through to element stacks.
apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx Adjusts embedded CTA positioning and adds bottom padding in embedded mode.
AGENTS.md Documents the embedded messaging trust boundary and harness usage constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread util/embed-harness/index.html
Comment thread util/embed-harness/mock-api.mjs
Comment on lines +23 to +30
}

res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader(
'Access-Control-Allow-Headers',
'content-type, authorization, x-graphql-yoga-csrf'
)
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

Access-Control-Allow-Credentials: true is set even when the request Origin is not allowed (i.e., when Access-Control-Allow-Origin is not set). While browsers will still block credentialed reads without a matching Allow-Origin, it's safer and clearer to only emit Allow-Credentials (and potentially the other CORS headers) when origin === allowedOrigin.

Suggested change
}
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader(
'Access-Control-Allow-Headers',
'content-type, authorization, x-graphql-yoga-csrf'
)
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader(
'Access-Control-Allow-Headers',
'content-type, authorization, x-graphql-yoga-csrf'
)
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
}

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx (1)

155-167: ⚠️ Potential issue | 🟡 Minor

Thread embedded prop to PracticeQuizOverview and apply fixed-button styling in the overview CTA.

PracticeQuizOverview does not currently accept an embedded prop (lines 18–28 of its interface definition). The overview button at lines 164–171 uses only static TailwindCSS utilities and does not opt into the fixed-button/padding behavior for embedded contexts. Without threading embedded through to this component, the overview screen remains the one step in the embedded flow that cannot adjust its CTA positioning for small iframes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx` around lines
155 - 167, The PracticeQuizOverview call inside PracticeQuiz (the currentIx ===
-1 branch) must pass the embedded boolean down so the overview can adjust its
CTA for embedded contexts; update the PracticeQuizOverview props (its
interface/type) to include embedded?: boolean, thread embedded={embedded} in the
PracticeQuiz JSX where PracticeQuizOverview is rendered, and inside the
PracticeQuizOverview component modify the overview CTA/button render logic to
apply the fixed-button/padding styling when props.embedded is true (toggle the
Tailwind classes used for fixed-button behavior based on embedded).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/frontend-pwa/src/pages/course/`[courseId]/practiceQuizzes/[id].tsx:
- Around line 155-160: The posting effect is emitting an initial "overview"
before persisted completion is restored; fix by ensuring completion is
derived/restored before any post runs: either initialize isCompleted
synchronously using readStoredCompletion when data?.practiceQuiz and id are
present (e.g. useState(() => readStoredCompletion(id, stackIdsDerivedFrom
data.practiceQuiz))) or add a completionRestored flag set true only after
readStoredCompletion updates setIsCompleted in the existing useEffect, then
guard the overview-posting useEffect (the effect that sends the overview payload
around lines 168-192) to run only when completionRestored is true (and embedded
&& data?.practiceQuiz). Reference readStoredCompletion, setIsCompleted and the
posting useEffect when making the change.

In `@util/embed-harness/index.html`:
- Around line 641-673: The loadIframe function currently accepts any normalized
origin from user input; enforce a strict allowlist by validating
normalizedUrl.origin equals "http://127.0.0.1:3101" before setting
state.iframeOrigin/state.iframeUrl and before assigning elements.quizFrame.src;
if the origin does not match, set state.iframeOrigin and state.iframeUrl to
null, call setInitState(false), append an 'error' log describing the forbidden
origin (include the provided URL) and call updateStatus(), thereby
hard-rejecting non-local origins in loadIframe.

In `@util/embed-harness/mock-api.mjs`:
- Around line 30-31: Update the CORS "Access-Control-Allow-Methods" header to
include GET so it matches server behavior: replace occurrences of
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') with a value that
includes 'GET' (e.g., 'GET, POST, OPTIONS') in both places in
util/embed-harness/mock-api.mjs (the res.setHeader calls around line ~30 and the
other occurrence around lines ~221-224).

In `@util/embed-harness/package.json`:
- Line 2: The package.json "name" field currently uses an unscoped value
("klicker-embed-harness"); update it to follow the org scope pattern by changing
the "name" value to "@klicker-uzh/klicker-embed-harness" and ensure any internal
references (build scripts, import paths or CI/publish configs that reference
"klicker-embed-harness") are updated to the new scoped name; locate the field in
util/embed-harness/package.json and adjust any occurrences in package.json
scripts or repo tooling that reference the old name.

---

Outside diff comments:
In `@apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx`:
- Around line 155-167: The PracticeQuizOverview call inside PracticeQuiz (the
currentIx === -1 branch) must pass the embedded boolean down so the overview can
adjust its CTA for embedded contexts; update the PracticeQuizOverview props (its
interface/type) to include embedded?: boolean, thread embedded={embedded} in the
PracticeQuiz JSX where PracticeQuizOverview is rendered, and inside the
PracticeQuizOverview component modify the overview CTA/button render logic to
apply the fixed-button/padding styling when props.embedded is true (toggle the
Tailwind classes used for fixed-button behavior based on embedded).
🪄 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: ca7a224d-fe44-47fb-9794-b779f6568778

📥 Commits

Reviewing files that changed from the base of the PR and between 23588c9 and 80c67a4.

📒 Files selected for processing (11)
  • AGENTS.md
  • apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx
  • apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx
  • apps/frontend-pwa/src/pages/course/[courseId]/practiceQuizzes/[id].tsx
  • packages/graphql/test/helpers.ts
  • util/embed-harness/README.md
  • util/embed-harness/index.html
  • util/embed-harness/mock-api.mjs
  • util/embed-harness/package.json
  • util/embed-harness/server.mjs
  • util/embedded.html

Comment on lines +155 to +160
useEffect(() => {
if (!embedded || !data?.practiceQuiz) return

const stackIds = data.practiceQuiz.stacks?.map((stack) => stack.id) ?? []
setIsCompleted(readStoredCompletion(id, stackIds))
}, [embedded, id, data?.practiceQuiz])

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

Avoid sending an initial overview payload before completion is restored.

On a reload of a completed embedded quiz, isCompleted is still false on the first render with practiceQuiz data. The restore effect updates it afterward, but this posting effect runs in the same commit and emits overview once before the rerender emits completed. Any parent that reacts to the first payload still sees the wrong state.

Gate the first emit until persisted completion has been restored, or derive the restored completion state before posting the first message.

Also applies to: 168-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend-pwa/src/pages/course/`[courseId]/practiceQuizzes/[id].tsx
around lines 155 - 160, The posting effect is emitting an initial "overview"
before persisted completion is restored; fix by ensuring completion is
derived/restored before any post runs: either initialize isCompleted
synchronously using readStoredCompletion when data?.practiceQuiz and id are
present (e.g. useState(() => readStoredCompletion(id, stackIdsDerivedFrom
data.practiceQuiz))) or add a completionRestored flag set true only after
readStoredCompletion updates setIsCompleted in the existing useEffect, then
guard the overview-posting useEffect (the effect that sends the overview payload
around lines 168-192) to run only when completionRestored is true (and embedded
&& data?.practiceQuiz). Reference readStoredCompletion, setIsCompleted and the
posting useEffect when making the change.

Comment on lines +641 to +673
function loadIframe() {
const normalizedUrl = normalizeIframeUrl(elements.quizUrl.value)

if (!normalizedUrl) {
state.iframeOrigin = null
state.iframeUrl = null
setInitState(false)
appendLog(
'error',
'Failed to load iframe because the URL is invalid.',
{
input: elements.quizUrl.value,
}
)
updateStatus()
return
}

state.iframeOrigin = normalizedUrl.origin
state.iframeUrl = normalizedUrl.toString()
state.lastAcceptedType = null
state.latestPayload = null
setInitState(false)

elements.quizUrl.value = state.iframeUrl
elements.quizFrame.src = state.iframeUrl

appendLog('outbound', 'Loaded iframe URL.', {
iframeOrigin: state.iframeOrigin,
iframeUrl: state.iframeUrl,
})
updateStatus()
}

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

Enforce local-origin-only iframe loading in the harness.

The harness currently accepts any origin from user input, so it can still load non-local targets (including production). Please hard-reject origins other than http://127.0.0.1:3101 in loadIframe.

💡 Proposed fix
 function loadIframe() {
   const normalizedUrl = normalizeIframeUrl(elements.quizUrl.value)
+  const localOrigin = 'http://127.0.0.1:3101'

   if (!normalizedUrl) {
@@
     return
   }
+
+  if (normalizedUrl.origin !== localOrigin) {
+    state.iframeOrigin = null
+    state.iframeUrl = null
+    setInitState(false)
+    appendLog('error', 'Only the local branch PWA origin is allowed.', {
+      expectedOrigin: localOrigin,
+      receivedOrigin: normalizedUrl.origin,
+    })
+    updateStatus()
+    return
+  }

   state.iframeOrigin = normalizedUrl.origin
As per coding guidelines: `util/embed-harness/**`: Local embed harness should target branch-local PWA (`http://127.0.0.1:3101/...`), not production URL.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/embed-harness/index.html` around lines 641 - 673, The loadIframe
function currently accepts any normalized origin from user input; enforce a
strict allowlist by validating normalizedUrl.origin equals
"http://127.0.0.1:3101" before setting state.iframeOrigin/state.iframeUrl and
before assigning elements.quizFrame.src; if the origin does not match, set
state.iframeOrigin and state.iframeUrl to null, call setInitState(false), append
an 'error' log describing the forbidden origin (include the provided URL) and
call updateStatus(), thereby hard-rejecting non-local origins in loadIframe.

Comment on lines +30 to +31
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
}

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

CORS allow-methods should include GET to match server behavior.

The server accepts GET but CORS only advertises POST, OPTIONS. Add GET to avoid cross-origin method mismatch.

💡 Proposed fix
-  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
+  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')

Also applies to: 221-224

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/embed-harness/mock-api.mjs` around lines 30 - 31, Update the CORS
"Access-Control-Allow-Methods" header to include GET so it matches server
behavior: replace occurrences of res.setHeader('Access-Control-Allow-Methods',
'POST, OPTIONS') with a value that includes 'GET' (e.g., 'GET, POST, OPTIONS')
in both places in util/embed-harness/mock-api.mjs (the res.setHeader calls
around line ~30 and the other occurrence around lines ~221-224).

@@ -0,0 +1,9 @@
{
"name": "klicker-embed-harness",

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

Scope the harness package name under @klicker-uzh.

klicker-embed-harness breaks the repo’s package-name convention. Keeping the scope consistent avoids one-off package identities, even for private tooling.

🔧 Proposed fix
-  "name": "klicker-embed-harness",
+  "name": "@klicker-uzh/embed-harness",

As per coding guidelines, "Package names must follow the pattern @klicker-uzh/<name>."

📝 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
"name": "klicker-embed-harness",
"name": "@klicker-uzh/embed-harness",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/embed-harness/package.json` at line 2, The package.json "name" field
currently uses an unscoped value ("klicker-embed-harness"); update it to follow
the org scope pattern by changing the "name" value to
"@klicker-uzh/klicker-embed-harness" and ensure any internal references (build
scripts, import paths or CI/publish configs that reference
"klicker-embed-harness") are updated to the new scoped name; locate the field in
util/embed-harness/package.json and adjust any occurrences in package.json
scripts or repo tooling that reference the old name.

@cypress

cypress Bot commented Apr 14, 2026

Copy link
Copy Markdown

klicker-uzh    Run #6683

Run Properties:  status check passed Passed #6683  •  git commit 02ccd4f7ed ℹ️: Merge 550f39de2516610838756f688930db7eaaa41c12 into 0dec1fa0e49ca0b8631d2cee39d5...
Project klicker-uzh
Branch Review v3-embedded-pq
Run status status check passed Passed #6683
Run duration 10m 12s
Commit git commit 02ccd4f7ed ℹ️: Merge 550f39de2516610838756f688930db7eaaa41c12 into 0dec1fa0e49ca0b8631d2cee39d5...
Committer Roland Schläfli
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 758
View all changes introduced in this branch ↗︎

@rschlaefli rschlaefli changed the title V3 embedded pq enhance: improve embedded PQ display and add postMessage interactions with parent app Apr 14, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx (2)

359-373: Consider extracting the toggle callback to reduce re-renders.

The inline callback is recreated on every render for each element. While this only affects embedded mode with existing evaluations, extracting it to a stable callback could improve performance if the element list is large.

♻️ Optional: Extract toggle callback
+  const handleToggleEvaluation = (elementId: number) => {
+    setOpenEvaluations((prev) => {
+      const next = new Set(prev)
+      if (next.has(elementId)) {
+        next.delete(elementId)
+      } else {
+        next.add(elementId)
+      }
+      return next
+    })
+  }
+
   // ... in the map function:
   onToggleEvaluation={
     embedded && stackStorage?.[element.id]?.evaluation
-      ? () =>
-          setOpenEvaluations((prev) => {
-            const next = new Set(prev)
-            if (next.has(element.id)) {
-              next.delete(element.id)
-            } else {
-              next.add(element.id)
-            }
-            return next
-          })
+      ? () => handleToggleEvaluation(element.id)
       : undefined
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx` around lines
359 - 373, The inline onToggleEvaluation lambda causes a new function per
element render; extract it into a stable callback to avoid re-renders when
embedded and stackStorage?.[element.id]?.evaluation is true. Create a memoized
handler (e.g., toggleEvaluation or handleToggleEvaluation) that accepts an
elementId and calls setOpenEvaluations(prev => { const next = new Set(prev);
next.has(elementId) ? next.delete(elementId) : next.add(elementId); return next;
}); then pass a pre-bound version (or use useCallback with element.id) to the
onToggleEvaluation prop only when embedded &&
stackStorage?.[element.id]?.evaluation is truthy, preserving evaluationOpen and
the conditional behavior.

381-381: Clarify the preview prop semantics.

The logic preview={embedded && !openEvaluations.has(element.id)} means:

  • Non-embedded mode: preview is always false
  • Embedded mode: preview is true by default, false when evaluation is toggled open

This naming feels inverted—when the user clicks to "open" the evaluation, preview becomes false. Consider whether the prop name or the logic should be adjusted for clarity, or add a comment explaining the intent.

💡 Consider adding a clarifying comment
 <StudentElement
   element={element}
   elementIx={elementIx}
   studentResponse={studentResponse}
   setStudentResponse={setStudentResponse}
   stackStorage={stackStorage}
+  // In embedded mode, show preview (hide evaluation) by default;
+  // toggle to full view when user clicks the evaluation button
   preview={embedded && !openEvaluations.has(element.id)}
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx` at line 381,
The prop name/logic is confusing: `preview={embedded &&
!openEvaluations.has(element.id)}` makes preview true only for embedded elements
when their evaluation is closed; either rename the prop to reflect that (e.g.,
`isPreviewMode` or `showPreviewWhenCollapsed`) and update all consumers, or keep
the prop name and add a concise inline comment above this prop explaining the
semantics ("preview is true for embedded elements while their evaluation is
closed — toggling open sets preview=false"), and ensure the expression uses
`embedded && !openEvaluations.has(element.id)` (reference: the `preview` prop,
`embedded` variable and `openEvaluations.has(element.id)` in ElementStack).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx`:
- Around line 359-373: The inline onToggleEvaluation lambda causes a new
function per element render; extract it into a stable callback to avoid
re-renders when embedded and stackStorage?.[element.id]?.evaluation is true.
Create a memoized handler (e.g., toggleEvaluation or handleToggleEvaluation)
that accepts an elementId and calls setOpenEvaluations(prev => { const next =
new Set(prev); next.has(elementId) ? next.delete(elementId) :
next.add(elementId); return next; }); then pass a pre-bound version (or use
useCallback with element.id) to the onToggleEvaluation prop only when embedded
&& stackStorage?.[element.id]?.evaluation is truthy, preserving evaluationOpen
and the conditional behavior.
- Line 381: The prop name/logic is confusing: `preview={embedded &&
!openEvaluations.has(element.id)}` makes preview true only for embedded elements
when their evaluation is closed; either rename the prop to reflect that (e.g.,
`isPreviewMode` or `showPreviewWhenCollapsed`) and update all consumers, or keep
the prop name and add a concise inline comment above this prop explaining the
semantics ("preview is true for embedded elements while their evaluation is
closed — toggling open sets preview=false"), and ensure the expression uses
`embedded && !openEvaluations.has(element.id)` (reference: the `preview` prop,
`embedded` variable and `openEvaluations.has(element.id)` in ElementStack).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e6a3776-521d-4fb3-ac64-caaad1ed04a4

📥 Commits

Reviewing files that changed from the base of the PR and between 80c67a4 and a692127.

📒 Files selected for processing (3)
  • apps/frontend-pwa/src/components/practiceQuiz/ElementStack.tsx
  • apps/frontend-pwa/src/components/practiceQuiz/InstanceHeader.tsx
  • apps/frontend-pwa/src/components/practiceQuiz/PracticeQuiz.tsx

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@sonarqubecloud

Copy link
Copy Markdown

@rschlaefli
rschlaefli enabled auto-merge (squash) April 15, 2026 10:18
@rschlaefli
rschlaefli disabled auto-merge April 15, 2026 10:40
@rschlaefli
rschlaefli merged commit 745d634 into v3 Apr 15, 2026
61 of 65 checks passed
@rschlaefli
rschlaefli deleted the v3-embedded-pq branch April 15, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement size:XXL This PR changes 1000+ lines, ignoring generated files.

Development

Successfully merging this pull request may close these issues.

3 participants