Skip to content

feat: Add Interactive API tile for embedding LARA-compatible interactives [CLUE-333]#2716

Merged
dougmartin merged 11 commits intomasterfrom
CLUE-333-add-stormexplorer-tile
Jan 24, 2026
Merged

feat: Add Interactive API tile for embedding LARA-compatible interactives [CLUE-333]#2716
dougmartin merged 11 commits intomasterfrom
CLUE-333-add-stormexplorer-tile

Conversation

@dougmartin
Copy link
Copy Markdown
Member

Implements a new tile type that embeds external interactive content via iframe using the LARA Interactive API protocol. Enables curriculum authors to integrate educational interactives from question-interactives and other LARA-compatible sources directly into CLUE documents.

Key features:

  • Full LARA Interactive API v1.x protocol support with iframe-phone
  • Bidirectional state management with 500ms debouncing for Firebase efficiency
  • Dynamic iframe height adjustment with loop prevention (100ms debounce, 5px threshold)
  • Separate interactiveState (student work) and authoredState (teacher config)
  • Read-only "report" mode and full "runtime" mode support
  • State polling fallback (2s interval) ensures capture without proactive messages
  • Configurable iframe permissions (geolocation, camera, microphone, etc.)
  • WCAG 2.1 AA accessibility with ARIA attributes and skip-to-content
  • Error boundaries for isolation and loading states with 2s delay
  • Comprehensive logging support forwarding interactive events

Implementation:

  • Content model using MST frozen types for immutable state management
  • 447-line React component handling iframe lifecycle and protocol messages
  • Configurable maxHeight and enableScroll options for layout control
  • 27 unit and integration tests covering state, rendering, and communication

Dependencies added:

  • @concord-consortium/lara-interactive-api for protocol definitions
  • iframe-phone for robust postMessage communication

Documentation:

  • Detailed README with architecture, usage, and troubleshooting guide
  • Implementation spec documenting step-by-step development process
  • Curriculum authoring examples and developer integration guide

…ives [CLUE-333]

Implements a new tile type that embeds external interactive content via
iframe using the LARA Interactive API protocol. Enables curriculum authors
to integrate educational interactives from question-interactives and other
LARA-compatible sources directly into CLUE documents.

Key features:
- Full LARA Interactive API v1.x protocol support with iframe-phone
- Bidirectional state management with 500ms debouncing for Firebase efficiency
- Dynamic iframe height adjustment with loop prevention (100ms debounce, 5px threshold)
- Separate interactiveState (student work) and authoredState (teacher config)
- Read-only "report" mode and full "runtime" mode support
- State polling fallback (2s interval) ensures capture without proactive messages
- Configurable iframe permissions (geolocation, camera, microphone, etc.)
- WCAG 2.1 AA accessibility with ARIA attributes and skip-to-content
- Error boundaries for isolation and loading states with 2s delay
- Comprehensive logging support forwarding interactive events

Implementation:
- Content model using MST frozen types for immutable state management
- 447-line React component handling iframe lifecycle and protocol messages
- Configurable maxHeight and enableScroll options for layout control
- 27 unit and integration tests covering state, rendering, and communication

Dependencies added:
- @concord-consortium/lara-interactive-api for protocol definitions
- iframe-phone for robust postMessage communication

Documentation:
- Detailed README with architecture, usage, and troubleshooting guide
- Implementation spec documenting step-by-step development process
- Curriculum authoring examples and developer integration guide
@dougmartin dougmartin requested review from Copilot January 16, 2026 15:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a new Interactive API tile type that enables embedding LARA-compatible external interactives into CLUE documents via iframe. The tile provides full protocol support for bidirectional state management, dynamic height adjustment, and accessibility features, allowing curriculum authors to integrate educational interactives from question-interactives and other LARA-compatible sources.

Changes:

  • Added new Interactive API tile plugin with complete LARA Interactive API v1.x protocol implementation
  • Integrated iframe-phone for robust postMessage communication with debounced state management
  • Added comprehensive documentation including implementation spec, README, and tile creation guide updates

Reviewed changes

Copilot reviewed 18 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tiles.md Added comprehensive guide for creating new tiles in CLUE with starter tile instructions
src/register-tile-types.ts Registered InteractiveApi tile for dynamic loading
src/public/demo/units/qa/investigation-0/problem-1/introduction/content.json Added three test Interactive API tiles for QA testing
src/public/demo/units/qa/content.json Added Interactive tile to QA unit toolbar
src/plugins/interactive-api/interactive-api-tile.tsx Implemented 447-line React component with iframe lifecycle and LARA protocol handling
src/plugins/interactive-api/interactive-api-tile.test.tsx Added 12 unit tests for component rendering and behavior
src/plugins/interactive-api/interactive-api-tile.scss Implemented styling with accessibility features and loading states
src/plugins/interactive-api/interactive-api-tile-types.ts Defined tile type constants
src/plugins/interactive-api/interactive-api-tile-registration.ts Registered tile content and component info
src/plugins/interactive-api/interactive-api-tile-integration.test.tsx Added 4 integration tests for iframe-phone communication
src/plugins/interactive-api/interactive-api-tile-content.ts Implemented MST content model with frozen state types
src/plugins/interactive-api/interactive-api-tile-content.test.ts Added 15 content model unit tests
src/plugins/interactive-api/README.md Added comprehensive documentation with architecture, usage, and troubleshooting
src/lib/logger-types.ts Added TILE_INTERACTIVE_LOG event type for interactive logging
src/clue/app-config.json Added InteractiveApi to default toolbar configuration
package.json Added lara-interactive-api and iframe-phone dependencies
docs/specs/add-interactive-api-tile.md Added detailed 2500-line implementation specification
README.md Added reference to tile creation documentation
Comments suppressed due to low confidence (1)

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The getInteractiveState message is posted without any data parameter, but line 359 shows it being called with null. For consistency and clarity with the LARA Interactive API protocol, always pass null explicitly as the second parameter when no data is needed.
import React, { useEffect, useRef, useState, useCallback, useMemo } from "react";

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

Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (1)

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The post method for 'getInteractiveState' is called without a second argument, but according to the iframe-phone interface definition (line 22), post expects two parameters: (type: string, data: any). This should pass null as the second argument: phoneRef.current?.post(\"getInteractiveState\", null).
import React, { useEffect, useRef, useState, useCallback, useMemo } from "react";

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

Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx
Comment thread docs/specs/add-interactive-api-tile.md
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 78.08219% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.64%. Comparing base (4edf4fb) to head (04d7f4a).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
...ins/iframe-interactive/iframe-interactive-tile.tsx 73.74% 38 Missing and 9 partials ⚠️
src/register-tile-types.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2716      +/-   ##
==========================================
- Coverage   86.70%   86.64%   -0.06%     
==========================================
  Files         812      816       +4     
  Lines       43535    43754     +219     
  Branches    11137    11186      +49     
==========================================
+ Hits        37746    37911     +165     
- Misses       5451     5496      +45     
- Partials      338      347       +9     
Flag Coverage Δ
cypress ?
cypress-regression 77.89% <6.06%> (-0.37%) ⬇️
cypress-smoke 43.91% <6.06%> (-0.20%) ⬇️
jest 49.40% <77.62%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress
Copy link
Copy Markdown

cypress Bot commented Jan 16, 2026

collaborative-learning    Run #17384

Run Properties:  status check passed Passed #17384  •  git commit 04d7f4a54f: Moved IFRAME_INTERACTIVE_TOOL_CHANGE to be next to other tool change events
Project collaborative-learning
Branch Review CLUE-333-add-stormexplorer-tile
Run status status check passed Passed #17384
Run duration 02m 59s
Commit git commit 04d7f4a54f: Moved IFRAME_INTERACTIVE_TOOL_CHANGE to be next to other tool change events
Committer Doug Martin
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 4
View all changes introduced in this branch ↗︎

- Remove unused _iframeHeight state variable and setIframeHeight call
- Fix closure staleness in loading timeout by removing unnecessary isLoading check
- Improve type safety by renaming baseProps to commonProps and moving linkedInteractives into mode-specific branches
- Fix iframe-phone reconnection loop by using contentRef for authoredState and interactiveState
- Remove unsafe type casting in keyboard event handler by implementing inline handler
Fix test isolation issues in interactive-api-tile.test.tsx by creating
fresh content and model instances for each test via createTestProps()
helper function. This prevents state leakage between tests where URL
changes in one test would affect subsequent tests.

Fix iframe-phone mock setup in interactive-api-tile-integration.test.tsx
by moving jest.mock() call before component import, ensuring the mock is
properly applied. Update polling test to account for initial setup calls
and fix state update test to properly trigger registered listeners.

All 17 tests now pass successfully.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The iframeHeight state variable is set but never used. The component doesn't apply this state to the iframe or container, making it a dead write. Either remove this state variable and the setIframeHeight call, or apply it to control the iframe's height.
import React, { useEffect, useRef, useState, useCallback, useMemo } from "react";

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The onKeyDown handler on line 593 in the spec differs from the implementation shown in the diff (lines 397-404). The spec version uses a concise inline handler onKeyDown={(e) => e.key === 'Enter' && handleSkipToContent(e)}, while the diff shows a more verbose version that properly calls preventDefault(). The spec's inline version doesn't call preventDefault(), which means pressing Enter would both trigger the handler AND follow the href. The diff's implementation is correct. This discrepancy suggests the spec documentation is outdated.
import React, { useEffect, useRef, useState, useCallback, useMemo } from "react";

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

Comment thread src/plugins/iframe-interactive/iframe-interactive-tile.tsx
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/iframe-interactive/iframe-interactive-tile.tsx
@dougmartin dougmartin requested a review from Copilot January 22, 2026 12:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (2)

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The iframeHeight state is declared but never used. The component calls setIframeHeight in handleHeight (line 369) but the state value is not applied anywhere in the render. Consider removing this unused state or integrating it with the height management logic.
/*

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The getInteractiveState polling passes null as the second argument, but according to the spec documentation (line 555 in the spec file), it should be called without any arguments. Consider removing the null parameter for consistency with the documented approach.
/*

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

Comment thread src/plugins/iframe-interactive/iframe-interactive-tile.tsx
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
Comment thread src/plugins/interactive-api/interactive-api-tile.tsx Outdated
@dougmartin dougmartin requested review from Copilot January 22, 2026 13:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 21 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The event handler type in handleSkipToContent is too restrictive. The onKeyDown handler at line 494 passes a React.KeyboardEvent, but this function only accepts React.MouseEvent. Change the parameter type to React.SyntheticEvent to accept both mouse and keyboard events.
/*

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The getInteractiveState message should be sent with null as the second parameter for consistency with the LARA protocol, as shown in the spec at line 437. Change to phoneRef.current?.post('getInteractiveState', null).
/*

src/plugins/interactive-api/interactive-api-tile.tsx:1

  • The inline event handler will fail when Enter is pressed because handleSkipToContent expects React.MouseEvent but receives React.KeyboardEvent. Extract this logic to a separate handler or update handleSkipToContent to accept React.SyntheticEvent.
/*

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

Copy link
Copy Markdown
Member

@scytacki scytacki left a comment

Choose a reason for hiding this comment

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

Great set of code. I requested a few changes. Feel free to argue about them. :)

Comment thread tiles.md Outdated
Comment thread tiles.md Outdated
Comment thread tiles.md
Comment thread tiles.md Outdated
Comment thread tiles.md Outdated
Comment thread src/register-tile-types.ts Outdated
Comment thread src/plugins/iframe-interactive/README.md
Comment thread src/plugins/interactive-api/interactive-api-tile-content.ts Outdated
Comment thread src/lib/logger-types.ts Outdated
Comment thread src/clue/app-config.json Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Code changes following file renames in previous commit:

- Rename tile type from InteractiveApi to IframeInteractive throughout codebase
- Move allowedPermissions from tile content model to unit-level settings
  for improved security (prevents runtime modification by users)
- Rename log event from TILE_INTERACTIVE_LOG to IFRAME_INTERACTIVE_TOOL_CHANGE
  to follow naming conventions
- Remove tile from global app-config.json toolbar (should be enabled per-unit)
- Update test tile IDs in QA content.json to use iframe-interactive prefix

Documentation updates:

- tiles.md: Remove incorrect steps about tile-content-union.ts (auto-generated)
- tiles.md: Add Step 5 for unit configuration best practices
- tiles.md: Fix example references to use starter tile instead of bar-graph
- starter/README.md: Add instructions for unit configuration
- iframe-interactive/README.md: Document unit-level permissions configuration
- add-interactive-api-tile.md: Add note explaining the rename for historical context
@dougmartin
Copy link
Copy Markdown
Member Author

@scytacki I've addressed your review comments in two commits. The first commit is pure git mv results to rename the file folder/files to preserve git history. The second commit, dcbfabf, makes all the other changes.

@dougmartin dougmartin requested a review from scytacki January 23, 2026 11:53
Copy link
Copy Markdown
Member

@scytacki scytacki left a comment

Choose a reason for hiding this comment

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

Looks good. I just left one last nit pick comment.

Comment thread src/lib/logger-types.ts Outdated
@dougmartin dougmartin merged commit 08b2a22 into master Jan 24, 2026
24 of 27 checks passed
@dougmartin dougmartin deleted the CLUE-333-add-stormexplorer-tile branch January 24, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants