Skip to content

Conversation

@cubap
Copy link
Member

@cubap cubap commented Jan 12, 2026

Swapped the default components for /transcribe and /transcribe-simple interfaces, making tpen-simple-transcription the default for /transcribe and tpen-transcription-interface for /transcribe-simple. Standardized interface selection using a data attribute for tool components. Improved event handling for layer changes and error feedback in simple transcription, including toast notifications for loading errors and empty pages. Updated layer-selector to dispatch a new event for layer changes and maintained backward compatibility.

Swapped the default components for /transcribe and /transcribe-simple interfaces, making tpen-simple-transcription the default for /transcribe and tpen-transcription-interface for /transcribe-simple. Standardized interface selection using a data attribute for tool components. Improved event handling for layer changes and error feedback in simple transcription, including toast notifications for loading errors and empty pages. Updated layer-selector to dispatch a new event for layer changes and maintained backward compatibility.
@github-actions
Copy link
Contributor

github-actions bot commented Jan 12, 2026

cubap added 9 commits January 12, 2026 13:46
Introduces a 'tpen-column-selected' event dispatched from the column selector and handled in both simple and standard transcription interfaces. This allows the active line to update based on column selection, improving integration between the column selector and transcription components while maintaining legacy support.
Eliminated code related to the old transcription interface, including the getTpenNodes and applyLineSelection methods, and removed their usage from selectColumn. This streamlines the component and reduces maintenance of deprecated functionality.
Update the 'tpen-column-selected' event listener to correctly destructure the event detail, ensuring columnData is properly accessed.
Updated links in both SimpleTranscriptionInterface and TranscriptionInterface to use 'projectID' instead of 'projectId' as the query parameter when directing users to manage their project. This ensures consistency and correct routing.
In both SimpleTranscriptionInterface and TranscriptionInterface, if no active tool is set, the first available tool from the project's tools list is now selected by default. This ensures the right pane always displays a tool if any are available.
This is because we're using a cheap version of Vault and will be fixed later
@thehabes
Copy link
Member

thehabes commented Jan 13, 2026

Claude Code deep-code-reviewer Subagent Review

Branch: 384-swap-in-transcription-alt-after-confirming-its-functionality-as-we-build-out-canvas-panel
Review Date: 2026-01-13
Files Changed: 10

Summary

This PR refactors the transcription interfaces by:

  • Swapping the default components for /transcribe and /transcribe-simple routes
  • Introducing event-based communication for layer and column selection
  • Adding a data-interface-type attribute for standardized component discovery
  • Adding IIIF Presentation API v2 compatibility alongside existing v3 support
  • Improving error feedback via toast notifications
  • Adding message handling from tools for line navigation
Category Issues Found
Critical 2
Major 4
Minor 5
Suggestions 4

Critical Issues

1. Layer Selector Value Mismatch Bug

File: components/layer-selector/index.js:89
Category: Logic Error

The layer selector uses layer.URI to find the selected layer, but the option values are set to layer["@id"]. This will cause layer selection to silently fail because selectedLayer will always be undefined.

Current Code:

const selectedURI = e.target.value
const selectedLayer = this.layers.find((layer) => layer.URI === selectedURI)

Suggested Fix:

const selectedURI = e.target.value
const selectedLayer = this.layers.find((layer) => layer["@id"] === selectedURI)

2. Event Detail Destructuring Inconsistency

File: components/simple-transcription/index.js:376
Category: Logic Error

The tpen-column-selected event handler incorrectly destructures the event data. The dispatcher wraps the payload in a detail property via CustomEvent, but this handler destructures incorrectly, causing columnData.lineIndex to be undefined.

Current Code:

TPEN.eventDispatcher.on('tpen-column-selected', ({detail: columnData}) => {
    if (typeof columnData.lineIndex === 'number') {
      TPEN.activeLineIndex = columnData.lineIndex
      this.updateLines()
    }
})

Suggested Fix:

TPEN.eventDispatcher.on('tpen-column-selected', (event) => {
    const columnData = event.detail
    if (typeof columnData?.lineIndex === 'number') {
      TPEN.activeLineIndex = columnData.lineIndex
      this.updateLines()
    }
})

Major Issues

3. Event Listener Memory Leak in TranscriptionInterface

File: interfaces/transcription/index.js:247-258
Category: Memory Leak / Missing Cleanup

The TranscriptionInterface class adds event listeners for tpen-layer-changed and tpen-column-selected but never removes them. This class lacks a disconnectedCallback() entirely, which could cause memory leaks if the component is removed and re-added to the DOM.

Suggested Fix:

Store handler references and add cleanup:

// In addEventListeners():

this._layerChangeHandler = (layerData) => {
    this.updateLines()
}
this._columnSelectedHandler = (event) => {
    const columnData = event.detail
    if (typeof columnData?.lineIndex === 'number') {
      TPEN.activeLineIndex = columnData.lineIndex
      this.updateLines()
    }
}
TPEN.eventDispatcher.on('tpen-layer-changed', this._layerChangeHandler)
TPEN.eventDispatcher.on('tpen-column-selected', this._columnSelectedHandler)

// Add new method:
disconnectedCallback() {
    if (this._layerChangeHandler) {
      TPEN.eventDispatcher.off('tpen-layer-changed', this._layerChangeHandler)
    }
    if (this._columnSelectedHandler) {
      TPEN.eventDispatcher.off('tpen-column-selected', this._columnSelectedHandler)
    }
}

4. PostMessage Security - Using Wildcard Origin

File: components/simple-transcription/index.js:826-835
Category: Security Concern

Multiple postMessage calls use "*" as the target origin, which could leak sensitive data to malicious iframes if a tool is compromised.

Suggested Fix:

const toolOrigin = new URL(tool.url).origin
iframe.contentWindow?.postMessage(
    {
      type: "MANIFEST_CANVAS_ANNOTATIONPAGE_ANNOTATION",
      // ...
    },
    toolOrigin
)

5. Missing Origin Validation in Message Handler

File: components/simple-transcription/index.js:881-910
Category: Security Concern

The #handleToolMessages method processes incoming messages from any origin without validation.

Suggested Fix:

#handleToolMessages(event) {
    // Validate message origin against loaded tools
    const toolOrigins = TPEN.activeProject?.tools
      ?.filter(t => t.url)
      ?.map(t => new URL(t.url).origin) ?? []

    if (!toolOrigins.includes(event.origin)) {
      return // Ignore messages from unknown origins
    }

    // ... rest of handler
}

6. Duplicate Script Loading Prevention Missing

File: interfaces/transcription/index.js:324-341
Category: Bug / Race Condition

The loadRightPaneContent() method loads tool scripts without checking if they're already being loaded. This differs from SimpleTranscriptionInterface which properly checks for existing scripts.

Suggested Fix:

const scriptId = `tool-script-${tool.toolName}`
const existingScript = document.getElementById(scriptId)
if (existingScript) {
    return
}
const script = document.createElement('script')
script.type = 'module'
script.src = tool.url
script.id = scriptId
script.onload = () => {
    rightPane.innerHTML = `<${tagName}></${tagName}>`
}
document.head.appendChild(script)

Minor Issues

7. Debug Statement Left in Production Code

File: components/simple-transcription/index.js:790

console.debug('Loading tool:', { tool, tagName: tool.custom?.tagName, url: tool.url, location: tool.location })

Remove this debug statement or wrap in a development mode check.


8. Inconsistent Event Handler Data Access

Files: Multiple

The codebase has inconsistent patterns for accessing event data:

  • SimpleTranscriptionInterface uses ({detail: columnData}) (incorrect)
  • TranscriptionInterface uses (columnData) (also incorrect)
  • Both should use (event) => { const data = event.detail; ... }

9. Toast Notification May Fire Repeatedly

File: components/simple-transcription/index.js:544-548

The info toast "This page has no line annotations" fires every time updateLines() is called on an empty page. Consider tracking whether the toast was already shown for the current page.


10. Missing Null Check in Layer Change Handler

File: components/simple-transcription/index.js:369-373

The handler resets activeLineIndex to 0 but doesn't verify if the new layer has any pages or items.


Suggestions

  1. Add JSDoc Documentation for the new events tpen-layer-changed and tpen-column-selected
  2. Extract Shared Logic between SimpleTranscriptionInterface and TranscriptionInterface into a base class
  3. Add Unit Tests for the new event-based communication
  4. Consider TypeScript or JSDoc type definitions for event payloads

Positive Observations

  • Good architectural decision: Moving to event-based communication improves component decoupling and testability
  • IIIF compatibility: Adding support for Presentation API v2 alongside v3 is thoughtful
  • Error feedback: New toast notifications provide clear user feedback
  • Standardized interface discovery: The data-interface-type="transcription" attribute is cleaner than tag-name checking
  • Default tool selection: Auto-selecting the first available tool improves UX

Checklist

  • Fix Critical Issue 1: Layer selector value mismatch
  • Fix Critical Issue 2: Event detail destructuring
  • Add disconnectedCallback to TranscriptionInterface
  • Review postMessage security improvements
  • Fix duplicate script loading prevention
  • Remove console.debug statement
  • Run full test suite after changes
  • Manual test layer and column selection in both interfaces

Updated event handler signatures for 'tpen-column-selected' to consistently use event.detail. In TranscriptionInterface, event handlers are now stored as instance properties and properly removed in disconnectedCallback to prevent memory leaks.
cubap added 2 commits January 13, 2026 13:29
Introduces state tracking to avoid showing duplicate toast/alert messages when a page has no line annotations in both SimpleTranscriptionInterface and TranscriptionInterface. Also ensures notification state is cleared when lines are present, and improves event detail handling and tool script loading logic.
Consolidates and enhances handling of tool messages for line navigation in both SimpleTranscriptionInterface and TranscriptionInterface. Now supports multiple message types and flexible line ID matching (full URI or ID part). Also ensures proper event listener cleanup and updates tool filtering logic in SplitscreenTool to default to enabled when custom.enabled is not set.
@CenterForDigitalHumanities CenterForDigitalHumanities deleted a comment from Copilot AI Jan 14, 2026
@thehabes
Copy link
Member

@cubap

Static Review Comments

Branch: 384-swap-in-transcription-alt-after-confirming-its-functionality-as-we-build-out-canvas-panel
Review Date: 2026-01-14
Files Changed: 11

Summary

This PR refactors transcription interfaces by swapping the default components between /transcribe and /transcribe-simple, standardizes interface selection using a data-interface-type attribute, and improves event handling for layer/column changes. The changes add toast notifications for error feedback and prevent repeated "no lines" notifications.

Category Issues Found
🔴 Critical 0
🟠 Major 4
🟡 Minor 4
🔵 Suggestions 2

Major Issues 🟠

Issue 1: Memory Leak - Missing Event Cleanup in SimpleTranscriptionInterface

File: components/simple-transcription/index.js:371-388
Category: Memory Leak

Problem:
The tpen-layer-changed and tpen-column-selected event handlers are registered in addEventListeners() but are NOT cleaned up in disconnectedCallback(). Meanwhile, the same events are properly cleaned up in interfaces/transcription/index.js. This creates memory leaks when the component is removed and re-added to the DOM.

Current Code:

// In addEventListeners() - handlers registered as anonymous functions
TPEN.eventDispatcher.on('tpen-layer-changed', (event) => {
  const layerData = event?.detail
  if (this.#page?.items?.length > 0) {
    TPEN.activeLineIndex = 0
  }
  this.updateLines()
})

TPEN.eventDispatcher.on('tpen-column-selected', (ev) => {
  const columnData = ev.detail
  if (typeof columnData.lineIndex === 'number') {
    TPEN.activeLineIndex = columnData.lineIndex
    this.updateLines()
  }
})

Suggested Fix:
Store handler references and clean them up like interfaces/transcription/index.js does:

// In addEventListeners():
this._layerChangeHandler = (event) => {
  const layerData = event?.detail
  if (this.#page?.items?.length > 0) {
    TPEN.activeLineIndex = 0
  }
  this.updateLines()
}
TPEN.eventDispatcher.on('tpen-layer-changed', this._layerChangeHandler)

this._columnSelectedHandler = (ev) => {
  const columnData = ev.detail
  if (typeof columnData.lineIndex === 'number') {
    TPEN.activeLineIndex = columnData.lineIndex
    this.updateLines()
  }
}
TPEN.eventDispatcher.on('tpen-column-selected', this._columnSelectedHandler)

// In disconnectedCallback():
if (this._layerChangeHandler) {
  TPEN.eventDispatcher.off('tpen-layer-changed', this._layerChangeHandler)
}
if (this._columnSelectedHandler) {
  TPEN.eventDispatcher.off('tpen-column-selected', this._columnSelectedHandler)
}

How to Verify:

  1. Mount the component, switch layers, unmount, remount
  2. Check browser DevTools memory snapshots for retained listeners
  3. Verify no duplicate event firings occur

Issue 2: Memory Leak - Uncleaned Event Listeners in TranscriptionInterface

File: interfaces/transcription/index.js:402-413
Category: Memory Leak

Problem:
The tpen-transcription-previous-line and tpen-transcription-next-line event handlers registered in loadRightPaneContent() are anonymous functions that are never cleaned up. Each call to loadRightPaneContent() adds new listeners without removing old ones.

Current Code:

TPEN.eventDispatcher.on('tpen-transcription-previous-line', () => {
  iframe.contentWindow?.postMessage(
    { type: "SELECT_ANNOTATION", lineId: this.#page?.items?.[TPEN.activeLineIndex]?.id },
    "*"
  )
})
TPEN.eventDispatcher.on('tpen-transcription-next-line', () => {
  iframe.contentWindow?.postMessage(
    { type: "SELECT_ANNOTATION", lineId: this.#page?.items?.[TPEN.activeLineIndex]?.id },
    "*"
  )
})

Suggested Fix:
Store references and clean them up (like SimpleTranscriptionInterface does with #toolLineListeners):

// Clean up previous listeners before adding new ones
this.#cleanupToolLineListeners()

const sendLineSelection = () => {
  const activeLineId = this.#page?.items?.[TPEN.activeLineIndex]?.id ?? null
  iframe.contentWindow?.postMessage(
    { type: "SELECT_ANNOTATION", lineId: activeLineId },
    "*"
  )
}

this.#toolLineListeners = sendLineSelection
TPEN.eventDispatcher.on('tpen-transcription-previous-line', sendLineSelection)
TPEN.eventDispatcher.on('tpen-transcription-next-line', sendLineSelection)

How to Verify:

  1. Open splitscreen multiple times with different tools
  2. Check if postMessage is fired multiple times per navigation event
  3. Monitor EventDispatcher listener count

Issue 3: Memory Leak - Window Event Listeners Not Cleaned Up

File: components/simple-transcription/index.js:365-367 and interfaces/transcription/index.js:247-249
Category: Memory Leak

Problem:
The window.addEventListener('keydown', ...) handler added in addEventListeners() is never removed in disconnectedCallback(). This means pressing Escape will try to call methods on a potentially removed component.

Current Code:

window.addEventListener('keydown', e => {
  if (e.key === 'Escape') closeSplitscreen()
})

Suggested Fix:
Store and clean up the handler:

// In addEventListeners():
this._escapeHandler = (e) => {
  if (e.key === 'Escape') closeSplitscreen()
}
window.addEventListener('keydown', this._escapeHandler)

// In disconnectedCallback():
if (this._escapeHandler) {
  window.removeEventListener('keydown', this._escapeHandler)
}

How to Verify:

  1. Mount and unmount the component
  2. Press Escape key after unmounting
  3. Check for console errors about accessing properties of removed elements

Issue 4: Security - postMessage with Wildcard Origin

File: Multiple locations
Category: Security

Problem:
Using postMessage(data, '*') broadcasts messages to ALL windows/iframes, including potentially malicious ones. This could leak sensitive data like annotation IDs, manifest URLs, and canvas information.

Locations:

  • components/simple-transcription/index.js:853-862
  • components/simple-transcription/index.js:865-871
  • components/simple-transcription/index.js:873-879
  • components/simple-transcription/index.js:887-890
  • interfaces/transcription/index.js:376-386
  • interfaces/transcription/index.js:388-393
  • interfaces/transcription/index.js:395-400
  • interfaces/transcription/index.js:403-406
  • interfaces/transcription/index.js:408-411

Current Code:

iframe.contentWindow?.postMessage(
  {
    type: "MANIFEST_CANVAS_ANNOTATIONPAGE_ANNOTATION",
    manifest: TPEN.activeProject?.manifest?.[0] ?? '',
    canvas: this.#canvas?.id ?? this.#canvas?.['@id'] ?? this.#canvas ?? '',
    annotationPage: this.fetchCurrentPageId() ?? this.#page?.id ?? '',
    annotation: TPEN.activeLineIndex >= 0 ? this.#page?.items?.[TPEN.activeLineIndex]?.id ?? null : null,
    columns: projectPage?.columns || []
  },
  '*'
)

Suggested Fix:
Use the tool's origin when available:

// Store the tool origin when creating the iframe
const toolOrigin = new URL(tool.url).origin

iframe.contentWindow?.postMessage(
  { /* ... */ },
  toolOrigin
)

How to Verify:

  1. Check that messages are only received by intended iframes
  2. Test with tools hosted on different origins
  3. Review browser console for CORS-related warnings

Minor Issues 🟡

Issue 1: Code Duplication - Identical handleToolMessages Method

File: components/simple-transcription/index.js:909-932 and interfaces/transcription/index.js:429-452
Category: Code Hygiene

Problem:
The #handleToolMessages method is completely duplicated between both interfaces. This violates DRY principles and makes maintenance harder.

How to Verify:
Compare the two methods - they are identical.


Issue 2: Empty Event Handlers

File: components/simple-transcription/index.js:49-51 and 60-61
Category: Dead Code

Problem:
Empty event handlers for drawer-opening and drawer-closing serve no purpose.

Current Code:

this.addEventListener('drawer-opening', () => {
})

this.addEventListener('drawer-closing', () => {
})

Suggested Fix:
Remove the empty handlers or add TODO comments explaining future intent.

How to Verify:
Confirm drawer functionality works without these handlers.


Issue 3: Unused Variable

File: components/simple-transcription/index.js:373
Category: Dead Code

Problem:
The layerData variable is assigned but never used.

Current Code:

TPEN.eventDispatcher.on('tpen-layer-changed', (event) => {
  // Prefer event.detail for consistency; guard against empty pages
  const layerData = event?.detail  // Never used
  if (this.#page?.items?.length > 0) {
    TPEN.activeLineIndex = 0
  }
  this.updateLines()
})

Suggested Fix:
Remove the unused variable or use it if intended.


Issue 4: Potential Null Return

File: interfaces/transcription/index.js:315-323
Category: Error Handling

Problem:
fetchCanvasesFromCurrentLayer() returns undefined when currentLayer is not found, but callers expect an array.

Current Code:

fetchCanvasesFromCurrentLayer() {
  const currentLayer = TPEN.activeProject?.layers.find(layer => layer.pages.some(page => page.id.split('/').pop() === TPEN.screen.pageInQuery))
  const canvases = currentLayer?.pages.flatMap(page => {
    return {
      id: page.target,
      label: page.label
    }
  })
  return canvases  // Could be undefined
}

Suggested Fix:

return canvases ?? []

How to Verify:
Call the function when no matching layer exists and check for errors.


Suggestions 🔵

Suggestion 1: Extract Shared Tool Message Handling

Consider extracting the #handleToolMessages method to a shared utility or base class to eliminate duplication:

// utilities/toolMessageHandler.js
export function createToolMessageHandler(getPage, setActiveIndex, updateLines) {
  return function(event) {
    const lineId = event.data?.lineId ?? event.data?.lineid ?? event.data?.annotation
    if (!lineId) return

    if (["CURRENT_LINE_INDEX", "RETURN_LINE_ID", "SELECT_ANNOTATION", "NAVIGATE_TO_LINE"].includes(event.data?.type)) {
      const lineIndex = getPage()?.items?.findIndex(item => {
        const itemId = item.id ?? item['@id']
        return itemId === lineId || itemId?.endsWith?.(`/${lineId}`) || itemId?.split?.('/').pop() === lineId
      })
      
      if (lineIndex !== undefined && lineIndex !== -1) {
        setActiveIndex(lineIndex)
        updateLines()
      }
    }
  }
}

Suggestion 2: Consider Using a Cleanup Registry Pattern

Both interfaces have multiple event listeners that need cleanup. Consider a pattern to track and clean up all listeners:

#cleanupRegistry = []

registerListener(target, event, handler, options) {
  target.addEventListener(event, handler, options)
  this.#cleanupRegistry.push(() => target.removeEventListener(event, handler, options))
}

registerDispatcherListener(event, handler) {
  TPEN.eventDispatcher.on(event, handler)
  this.#cleanupRegistry.push(() => TPEN.eventDispatcher.off(event, handler))
}

disconnectedCallback() {
  this.#cleanupRegistry.forEach(cleanup => cleanup())
  this.#cleanupRegistry = []
}

Files Reviewed

  • components/column-selector/index.js - Refactored to use events, cleanup looks good
  • components/layer-selector/index.js - Event name change verified as safe
  • components/magnifier-tool/index.js - Good use of data attribute selector
  • components/quicktype-tool/index.js - Good use of data attribute selector
  • components/read-only-transcribe/index.js - Good v2/v3 IIIF handling
  • components/simple-transcription/index.js - Memory leaks need addressing
  • components/splitscreen-tool/index.js - Logic change looks correct
  • components/workspace-tools/index.js - Good use of data attribute selector
  • interfaces/transcription/index.html - Component swap verified
  • interfaces/transcription/index.js - Memory leaks need addressing
  • interfaces/transcription/simple.html - Component swap verified

Checklist for Developer

  • Address all Major issues (memory leaks, security)
  • Review and address Minor issues as appropriate
  • Consider Suggestions for future improvements
  • Run full test suite after changes
  • Test layer switching in both interfaces
  • Test mounting/unmounting components to verify no memory leaks
  • Self-review the fixes before requesting re-review

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.

swap in transcription alt after confirming its functionality as we build out canvas-panel

3 participants