feat(annotation): render collapsible widget content as markdown + HTML#233
Draft
henrycgbaker wants to merge 21 commits into
Draft
feat(annotation): render collapsible widget content as markdown + HTML#233henrycgbaker wants to merge 21 commits into
henrycgbaker wants to merge 21 commits into
Conversation
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.
…rop module constant
…lections with bold values
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.
3 tasks
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).
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).
9fe6999 to
8b9eda6
Compare
850c15c to
bd5a2e1
Compare
53ba408 to
f9b6faa
Compare
Base automatically changed from
feat/annotation-logical-constraints/05-export-summary-by-id
to
main
July 3, 2026 13:56
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.
Summary
Brings the collapsible widget's record-content rendering in line with the main
rg.TextFieldrendering (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'suse_markdown=Truerenderer renders the main TextField.Two commits on this branch (will squash on merge):
textContent→innerHTMLon the#contentslot.marked.jsvia CDN, run record content throughmarked.parse(text)before assigning toinnerHTML.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.htmlmarked.min.js<script src=...>in<head>marked.parse(text)theninnerHTML(with graceful fallback to plain passthrough if the CDN load fails)white-space: pre-wrapon#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_markdownon the mainrg.TextFieldchrome; this PR is about the embedded collapsible widget's own iframe. They touch disjoint code and can land in either order.Note on
marked.jsvia CDNLoaded 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
use_markdown=TrueTextField for the same content shape.marked@12.0.0pin is acceptable; or request inline parser instead.