feat(studio): anonymizer builder live preview panel [ASTD-332] - #1005
Draft
marcusds wants to merge 9 commits into
Draft
feat(studio): anonymizer builder live preview panel [ASTD-332]#1005marcusds wants to merge 9 commits into
marcusds wants to merge 9 commits into
Conversation
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>
Contributor
|
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 Settingstabs) streams the current form config through the anonymizer/previewendpoint and renders:‹ Record 1 of N ›), loading skeletons, empty state, error bannerlogframesWhile a run is in flight the button becomes Stop.
Full Runmoves 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 leftCancelstranded. Exit is via the breadcrumb.Why
previewApi.tsis hand-written rather than generatedThe request half is generated —
PreviewRequestcomes from the SDK. Only the transport and the frame types are hand-rolled, for two reasons:The generated client can't stream.
anonymizerPreviewfunctionRouteexists, but it routes through the axios mutator ingenerated/fetchers/anonymizer.ts, which buffers the whole body. A preview streams log frames for minutes; through that client you'd see nothing until it finished, which defeats the panel.The frames aren't in the spec to generate from.
plugins/nemo-anonymizer/openapi/openapi.yamldeclares the 200 as:so orval emits
customFetch<unknown>and no frame schemas. That traces tofunctions/routes.py:203, where the sharedNemoFunctionroute factory registers streaming functions withresponse_model=None.Worth fixing upstream, not here. If the factory declared the frame union and
application/x-ndjsoninresponses=, orval would generateLogFrame | PreviewDatasetFrame | TraceDatasetFrame | FailedRecordsFrame | Heartbeat | Done | Errorand roughly 60 lines of hand-written types and guards would come out ofpreviewApi.ts. That's a platform-wide change affecting every streaming function plus a spec/SDK regen, so it wants its own ticket. The streaming transport itself would stay hand-written either way — 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.tsmirrors itsdisplay.pylogic: the exact → value-only → case-insensitive lookup ladder, plus the build-from-replacement-map fallbacks used when detection returns no spans but the map is populated.PII tags use
Badge, notTag. Code Connect on the FigmaPII Tagcomponent maps toBadge(solidfor the value,outlinefor the label);Tagis 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 underabandon_on_cancel=True, the worker thread is abandoned rather than joined, so an in-flight model call still finishes.The shared-URL-param hazard.
useStudioDataViewStatesyncs to unprefixed params (page,page_size,sort, …). That bit inside this PR: with a map that overflows a page, going to?page=2then 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 alongsideResultsPreviewTable; that wants aparamPrefixoption on the shared hook.Reuse audit. Four things this branch had reimplemented now use what Studio already ships:
isAbortError(common's version also handlesAPIUserAbortError, mine only caughtDOMException), the NDJSON read loop (extracted asreadLineDelimitedStream, now shared with the data designer preview and given the chunk-boundary test neither call site had),OUTPUT_SUFFIXES(was declared here and inAnonymizerJobDetailRoute), andStackedSkeleton.The record view lives in
components/AnonymizerRecordView/so the job detail page (ASTD-330) and evaluation results (ASTD-322) can reuse it.No progress indicator is available
The stream carries no
progressframe —frames.pyreserves the kind but the plugin never emits one — and records arrive only after the run completes, so "3 of 10" isn't derivable. The library does compute an ETA, but it's gated ontotal >= 50while preview caps at 10, so it can never fire. Only phase log lines and a 5s heartbeat are available today.Testing
parse,previewApi,lineStream, the record view and the tab/heading helpers