Skip to content

feat(studio): anonymizer builder live preview panel [ASTD-332] - #1005

Merged
marcusds merged 18 commits into
mainfrom
astd-332-anonymizer-live-preview-panel/mschwab
Aug 5, 2026
Merged

feat(studio): anonymizer builder live preview panel [ASTD-332]#1005
marcusds merged 18 commits into
mainfrom
astd-332-anonymizer-live-preview-panel/mschwab

Conversation

@marcusds

@marcusds marcusds commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Builds the right-hand Preview panel of the anonymizer builder (/anonymizer/new), replacing the "Your records preview will appear here" placeholder.

Closes ASTD-332.

What it does

Preview (beside the Source | Model Settings tabs) streams the current form config through the anonymizer /preview endpoint and renders:

  • Original vs Replaced columns with inline PII tags on detected/replaced entities
  • Replacement Map table (Label | Original | → | Replacement)
  • Record pager (‹ Record 1 of N ›), loading skeletons, empty state, error banner
  • Collapsible Logs panel fed by the stream's log frames

While a run is in flight the button becomes Stop. Full Run moves into the panel header per the Figma and still submits the create-job form — the panel is inside the same <form>. The left panel footer is dropped: Figma marks that frame hidden and moves its only visible button into the preview header, which left Cancel stranded. Exit is via the breadcrumb.

Frame types now come from the SDK

Originally this branch hand-wrote the PreviewFrame union and its guards, because the spec described the streaming 200 as an empty schema under the wrong media type. #1024 (ASTD-350) fixed that at the source, and this now imports the generated LogFrame, PreviewDatasetFrame, TraceDatasetFrame, FailedRecordsFrame, Heartbeat, Done and Error.

The runtime guards stay deliberately. Orval emits zod.unknown() for the response because the media type isn't JSON, and frames are untrusted network input regardless — the SDK removed the duplicated declarations, not the need to validate. The streaming reader stays hand-written too; orval doesn't generate incremental readers.

Notes for review

Replaced-side spans are searched, not replayed. Each entity is located in the replaced text by scanning forward for its synthetic value. Replaying the original offsets drifts the moment a replacement changes length — a bug the upstream library already hit and worked around, so parse.ts mirrors its display.py logic: the exact → value-only → case-insensitive lookup ladder, plus the build-from-replacement-map fallbacks for when detection returns no spans but the map is populated.

PII tags use Badge, not Tag. Code Connect on the Figma PII Tag component maps to Badge (solid for the value, outline for the label); Tag is the interactive pill and rendered noticeably heavier than the mock.

Stop is real, not cosmetic — but not instant. Aborting makes Starlette close the response generator, cancelling the drain task and unwinding the function's task group until the worker's next frame send raises PreviewMessageDeliveryError. Because the anonymizer runs under abandon_on_cancel=True, the worker thread is abandoned rather than joined, so an in-flight model call still finishes.

Validation routing was reading empty errors. Clicking Preview on an incomplete form jumped to Model Settings even when the failing field was on Source. formState is a proxy that only maintains what the render body subscribes to, and this component reads none of it — so errors came back {}, and [].every() is vacuously true. Errors now come from handleSubmit's invalid callback, which is what the Full Run path already used.

The shared-URL-param hazard. useStudioDataViewState syncs to unprefixed params (page, page_size, sort, …). That bit inside this PR: with a map that overflows a page, going to ?page=2 then advancing the record pager to a shorter map sliced past the end and rendered an empty table — with the map's pager hidden below page size, nothing was left to recover with. Clamped, with a regression test confirmed to fail without the fix. The broader collision remains for when ASTD-330 reuses this table alongside ResultsPreviewTable; that wants a paramPrefix option on the shared hook.

Reuse audit. Four things this branch had reimplemented now use what Studio already ships: isAbortError (common's version also handles APIUserAbortError, mine only caught DOMException), the NDJSON read loop (extracted as readLineDelimitedStream, shared with the data designer preview and given the chunk-boundary test neither call site had), OUTPUT_SUFFIXES (was declared here and in AnonymizerJobDetailRoute), and StackedSkeleton.

The record view lives in components/AnonymizerRecordView/ so the job detail page (ASTD-330) and evaluation results (ASTD-322) can reuse it.

Screenshots

01-builder-empty 02-file-picker 02b-after-file 03-builder-configured 04-preview-running 05-preview-records 07-walkthrough-final

Testing

  • 73 unit tests across parse, previewApi, lineStream, the record view and the tab/heading helpers
  • Verified in a real browser against a mocked NDJSON stream: empty, loading, populated, stop-mid-run, validation routing, and the rewrite-mode skeleton heading
  • Full studio suite green (310 files, 2772 tests), typecheck and lint clean

Rebased onto main after #1024 merged.

Summary by CodeRabbit

  • New Features
    • Added streaming anonymizer preview panel with paging (up to 10 rows), logs, stop/full-run controls, and clear empty/error states.
    • Enhanced record viewing with entity highlighting, configurable output headings, and replacement maps (including skeleton loading).
    • Added record navigation (previous/next) and improved builder tab behavior for validation.
  • Bug Fixes
    • Improved resilience for malformed/empty preview and replacement data, plus safer preview pagination and record selection resets.
  • Tests
    • Expanded parsing, preview streaming/frame handling, and UI state coverage for edge cases.

@github-actions github-actions Bot added the feat label Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 30781/39277 78.4% 62.8%
Integration Tests 18077/37229 48.6% 21.1%

@marcusds
marcusds force-pushed the astd-332-anonymizer-live-preview-panel/mschwab branch from 4f9806f to ceed457 Compare August 4, 2026 22:02
@marcusds
marcusds marked this pull request as ready for review August 4, 2026 22:23
@marcusds
marcusds requested review from a team as code owners August 4, 2026 22:23
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR adds streamed anonymizer previews, anonymizer record parsing, highlighted record rendering, replacement-map pagination, preview validation, cancellation, logs, and builder controls.

Anonymizer preview flow

Layer / File(s) Summary
Preview stream transport
web/packages/studio/src/util/*, web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.*, web/packages/studio/src/components/NewDataDesignerJobForm/previewApi.ts
Parses streamed NDJSON frames and handles line buffering, validation, authentication, and HTTP errors.
Anonymizer record construction
web/packages/studio/src/components/AnonymizerRecordView/{types,parse}.*
Parses trace cells, converts offsets, resolves replacements, builds text segments, and constructs anonymizer records.
Preview request and state management
web/packages/studio/src/routes/AnonymizerBuilderRoute/{constants,schema,utils,useAnonymizerPreview}.*
Builds validated requests, limits preview rows, selects validation tabs, derives output headings, and manages streamed state and cancellation.
Record display components
web/packages/studio/src/components/AnonymizerRecordView/*
Renders highlighted text, replacement maps, record sections, loading skeletons, empty states, and paginated replacement data.
Builder preview interface
web/packages/studio/src/routes/AnonymizerBuilderRoute/components/*
Connects preview execution to the form and renders records, paging, logs, controls, loading states, and failures.
Shared output-column definitions
web/packages/studio/src/routes/AnonymizerJobDetailRoute/util.ts
Uses shared output suffix definitions for result-column ordering.

Sequence Diagram(s)

sequenceDiagram
  participant AnonymizerBuilderForm
  participant useAnonymizerPreview
  participant streamAnonymizerPreview
  participant PreviewPanel
  participant AnonymizerRecordView

  AnonymizerBuilderForm->>useAnonymizerPreview: runPreview()
  useAnonymizerPreview->>streamAnonymizerPreview: submit PreviewRequest
  streamAnonymizerPreview-->>useAnonymizerPreview: streamed preview frames
  useAnonymizerPreview-->>PreviewPanel: preview records, logs, and status
  PreviewPanel->>AnonymizerRecordView: build and render selected record
Loading

Possibly related PRs

Suggested reviewers: steramae-nvidia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding a live anonymizer builder preview panel in Studio.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch astd-332-anonymizer-live-preview-panel/mschwab

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx (1)

96-111: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Resolve the promise if handleSubmit rejects.

form.handleSubmit returns a promise. If the resolver throws, neither callback runs and this promise never settles. runPreview then awaits forever with no user feedback and no error state.

♻️ Proposed fix
       new Promise<PreviewRequest | undefined>((resolve) => {
-        void form.handleSubmit(
+        form.handleSubmit(
           (values) => {
             setSubmitError(undefined);
             resolve(buildAnonymizerPreviewRequest(values, defaultEntityLabels?.data ?? []));
           },
           (errors) => {
             showValidationErrors(errors);
             resolve(undefined);
           }
-        )();
+        )().catch(() => {
+          setSubmitError('Could not validate the form.');
+          resolve(undefined);
+        });
       }),
🤖 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
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx`
around lines 96 - 111, Update getPreviewRequest to handle rejection from the
promise returned by form.handleSubmit, ensuring the surrounding Promise always
settles when the resolver or submission flow throws. Reject or resolve with
undefined consistently with the existing error path, and surface the failure
through the existing validation/error handling used by runPreview.
🤖 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 `@web/packages/studio/src/components/AnonymizerRecordView/parse.ts`:
- Around line 136-153: Update the matching logic in the parse flow around
resolveSynthetic and indexOf so repeated replacement text is aligned with its
source entity rather than selecting the first identical occurrence. Use
surrounding source context or edit alignment to map “Teddy met Bobby” to the
replacement occurrence corresponding to Bobby, while preserving unchanged-text
handling; add a regression test covering this scenario.
- Around line 36-43: Update toEntity to validate that start and end are
integers, not merely numbers, before returning an AnonymizerEntity; reject
fractional offsets so slicing and cursor tracking remain consistent. Add a test
covering fractional start or end positions and verify the entity is rejected.

In
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.ts`:
- Around line 66-79: The runPreview flow should reserve and store its
AbortController before awaiting getRequest, and move request construction inside
the existing try/error path so rejected builds update error state. In
runPreview, ignore streamed frames and guard the setLogs updater unless that
controller remains abortRef.current, preventing stale runs from overwriting
newer previews. Add a test covering deferred request construction resolving out
of order.

---

Nitpick comments:
In
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx`:
- Around line 96-111: Update getPreviewRequest to handle rejection from the
promise returned by form.handleSubmit, ensuring the surrounding Promise always
settles when the resolver or submission flow throws. Reject or resolve with
undefined consistently with the existing error path, and surface the failure
through the existing validation/error handling used by runPreview.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8c166a57-0e9c-4807-a2f1-1f728cdd4ce3

📥 Commits

Reviewing files that changed from the base of the PR and between e7dbf70 and ceed457.

📒 Files selected for processing (24)
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordSkeleton.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.test.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/HighlightedText.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/RecordSection.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/ReplacementMapTable.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/parse.test.ts
  • web/packages/studio/src/components/AnonymizerRecordView/parse.ts
  • web/packages/studio/src/components/AnonymizerRecordView/types.ts
  • web/packages/studio/src/components/NewDataDesignerJobForm/previewApi.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/PreviewPanel.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/RecordPager.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/utils.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/utils.ts
  • web/packages/studio/src/routes/AnonymizerJobDetailRoute/util.ts
  • web/packages/studio/src/util/guards.ts
  • web/packages/studio/src/util/lineStream.test.ts
  • web/packages/studio/src/util/lineStream.ts

Comment thread web/packages/studio/src/components/AnonymizerRecordView/parse.ts
Comment thread web/packages/studio/src/components/AnonymizerRecordView/parse.ts
Comment thread web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.ts Outdated
marcusds added 12 commits August 4, 2026 22:31
Builds the right-hand Preview panel of the anonymizer builder, replacing the
"Your records preview will appear here" placeholder.

A "Preview" button beside the Source / Model Settings tabs streams the current
form config through the anonymizer `/preview` endpoint and renders the result:
Original vs Replaced text with inline PII tags, a Replacement Map table, a
record pager, loading skeletons, and a collapsible Logs panel. "Full Run" moves
into the panel header and still submits the create-job form.

The endpoint returns `application/x-ndjson`, not SSE, so this follows the data
designer's `streamPreview` shape rather than the SSE reader in `util/sseStream`
(which is GET-only). Frames are shape-guarded and unknown `kind` values are
dropped rather than surfaced.

Replaced-side entity spans are located by searching the replaced text for each
synthetic value rather than replaying the original offsets, which drift as soon
as a replacement changes length. This mirrors the upstream library's own
display path, including its case-insensitive lookup fallbacks and the
build-from-replacement-map path used when detection returns no spans.

The record view lives under components/AnonymizerRecordView so the job detail
page (ASTD-330) and evaluation results (ASTD-322) can reuse it.

Signed-off-by: mschwab <mschwab@nvidia.com>
…er [ASTD-332]

`useStudioDataViewState` reads `page` from a shared, unprefixed URL param, so
the replacement map's page index survives moving the record pager. Paging to a
record with a shorter map then sliced past the end and rendered an empty table
— with the pager hidden below page size, nothing was left to recover with.
Clamp the requested page to the row count.

Also drops the left panel footer. The Figma marks that frame hidden and moves
its only visible button, Full Run, into the preview header, which left Cancel
stranded there on its own. Exit is via the breadcrumb.

Signed-off-by: mschwab <mschwab@nvidia.com>
Clicking Preview on an incomplete form jumped to Model Settings even when the
failing field was on Source, hiding the error that explained the refusal.

Two causes. `getPreviewRequest` read `form.formState.errors` after `trigger()`,
but RHF's `formState` is a proxy that only maintains the fields the render body
subscribes to, and this component reads none of it — the lookup returned `{}`.
`[].every()` is then vacuously true, so an empty error set satisfied the
"models are the only failure" test and routed to the wrong tab.

Take the errors from `handleSubmit`'s invalid callback instead, which is what
the Full Run path already does, and require a non-empty field list before
treating models as the sole failure. The tab choice moves into
`tabForValidationErrors` so the empty case is pinned by a test.

Signed-off-by: mschwab <mschwab@nvidia.com>
The Preview button was disabled for the duration of a run, so a preview that
turned out to be misconfigured had to be waited out or escaped by navigating
away. Swap it for Stop, which aborts the request.

The abort is honest rather than cosmetic: closing the connection makes
Starlette close the response generator, which cancels the drain task and
unwinds the function's task group until the worker's next frame send raises
`PreviewMessageDeliveryError`. It is not instant — the anonymizer runs under
`abandon_on_cancel=True`, so the worker thread is abandoned rather than joined
and an in-flight model call still finishes.

A stopped run reports itself as stopped instead of claiming the run returned
no records. Partial records already streamed are kept.

This departs from the Figma, which greys the button out for the duration.

Signed-off-by: mschwab <mschwab@nvidia.com>
…D-332]

The entity chips used `Tag`, which is the interactive pill — button-based,
large radius, generous padding — so inline text read as chunky and did not
match the mock. Code Connect on the Figma `PII Tag` component maps both chips
to `Badge` (`kind="solid"` for the value, `kind="outline"` for the label),
which is the non-interactive `<span>` label at Label/Bold/sm. The design's
wrapper carries no gap, so the pair now sits flush.

Same swap for the Replacement Map's Label column, which Code Connect also maps
to an outline `Badge`.

`entityTagColor` is unchanged — Tag and Badge expose the same seven colours, so
the existing category mapping carries over. `EntitiesSection` keeps `Tag`,
which is correct there: those chips are removable and interactive.

Signed-off-by: mschwab <mschwab@nvidia.com>
…ASTD-332]

Log frames arrive one per stream read, too far apart for React to batch, so
each one synchronously re-rendered the builder form and the preview panel.
That put the new Stop button behind a queue of log renders exactly when a
chatty run made it most useful. They now update in a transition.

`AnonymizerRecordView` and `HighlightedText` are memoized. Their props are
already stable references — the record is built inside a memo — so this
actually prevents work rather than adding a comparison that never hits.

Also hoists the static skeleton rows out of the render path and folds the
output heading into the record memo, since both derive from the same inputs.

Trims the branch's comments back to the ones carrying a non-obvious why.

Signed-off-by: mschwab <mschwab@nvidia.com>
`PreviewPanel` had grown a skeleton, a record pager and the panel chrome in one
file. Splits out `RecordPager`, and moves the skeleton next to the view it
mirrors as `AnonymizerRecordSkeleton` — the two render the same three sections
and had drifted apart in the same file already.

`RecordSection` replaces six hand-written copies of the heading-plus-content
block across the view and the skeleton, so they can no longer diverge.

The `asObject` guard was written twice on this branch, in `parse.ts` and
`previewApi.ts`. Promotes it to `@studio/util/guards` as `asRecord`, matching
the name the two pre-existing hand-rolled copies elsewhere in Studio already
use; those call sites are left alone as out of scope here.

Signed-off-by: mschwab <mschwab@nvidia.com>
…STD-332]

The output heading is derived from whichever output column a record carries,
which does not exist until results arrive — so a rewrite preview showed
"Replaced" over the skeleton for the whole run, then flipped to "Rewritten".

The form knows the strategy before the run starts, so it now passes the
heading down for the loading state. A loaded record still derives its own,
which stays correct if the strategy is changed after a run.

Signed-off-by: mschwab <mschwab@nvidia.com>
…es [ASTD-332]

An audit of this branch against what Studio already ships turned up four
things it had reimplemented.

`isAbortError` already exists in `common/AssistantChat/completionUtils`, and
its version is the better one — it matches any `Error` whose name is
`AbortError` or `APIUserAbortError`, where the copy here only recognised a
`DOMException`. Drops the copy and its tests.

The NDJSON read loop was duplicated between this preview and the data designer
preview, down to the trailing-partial handling. Extracts
`readLineDelimitedStream` and points both at it, with a test covering the
chunk-boundary cases neither call site exercised.

`OUTPUT_SUFFIXES` was declared here and again in `AnonymizerJobDetailRoute`.
The record view is the reusable home the ticket asked for, so the job detail
route now imports it, and `PreviewPanel` drops its third copy of the literal.

`StackedSkeleton` already renders N stacked skeleton lines, so the local
`SkeletonBlock` uses it rather than mapping its own.

Signed-off-by: mschwab <mschwab@nvidia.com>
ASTD-350 landed, so the anonymizer spec now describes the NDJSON frames and the
SDK generates them. Drops the hand-written `PreviewFrame` union and
`PreviewLogLevel` in favour of the generated `LogFrame`, `PreviewDatasetFrame`,
`TraceDatasetFrame`, `FailedRecordsFrame`, `Heartbeat`, `Done` and `Error`.

The runtime guards stay. Orval emits `zod.unknown()` for the response because
the media type isn't JSON, and these frames are untrusted network input either
way — what the SDK removes is the duplicated type declarations, not the need to
validate them.

`trace_dataset`'s column keeps its wire name, `original_text_column`, rather
than the camelCase alias this file used to apply at the parse boundary.

Signed-off-by: mschwab <mschwab@nvidia.com>
…STD-332]

buildReplacedEntities located each span with a forward indexOf on the
synthetic value, which is not bound to the source entity. When the
unchanged text already contained that value, the preview tagged the wrong
occurrence — "Bobby" -> "Teddy" in "Teddy met Bobby" mapped to offset 0.
Text between entities is untouched, so the gap since the last match
predicts the next position; fall back to the forward search only when
that alignment misses.

useAnonymizerPreview built its request before taking controller
ownership, so two runs could resolve out of order and let an older run
overwrite a newer preview, and a rejected build never reached the error
state. Reserve the controller first, construct inside the try, and drop
frames from superseded runs.

getPreviewRequest also left its promise unsettled if handleSubmit
rejected, hanging runPreview with no feedback.

Signed-off-by: mschwab <mschwab@nvidia.com>
main dropped the react-router-dom re-export.

Signed-off-by: mschwab <mschwab@nvidia.com>
@marcusds
marcusds force-pushed the astd-332-anonymizer-live-preview-panel/mschwab branch from ceed457 to fe83b2e Compare August 5, 2026 05:50
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.test.ts (1)

4-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Place external imports before internal aliases.

Move @testing-library/react before the @studio imports.
As per coding guidelines, “Group imports: external libraries, internal modules, relative imports in TypeScript.”

Proposed fix
 import type { PreviewRequest } from '`@nemo/sdk/generated/anonymizer/schema`';
+import { act, renderHook, waitFor } from '`@testing-library/react`';
+
 import { streamAnonymizerPreview } from '`@studio/routes/AnonymizerBuilderRoute/previewApi`';
 import { useAnonymizerPreview } from '`@studio/routes/AnonymizerBuilderRoute/useAnonymizerPreview`';
-import { act, renderHook, waitFor } from '`@testing-library/react`';
🤖 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
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.test.ts`
around lines 4 - 7, Reorder imports in the useAnonymizerPreview test so the
external `@testing-library/react` import appears before the internal `@nemo` and
`@studio` alias imports, preserving the existing imported symbols.

Source: Coding guidelines

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

Nitpick comments:
In
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.test.ts`:
- Around line 4-7: Reorder imports in the useAnonymizerPreview test so the
external `@testing-library/react` import appears before the internal `@nemo` and
`@studio` alias imports, preserving the existing imported symbols.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0486fd25-8d69-4ecf-bc4e-cbbe12b02211

📥 Commits

Reviewing files that changed from the base of the PR and between a6904bd and fe83b2e.

📒 Files selected for processing (25)
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordSkeleton.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.test.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/HighlightedText.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/RecordSection.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/ReplacementMapTable.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/parse.test.ts
  • web/packages/studio/src/components/AnonymizerRecordView/parse.ts
  • web/packages/studio/src/components/AnonymizerRecordView/types.ts
  • web/packages/studio/src/components/NewDataDesignerJobForm/previewApi.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/PreviewPanel.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/RecordPager.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/utils.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/utils.ts
  • web/packages/studio/src/routes/AnonymizerJobDetailRoute/util.ts
  • web/packages/studio/src/util/guards.ts
  • web/packages/studio/src/util/lineStream.test.ts
  • web/packages/studio/src/util/lineStream.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • web/packages/studio/src/components/AnonymizerRecordView/types.ts
  • web/packages/studio/src/util/lineStream.ts
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.tsx
  • web/packages/studio/src/util/guards.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.test.ts
  • web/packages/studio/src/components/AnonymizerRecordView/HighlightedText.tsx
  • web/packages/studio/src/util/lineStream.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordSkeleton.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/RecordPager.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/utils.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/utils.test.ts
  • web/packages/studio/src/components/NewDataDesignerJobForm/previewApi.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/PreviewPanel.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/parse.test.ts
  • web/packages/studio/src/routes/AnonymizerJobDetailRoute/util.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.ts
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.test.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/previewApi.ts
  • web/packages/studio/src/components/AnonymizerRecordView/ReplacementMapTable.tsx

Detection reports code-point offsets the way Python counts them, but JS
string indices are UTF-16 code units, so any non-BMP character ahead of a
span shifted the highlight — "😀 Alice" at 2..7 rendered " Alic".
Remap detected spans once per record; texts without a surrogate pair
short-circuit to the input array.

The preview endpoint rejects num_records above preview_num_records.max
with a 422 instead of clamping, and the bound is imperative so it never
reached the OpenAPI spec or the generated schema. Mirror the default
client-side so the form catches it before the round trip.

ReplacementMapTable clamped only the row slice, leaving the shared page
index untouched, so moving from page 3 to a smaller map rendered the last
rows under a "page 3 of 2" control.

Signed-off-by: mschwab <mschwab@nvidia.com>
Signed-off-by: mschwab <mschwab@nvidia.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 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 `@web/packages/studio/src/components/AnonymizerRecordView/parse.ts`:
- Around line 82-86: Validate entity start and end offsets against the original
text/code-point mapping before converting them in unitAt, dropping any entity
with an out-of-range offset instead of mapping it to text.length. Update the
fallback at the detected-entity handling near line 248 to use the converted,
filtered entity list rather than detected.length. Add a regression test covering
😀 with an out-of-range end offset.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fa62aca7-217c-41a9-84dd-158fcdbfe838

📥 Commits

Reviewing files that changed from the base of the PR and between fe83b2e and f92b34c.

📒 Files selected for processing (8)
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.test.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/ReplacementMapTable.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/parse.test.ts
  • web/packages/studio/src/components/AnonymizerRecordView/parse.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/GenerationSection.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/components/AnonymizerRecordView/ReplacementMapTable.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/AnonymizerRecordView.test.tsx
  • web/packages/studio/src/components/AnonymizerRecordView/parse.test.ts

Comment thread web/packages/studio/src/components/AnonymizerRecordView/parse.ts Outdated
marcusds and others added 2 commits August 5, 2026 08:07
toUtf16Offsets mapped an offset past the last code point onto
text.length, which is exactly the value toSegments' bounds check lets
through — so a span the old code rejected became a highlight running to
the end of the text. Drop those entities instead, and decide the
replacement-map fallback on the converted list, since conversion can now
empty it.

Signed-off-by: mschwab <mschwab@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- import/order configuration ---'
rg -n -A20 -B8 "'import/order'|\"import/order\"" web/eslint.config.js

printf '%s\n' '--- test imports ---'
sed -n '1,70p' web/packages/studio/src/routes/AnonymizerBuilderRoute/useAnonymizerPreview.test.ts

Length of output: 3928


@marcusds Confirmed. web/eslint.config.js requires this order. The current imports sort correctly within the shared external group. I withdraw the import-order finding.

You are interacting with an AI system.

marcusds added a commit that referenced this pull request Aug 5, 2026
The guideline said "group imports: external libraries, internal modules,
relative imports", which is not what `import/order` does. It collapses
builtin and external into one group and alphabetizes within it, and the
`@nemo` / `@nvidia` / `@studio` aliases resolve as external — so they sort
alongside `react` instead of forming an internal group.

Reviewers read the guideline, see an alias ahead of a package, and file it
as a defect. CodeRabbit did exactly that on #1005, citing "Coding
guidelines" as its source. Its suggested reordering fails `--max-warnings 0`.

State the rule's real behaviour, and tell CodeRabbit that import order is
lint-owned and not reviewable.

Signed-off-by: mschwab <mschwab@nvidia.com>
Every streamed log frame appends to logs and re-renders the panel, so the
items array and the join over the whole buffer were rebuilt per frame —
and slotContent is a prop rather than a render callback, so both ran even
with the accordion collapsed. Cost grew with the buffer, making a long run
quadratic.

Signed-off-by: mschwab <mschwab@nvidia.com>
Comment thread web/packages/studio/src/components/AnonymizerRecordView/HighlightedText.tsx Outdated
Comment thread web/packages/studio/src/components/AnonymizerRecordView/parse.test.ts Outdated
Comment thread web/packages/studio/src/components/AnonymizerRecordView/ReplacementMapTable.tsx Outdated
…332]

Bind the replacement map's data columns with col.accessor so they carry a data
model rather than being unlinked display columns; the arrow column stays
display. enableSorting is false by default in useCustomReactTable, so this adds
no sort UI.

Drop the explicit displayName assignments. There is no react/display-name rule
in the web config and most memo components in studio already omit them.

Share the trace row between parse.test.ts and AnonymizerRecordView.test.tsx via
testFixtures.ts. The parsed entity list and the wire-shaped final_entities stay
separate literals so the parseEntities assertion is not checked against a second
implementation of the same mapping.

Add a buildReplacedEntities regression case where a duplicate of the synthetic
value sits between searchFrom and the true position after a length-changing
replacement. The gap prediction in locate() is what keeps this correct; without
it the preview would tag the earlier literal.

Signed-off-by: mschwab <mschwab@nvidia.com>
@marcusds
marcusds enabled auto-merge August 5, 2026 21:26
@marcusds
marcusds added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 5af86de Aug 5, 2026
52 checks passed
@marcusds
marcusds deleted the astd-332-anonymizer-live-preview-panel/mschwab branch August 5, 2026 21:48
ryana pushed a commit to ryana/nemo-platform that referenced this pull request Aug 12, 2026
…VIDIA-NeMo#1094)

* docs(studio): describe the import order the linter actually enforces

The guideline said "group imports: external libraries, internal modules,
relative imports", which is not what `import/order` does. It collapses
builtin and external into one group and alphabetizes within it, and the
`@nemo` / `@nvidia` / `@studio` aliases resolve as external — so they sort
alongside `react` instead of forming an internal group.

Reviewers read the guideline, see an alias ahead of a package, and file it
as a defect. CodeRabbit did exactly that on NVIDIA-NeMo#1005, citing "Coding
guidelines" as its source. Its suggested reordering fails `--max-warnings 0`.

State the rule's real behaviour, and tell CodeRabbit that import order is
lint-owned and not reviewable.

Signed-off-by: mschwab <mschwab@nvidia.com>

* docs(studio): point at pnpm lint:fix, not eslint directly

The repo script carries --report-unused-disable-directives and --max-warnings 0 and uses the pinned ESLint; AGENTS.md already says to go through package scripts rather than invoking tools directly.

Signed-off-by: mschwab <mschwab@nvidia.com>

---------

Signed-off-by: mschwab <mschwab@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants