Skip to content

feat(annotation): render collapsible widget content as markdown + HTML#233

Draft
henrycgbaker wants to merge 21 commits into
mainfrom
feat/widget-render-html-content
Draft

feat(annotation): render collapsible widget content as markdown + HTML#233
henrycgbaker wants to merge 21 commits into
mainfrom
feat/widget-render-html-content

Conversation

@henrycgbaker

@henrycgbaker henrycgbaker commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Brings the collapsible widget's record-content rendering in line with the main rg.TextField rendering (see #232). Pipeline content (e.g. publikationsbot output) is markdown with embedded HTML/XML structure tags; we want the widget to render it the same way Argilla's use_markdown=True renderer renders the main TextField.

Two commits on this branch (will squash on merge):

  1. textContentinnerHTML on the #content slot.
  2. Add marked.js via CDN, run record content through marked.parse(text) before assigning to innerHTML.

Together: widget renders headings, bold, numbered lists, line breaks, and embedded HTML — same as the main TextField.

Trust assumption: record content is first-party pipeline output; no XSS sanitisation needed.

Files changed

  • src/pragmata/core/annotation/collapsible_field.html
    • marked.min.js <script src=...> in <head>
    • marked.parse(text) then innerHTML (with graceful fallback to plain passthrough if the CDN load fails)
    • Drops white-space: pre-wrap on #content (markdown rendering handles its own whitespace) and tightens first/last child margins so rendered HTML sits flush in the collapsed area.

Relationship to #232

Independent of #232 (no code/test dependency). #232 configures use_markdown on the main rg.TextField chrome; this PR is about the embedded collapsible widget's own iframe. They touch disjoint code and can land in either order.

Note on marked.js via CDN

