Skip to content

Add UI that enables user to gloss tokens#77

Open
alex-rawlings-yyc wants to merge 3 commits into
mainfrom
gloss-token
Open

Add UI that enables user to gloss tokens#77
alex-rawlings-yyc wants to merge 3 commits into
mainfrom
gloss-token

Conversation

@alex-rawlings-yyc
Copy link
Copy Markdown
Contributor

@alex-rawlings-yyc alex-rawlings-yyc commented May 19, 2026

  • Add gloss input to TokenChip (word tokens only): bordered chip now stacks surface text above an editable gloss field
  • Thread glosses (Record<string, string>) and onGlossChange(tokenId, value) props down through TokenChip → PhraseBox → SegmentView → ContinuousView
  • Introduce a phrase-window around the focused index in ContinuousView to cap rendered PhraseBoxes and reduce overhead on large books
  • Replace SegmentView's onClick with onSelect(ref, tokenId?) so the clicked token id propagates up to Interlinearizer
  • Add activePhraseIndex prop to ContinuousView for external phrase jumps when a segment-view token is clicked in continuous-scroll mode
  • Update tests to cover gloss propagation, activePhraseIndex jumps, and both phrase-window boundary branches

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Per-token gloss (translation) editing in continuous and segment views.
    • Direct phrase-index focus/jump support for continuous scrolling.
  • Improvements

    • Better focus preservation across mode switches and instant scroll behavior after jumps.
    • Windowed rendering for large books to improve performance.
    • More reliable verse-change notifications and cross-boundary navigation.
  • Tests

    • Expanded coverage for gloss editing, focus behavior, scrolling, and mode transitions.

Review Change Stack

- Add gloss input to TokenChip (word tokens only): bordered chip now stacks surface text above an editable gloss field
- Thread `glosses` (Record<string, string>) and `onGlossChange(tokenId, value)` props down through TokenChip → PhraseBox → SegmentView → ContinuousView
- Introduce a phrase-window around the focused index in ContinuousView to cap rendered PhraseBoxes and reduce overhead on large books
- Replace SegmentView's `onClick` with `onSelect(ref, tokenId?)` so the clicked token id propagates up to Interlinearizer
- Add `activePhraseIndex` prop to ContinuousView for external phrase jumps when a segment-view token is clicked in continuous-scroll mode
- Update tests to cover gloss propagation, `activePhraseIndex` jumps, and both phrase-window boundary branches
@alex-rawlings-yyc alex-rawlings-yyc self-assigned this May 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Warning

Rate limit exceeded

@alex-rawlings-yyc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 13 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 728e07c1-8be1-4ce1-a2d1-754cd0e7433c

📥 Commits

Reviewing files that changed from the base of the PR and between c390d64 and e35c0ff.

📒 Files selected for processing (8)
  • src/__tests__/components/PhraseBox.test.tsx
  • src/__tests__/components/SegmentView.test.tsx
  • src/__tests__/components/TokenChip.test.tsx
  • src/components/ContinuousView.tsx
  • src/components/InterlinearizerLoader.tsx
  • src/components/PhraseBox.tsx
  • src/components/SegmentView.tsx
  • src/components/TokenChip.tsx
📝 Walkthrough

Walkthrough

This PR expands token-level gloss editing and external phrase focus control: TokenChip/PhraseBox/SegmentView/ContinuousView APIs and implementations now carry gloss state and callbacks, continuous view window-renders around an externally driven focus index, and Interlinearizer coordinates shared glosses, focus preservation, and scrolling; tests were updated and extended accordingly.

Changes

Gloss editing and phrase focus

Layer / File(s) Summary
TokenChip gloss input support
src/components/TokenChip.tsx, src/__tests__/components/TokenChip.test.tsx
TokenChip now renders optional gloss <input> for word tokens, forwarding gloss changes and focus events via callbacks; tests verify gloss values, per-keystroke onChange behavior, and onFocus handling.
PhraseBox gloss wiring and container tabIndex
src/components/PhraseBox.tsx, src/__tests__/components/PhraseBox.test.tsx
PhraseBox accepts glosses and onGlossChange, forwards per-token gloss and focus down to TokenChip, sets tabIndex={-1} on interactive button wrapper, and updates focused-state styling; tests introduce requiredProps/nonInteractiveProps helpers and validate gloss pass-through, callback invocation, and rendering branches.
SegmentView token selection and gloss model
src/components/SegmentView.tsx, src/__tests__/components/SegmentView.test.tsx
SegmentView replaces segment-level onClick with onSelect(ref, tokenId?), adds required glosses/onGlossChange and optional focusedTokenId, and returns mode-specific roots (baseline-text as button, token-chip as div) while mapping tokens via PhraseBox/TokenChip; tests updated to use full prop helpers and verify selection and gloss editing flows.
ContinuousView phrase focus and windowing
src/components/ContinuousView.tsx, src/__tests__/components/ContinuousView.test.tsx
ContinuousView adds externally controlled activePhraseIndex, requires activeVerse/onVerseChange, accepts glosses/onGlossChange, exposes onFocusPhraseIndexChange, implements centered phrase-window rendering with PHRASE_WINDOW_HALF, coordinates external vs internal jumps using refs, and calls focus callbacks on focus changes; tests standardized via requiredProps(), include makeLargeBook fixture, and add activePhraseIndex direct-jump timing/regression coverage.
Interlinearizer state and coordination
src/components/Interlinearizer.tsx, src/__tests__/components/Interlinearizer.test.tsx
Interlinearizer manages shared glosses state and focusedTokenId, computes wordTokens/phraseIndexByTokenId for continuous strip indexing, preserves and applies focus on mode switches, provides scrollActiveSegmentIntoView, and wires ContinuousView/SegmentView with gloss and focus callbacks; tests updated to assert selection, focus carryover, and gloss propagation.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SegmentView
  participant ContinuousView
  participant TokenChip
  participant Interlinearizer
  User->>SegmentView: click token
  SegmentView->>Interlinearizer: onSelect(ref, tokenId)
  Interlinearizer->>Interlinearizer: compute activePhraseIndex
  Interlinearizer->>ContinuousView: activePhraseIndex
  ContinuousView->>ContinuousView: update focusPhraseIndex
  ContinuousView->>Interlinearizer: onFocusPhraseIndexChange
  Interlinearizer->>SegmentView: focusedTokenId
  User->>TokenChip: type gloss
  TokenChip->>Interlinearizer: onGlossChange(tokenId, value)
  Interlinearizer->>ContinuousView: glosses map
  Interlinearizer->>SegmentView: glosses map
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sillsdev/interlinearizer-extension#40: Main PR extends the continuous-scroll implementation introduced in PR #40 by heavily updating src/components/ContinuousView.tsx's focus/navigation model and wiring gloss-editing callbacks through ContinuousView/PhraseBox/SegmentView.

Suggested labels

up next

Suggested reviewers

  • imnasnainaec
  • jasonleenaylor

Poem

🐰 I nibble letters, glosses tucked in rows,
Phrases hop to focus where the daylight glows,
From strip to segment, every token shows,
Tiny inputs bloom where little knowledge grows,
Hop — edit, focus, scroll — the interlinear flows.

🚥 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 title 'Add UI that enables user to gloss tokens' directly and clearly summarizes the main change: adding a glossing user interface for tokens throughout the component hierarchy.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gloss-token

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.

@alex-rawlings-yyc alex-rawlings-yyc linked an issue May 19, 2026 that may be closed by this pull request
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: 6

Caution

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

⚠️ Outside diff range comments (1)
src/components/PhraseBox.tsx (1)

54-63: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Avoid nesting gloss <input> controls inside a <button> phrase wrapper.

PhraseBox now wraps TokenChip (which renders <input>) inside a button. Interactive-in-interactive nesting is invalid and can break focus/click semantics for gloss editing.

Suggested direction
-  if (onClick) {
-    return (
-      <button
+  if (onClick) {
+    return (
+      <div
         className={`${baseClass} tw:cursor-pointer tw:text-left tw:hover:bg-muted/30`}
         data-focus-state={isFocused ? 'focused' : 'default'}
         data-phrase-box="true"
         onClick={() => onClick?.(index)}
-        tabIndex={-1}
-        type="button"
+        role="button"
+        tabIndex={0}
       >
         {innerContent}
-      </button>
+      </div>
     );
   }

(Then add keyboard handlers for Enter/Space to preserve keyboard activation behavior.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PhraseBox.tsx` around lines 54 - 63, PhraseBox currently
renders a native <button> around innerContent (which can include TokenChip that
renders an <input>), creating invalid interactive-in-interactive nesting;
replace the <button> wrapper with a non-interactive container (e.g., a <div> or
<span>) that carries the existing className/baseClass, data-* attributes and
onClick handler (onClick?.(index)), and add explicit keyboard handlers on that
wrapper (onKeyDown) to trigger the same activation when Enter or Space are
pressed so keyboard activation is preserved; ensure tabIndex and
data-focus-state are preserved and do not interfere with the TokenChip input
focus, and keep TokenChip unchanged so its native input remains accessible.
🧹 Nitpick comments (1)
src/components/SegmentView.tsx (1)

54-54: ⚡ Quick win

Add JSDoc for handleTokenClick to match repo function-doc requirements.

This helper is undocumented, but repo rules require JSDoc on every function/method in src/**/*.{ts,tsx}.

As per coding guidelines: “Every function and method — exported or internal — must have a JSDoc block with summary sentence, @param for every parameter, @returns describing return value (omit for void/Promise), and @throws for every error condition”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/SegmentView.tsx` at line 54, Add a JSDoc block above the
handleTokenClick declaration describing its purpose and parameters: document the
summary sentence (what handleTokenClick does), include an `@param` for tokenId
(string) and an `@param` for any closed-over variables if desired (mention ref is
used), omit `@returns` because it returns void, and add an `@throws` noting that it
may propagate errors thrown by onSelect; reference the handleTokenClick
function, the onSelect callback, the ref variable, and the tokenId parameter so
the maintainer can locate and verify the documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/components/ContinuousView.test.tsx`:
- Around line 297-298: Remove the unnecessary type assertions "as const" on the
test object fields (writingSystem and type) in the ContinuousView tests; instead
assign the string literals directly (e.g., writingSystem: 'en', type: 'word') —
the Token/Book types already enforce correct typings, so update the test data in
the ContinuousView.test.tsx test case where those fields are set to use plain
literals and remove the "as const" casts.

In `@src/__tests__/components/Interlinearizer.test.tsx`:
- Around line 256-267: The test invokes capturedSegmentViewPropsList[1].onSelect
which triggers handleSegmentSelect and updates state via setFocusedTokenId; wrap
that invocation in React's act() to ensure the state update is flushed before
assertions. Update the test in Interlinearizer.test.tsx so the call to
capturedSegmentViewPropsList[1].onSelect?.({ book: 'GEN', chapter: 1, verse: 2
}, 'GEN 1:2:0') is executed inside act(() => { ... }); then keep the
expect(mockSetScrRef) assertion after the act block to avoid React state-update
warnings.

In `@src/__tests__/components/PhraseBox.test.tsx`:
- Around line 194-199: The test's second expectation for onGlossChange is
asserting the last typed character instead of the cumulative gloss value; update
the assertion for handleGlossChange (used in this test) so the second call
expects ('token-1', 'hi') rather than ('token-1', 'i') — locate the
userEvent.type invocation and the subsequent
expect(handleGlossChange).toHaveBeenNthCalledWith assertions and change the
second expected gloss value to the full accumulated string.

In `@src/__tests__/components/SegmentView.test.tsx`:
- Around line 221-226: The test's second gloss callback assertion is wrong: when
typing via userEvent.type the handler should receive the cumulative text, so
update the expectation for handleGlossChange to assert the second call was with
'In' (e.g., expect(handleGlossChange).toHaveBeenNthCalledWith(2, 'tok-0', 'In'))
instead of 'n'; locate the assertions referencing handleGlossChange in the
SegmentView.test.tsx block around the userEvent.type call and change the second
expected argument accordingly.

In `@src/components/ContinuousView.tsx`:
- Around line 155-156: The code stores an external index (activePhraseIndex)
into focusPhraseIndex and later uses it to compute windowStartTokenIndex from
phraseEntries[windowStart], but it never clamps or validates the external index;
update the assignment sites (where activePhraseIndex is applied to
focusPhraseIndex and any similar assignments in the regions around lines 215–251
and 319–325) to validate and clamp the incoming index to the valid range [0,
phraseEntries.length - 1] before storing, and handle empty phraseEntries by
setting a safe default (e.g., undefined or 0) to avoid dereferencing
phraseEntries[windowStart]; ensure the clamp logic references activePhraseIndex,
focusPhraseIndex, phraseEntries, and windowStart so out-of-range or stale
indices cannot cause phraseEntries[windowStart] === undefined at runtime.

In `@src/components/TokenChip.tsx`:
- Around line 33-38: In TokenChip, the input uses defaultValue which makes it
uncontrolled and prevents future gloss prop updates from showing; change it to a
controlled input by replacing defaultValue={gloss ?? ''} with value={gloss ??
''} and keep onChange={(e) => onGlossChange?.(e.target.value)} so updates flow
from the gloss prop into the textbox; if TokenChip intentionally needs local
editing state, introduce a controlled local state initialized from gloss and
sync props via useEffect, but otherwise remove any local uncontrolled logic so
gloss prop and onGlossChange stay authoritative.

---

Outside diff comments:
In `@src/components/PhraseBox.tsx`:
- Around line 54-63: PhraseBox currently renders a native <button> around
innerContent (which can include TokenChip that renders an <input>), creating
invalid interactive-in-interactive nesting; replace the <button> wrapper with a
non-interactive container (e.g., a <div> or <span>) that carries the existing
className/baseClass, data-* attributes and onClick handler (onClick?.(index)),
and add explicit keyboard handlers on that wrapper (onKeyDown) to trigger the
same activation when Enter or Space are pressed so keyboard activation is
preserved; ensure tabIndex and data-focus-state are preserved and do not
interfere with the TokenChip input focus, and keep TokenChip unchanged so its
native input remains accessible.

---

Nitpick comments:
In `@src/components/SegmentView.tsx`:
- Line 54: Add a JSDoc block above the handleTokenClick declaration describing
its purpose and parameters: document the summary sentence (what handleTokenClick
does), include an `@param` for tokenId (string) and an `@param` for any closed-over
variables if desired (mention ref is used), omit `@returns` because it returns
void, and add an `@throws` noting that it may propagate errors thrown by onSelect;
reference the handleTokenClick function, the onSelect callback, the ref
variable, and the tokenId parameter so the maintainer can locate and verify the
documentation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8fba8299-8ba6-4983-ad70-29001326b68a

📥 Commits

Reviewing files that changed from the base of the PR and between 75c422f and 53159d0.

📒 Files selected for processing (10)
  • src/__tests__/components/ContinuousView.test.tsx
  • src/__tests__/components/Interlinearizer.test.tsx
  • src/__tests__/components/PhraseBox.test.tsx
  • src/__tests__/components/SegmentView.test.tsx
  • src/__tests__/components/TokenChip.test.tsx
  • src/components/ContinuousView.tsx
  • src/components/Interlinearizer.tsx
  • src/components/PhraseBox.tsx
  • src/components/SegmentView.tsx
  • src/components/TokenChip.tsx

Comment thread src/__tests__/components/ContinuousView.test.tsx
Comment thread src/__tests__/components/Interlinearizer.test.tsx
Comment thread src/__tests__/components/PhraseBox.test.tsx
Comment thread src/__tests__/components/SegmentView.test.tsx
Comment thread src/components/ContinuousView.tsx
Comment thread src/components/TokenChip.tsx
Copy link
Copy Markdown
Contributor Author

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Choose a reason for hiding this comment

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

@alex-rawlings-yyc resolved 5 discussions.
Reviewable status: 0 of 10 files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

@alex-rawlings-yyc
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/SegmentView.tsx`:
- Around line 95-104: SegmentView is passing unstable per-render values to
MemoizedPhraseBox (tokens={[token]} and onClick={() =>
handleTokenClick(token.id)}), which defeats memoization; stabilize these by (a)
wrapping handleTokenClick in useCallback (or reusing the ContinuousView pattern)
so the onClick prop is stable, and (b) stop creating a new array per
token—either pass the single token (change Prop name from tokens to token on
MemoizedPhraseBox/PhraseBox) or precompute a stable tokens array/share a
reference—so MemoizedPhraseBox receives stable props and can properly memoize.

In `@src/components/TokenChip.tsx`:
- Around line 33-40: The input for the gloss in TokenChip is rendered as a
controlled field even when the optional onGlossChange handler is missing,
causing confusing UX and React warnings; update the input element (the <input>
used in TokenChip that binds value={gloss} and onChange={(e) =>
onGlossChange?.(e.target.value)}) to explicitly set readOnly to true when
onGlossChange is not provided (readOnly={!onGlossChange}) so the field appears
and behaves as non-editable unless an edit handler is passed.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f2c537f-015f-4f39-ad08-f0d52b44820a

📥 Commits

Reviewing files that changed from the base of the PR and between 53159d0 and c390d64.

📒 Files selected for processing (4)
  • src/__tests__/components/Interlinearizer.test.tsx
  • src/__tests__/components/TokenChip.test.tsx
  • src/components/SegmentView.tsx
  • src/components/TokenChip.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tests/components/TokenChip.test.tsx
  • src/tests/components/Interlinearizer.test.tsx

Comment thread src/components/SegmentView.tsx Outdated
Comment thread src/components/TokenChip.tsx
…nChip` type guard to ensure that `input` always has a handler; update docs
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.

Gloss a word

1 participant