Loaded from jsdelivr (https://cdn.jsdelivr.net/npm/marked@12.0.0/marked.min.js). Annotator browsers fetch it per record. Works through normal corporate proxies; deployments behind strict firewalls that block CDNs fall back to plain passthrough (graceful degradation: text still shows, just unrendered). If that's a concern, a follow-up could inline a smaller parser (e.g. snarkdown, ~1KB) — flagged but not done here since the demo deployment confirmed CDN works.

Test plan

  • Manual: verified on a demo deployment with publikationsbot-emitted markdown-with-embedded-HTML content. Widget renders identically to the main use_markdown=True TextField for the same content shape.
  • Reviewer: confirm marked@12.0.0 pin is acceptable; or request inline parser instead.

Hoist the four binary implication rules out of three hand-written check_*
functions into a frozen ConstraintRule dataclass list. The check_* helpers
now return derived violation strings, preserving existing export CSV and
constraint_summary key formats. Adds to_widget_payload() so the same
definitions can drive an annotator-time JS widget without duplication.
The term "rule" was too generic for a binary-implication constraint between
question values; "logical constraint" reflects intent more directly. Renames:
- ConstraintRule → LogicalConstraint
- CONSTRAINT_RULES → LOGICAL_CONSTRAINTS
- rule_id field → constraint_id
- constraint_rules.py → logical_constraints.py
- violated_rules() → violated_constraints()
- LogicalConstraint.applies/violated_by: row: Any → row: object
  (matches constraints.py and reflects the actual surface — getattr-only access)
- Replace `is`/`is not` with `==`/`\!=` for boolean comparisons
  (idiomatic value comparison; ruff E712-safe)
Introduced as a structured-form alternative for callers needing severity,
but never used. The same shape is available by walking LOGICAL_CONSTRAINTS
directly when a real caller appears.
…et PR

Move severity, message, Severity Literal, and to_widget_payload off
LogicalConstraint until the constraint widget actually lands. The fields
had no consumer in this PR and the docstring referenced a
constraints_field.html template that does not yet exist.

Also drop misleading "backwards-compat" framing from constraints module
docstrings and align LogicalConstraint docstring with the runtime: the
export uses violation_string() as the constraint_summary key, not
constraint_id.
…ks.py

The module name was ambiguous now that constraint validation runs in two
places (live UI widget at annotation time, post-hoc audit at export time).
Rename to make the file's purpose explicit and expand the docstring to
spell out the dual-half story.
Adds an annotation-time UI surface for the logical constraints already
validated at export time. A new constraints_panel rg.CustomField (srcdoc
iframe + same-origin parent DOM access, mirroring the existing discard
widget pattern) re-evaluates each rule against the current chip state on
every observed change.

- Warn-severity rules (heuristic): yellow banner; submission allowed.
- Block-severity rules (logical implications): red banner; native
  Submit button is disabled until the violation is resolved.

The panel is injected above .footer-form so the warning is adjacent to
the action area. Inline styles + DOM are inserted into the parent doc,
the iframe's own host box is collapsed.

Argilla v2 record-field contract: the frontend silently skips rendering
a CustomField when the record has no value for that field, so every
record now carries a placeholder for both constraints_panel and
discard_flow via WIDGET_FIELD_PLACEHOLDERS.

Argilla DOM contract documented inline in the widget (selector targets
verified against argilla-frontend v2.8.0 source). Maintenance surface
matches the existing discard widget.
- argilla_task_definitions imports LOGICAL_CONSTRAINTS / drops CONSTRAINT_RULES
- constraints_field.html renames JS RULES → CONSTRAINTS, rule_id → constraint_id,
  and the .rule-msg/.rule-cite CSS classes → .constraint-msg/.constraint-cite
- tests updated to the new names
- Submit-gate state-machine: detect drift between visible-state signature
  and the actual `dataset.constraintBlocked` on the submit button (covers
  the case where the very first tick evaluated a block before the button
  was mounted in parent DOM)
- Cite line renders human question titles from QUESTION_TITLES rather
  than raw snake_case names
- hideHostBox catch now logs via console.warn so future Argilla DOM
  changes that hide the host class are visible in dev tools
- cardFor docstring: note dependence on one-visible-record invariant
  (Argilla v2.8.0 shows records one at a time)
…get PR

Re-add severity, message, Severity Literal, and to_widget_payload on
LogicalConstraint where the widget actually consumes them. These fields
were lifted out of the SSOT PR (01) because they had no consumer there.

Also fix stale design-doc reference (constraint_rules.CONSTRAINT_RULES
to logical_constraints.LOGICAL_CONSTRAINTS) and tighten to_widget_payload
to a direct dict literal (drops unused task field from the wire payload,
removes asdict import).
Severity is a runtime setting, not a property of the rule, so its
defaults live in AnnotationSettings.constraint_severity alongside other
deployment-scope defaults (production_min_submitted, calibration_min_submitted).

- LogicalConstraint no longer carries a severity field; the dataclass
  is now pure rule definition.
- AnnotationSettings.constraint_severity ships full defaults via
  _DEFAULT_CONSTRAINT_SEVERITY. A model_validator(mode="before") overlays
  user-supplied entries on top, so configs need only specify the
  constraint_ids they want to override (sparse overlay).
- Unknown constraint_ids are rejected at construction time.
- build_task_settings(settings) and _render_constraints_template take
  settings; the widget reads severity from settings.constraint_severity.
- @functools.cache dropped from build_task_settings since the result
  depends on settings; callers hold the dict for the import duration.
Extract the per-task fields-list assembly (content + constraints_panel +
discard_flow) and rg.Settings construction into a single inner
assemble() helper, so the three task return entries become a single
content_fields argument rather than repeating the discard+constraints
wiring per task.

Pure refactor: no behaviour change.
Adds per-workspace overrides on top of the deployment-scope defaults
introduced in the widget PR.

- WorkspaceSettings.constraint_severity: sparse constraint_id to severity
  map, empty default. Listed constraint_ids override deployment values
  for that workspace only; omitted constraint_ids fall through.
- AnnotationSettings.resolved_severity(workspace_name, constraint_id)
  walks workspace then deployment. Unknown constraint_ids raise KeyError.
- AnnotationSettings.task_to_workspace() inverts the topology, consolidating
  the lookup that record_builder.fan_out_records and build_task_settings
  both need. _invert_workspace_map removed from record_builder.
- The constraint widget now renders with the workspace-resolved severity
  per task, so per-workspace overrides reach the annotator UI.
- build_task_settings only builds Settings for tasks present in the
  workspaces topology (avoids resolving severity against a missing
  workspace; the unused rg.Settings had no consumer either way).
- Validator rejects unknown constraint_ids at workspace scope too.
CONSTRAINT_CHECKERS and the check_retrieval/check_grounding/check_generation
helpers now return list[LogicalConstraint] instead of list[str]. The per-row
CSV constraint_details column is unchanged - export_runner generates it via
LogicalConstraint.violation_string() at write time. export_meta.json's
constraint_summary is now keyed by the stable short constraint_id (e.g.
"evidence_requires_relevance") instead of the verbose violation message,
matching how AnnotationSettings.constraint_severity overrides are keyed.
Flips the collapsible_field.html widget to use innerHTML for
record-supplied content (instead of textContent) so embedded HTML in
pipeline-emitted content renders rather than escapes. Trust assumption:
record content comes from a first-party pipeline.

Known incomplete: innerHTML alone does not parse markdown, and live
pipeline content is markdown-with-embedded-HTML (not pure HTML), so
markdown syntax in the widget will still display unrendered. A
follow-up should add a markdown parser to the widget template.
@github-actions github-actions Bot added the feature Adds or expands user-facing functionality label May 27, 2026
@github-actions github-actions Bot added the size: XS < 50 LOC label May 27, 2026
Pipeline content is markdown with embedded HTML/XML structure tags, not
pure HTML. The main TextField gets this right via use_markdown=True
(Argilla's renderer handles markdown + embedded HTML); switching the
widget to innerHTML alone left markdown raw (asterisks, numbered lists,
newlines unrendered).

Adds marked.js via CDN, runs record content through marked.parse() then
sets innerHTML. Widget now renders identically to the use_markdown=True
TextField. Falls back to plain passthrough if the CDN load fails. Trust
assumption: pipeline output is first-party (no sanitisation needed).
@henrycgbaker henrycgbaker changed the title [WIP] feat(annotation): render collapsible widget content as HTML feat(annotation): render collapsible widget content as markdown + HTML May 27, 2026
Wrap marked.parse(text) in try/catch so an unexpected library exception
on malformed input doesn't break widget rendering. Falls back to the
same passthrough as the marked-undefined case (raw text via innerHTML).
@henrycgbaker henrycgbaker added the annotation Changes affecting the annotation tool label May 27, 2026
@henrycgbaker henrycgbaker marked this pull request as draft May 28, 2026 07:31
@henrycgbaker henrycgbaker force-pushed the feat/annotation-logical-constraints/05-export-summary-by-id branch from 9fe6999 to 8b9eda6 Compare June 3, 2026 13:45
@henrycgbaker henrycgbaker force-pushed the feat/annotation-logical-constraints/05-export-summary-by-id branch 2 times, most recently from 850c15c to bd5a2e1 Compare July 2, 2026 08:46
@henrycgbaker henrycgbaker force-pushed the feat/annotation-logical-constraints/05-export-summary-by-id branch 3 times, most recently from 53ba408 to f9b6faa Compare July 3, 2026 12:06
Base automatically changed from feat/annotation-logical-constraints/05-export-summary-by-id to main July 3, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

annotation Changes affecting the annotation tool feature Adds or expands user-facing functionality size: XS < 50 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant