# Display Type System Architecture This document describes the design contracts and extension points for the display type system in `potato/server_utils/displays/`. ## Overview The display system separates **content presentation** from **annotation collection**. Each field in `instance_display.fields` has a `type` that maps to a registered display class. Displays produce HTML; annotation schemas collect labels. ``` Config YAML └─ instance_display.fields[].type └─ DisplayRegistry.render() └─ BaseDisplay.render() → inner HTML └─ render_display_container() → wrapped HTML └─ template {{ display_html | safe }} ``` ## Key Files | File | Purpose | |------|---------| | `base.py` | `BaseDisplay` ABC — class attributes, abstract `render()`, helpers | | `registry.py` | `DisplayRegistry` singleton — registration, lookup, render dispatch | | `../instance_display.py` | `InstanceDisplayRenderer` — orchestrates field rendering | | `__init__.py` | Package exports | ## BaseDisplay Contract ### Required to implement | Method / Attribute | Description | |--------------------|-------------| | `name: str` | Unique type identifier (e.g., `"dialogue"`) | | `render(field_config, data) -> str` | Return inner HTML for the field content | ### Optional to override | Method | Default | When to override | |--------|---------|------------------| | `get_css_classes(field_config)` | `["display-field", "display-type-{name}"]` | Add type-specific classes | | `get_data_attributes(field_config, data)` | `{"field-key", "field-type", "span-target"}` | Add custom data attrs | | `get_js_init()` | `None` | Return JS to run on page load | | `validate_config(field_config)` | Checks `required_fields` | Add enum/range validation | | `has_inline_label(field_config)` | `False` | Return `True` if display renders its own label (avoids duplicate) | | `get_display_options(field_config)` | Merges `optional_fields` with `display_options` | Rarely needed | ### Class attributes | Attribute | Type | Description | |-----------|------|-------------| | `required_fields` | `List[str]` | Config keys that must be present | | `optional_fields` | `Dict[str, Any]` | Default values for optional display_options | | `description` | `str` | Human-readable description | | `supports_span_target` | `bool` | Whether this type implements the span annotation contract | ## Span Target Contract **If `supports_span_target = True`, the display MUST satisfy these requirements when `field_config["span_target"]` is `True`:** ### 1. `.text-content` wrapper The rendered HTML must contain: ```html
{content HTML}
``` Use the `render_span_wrapper()` helper: ```python if field_config.get("span_target"): inner_html = self.render_span_wrapper(field_key, inner_html, plain_text) ``` ### 2. `data-original-text` must contain plain text The `plain_text` argument to `render_span_wrapper()` must be the canonical plain text that `routes.py` will use for span offset extraction. For structured data (dialogue, lists), use `concatenate_dialogue_text()` from `base.py` so both rendering and API extraction use identical formats. ### 3. CSS classes on the outer container Override `get_css_classes()` to add `"span-target-field"` and `"span-target-{name}"` when span_target is true. ### 4. Text format consistency The text format used in `data-original-text` **MUST** match the text extraction logic in `routes.py` (`/api/spans/` endpoint). If the data is a list of dicts, both sides must use `concatenate_dialogue_text()`. ### Why this matters SpanManager (span-core.js) discovers span-target fields via: ```javascript document.querySelectorAll('.display-field[data-span-target="true"]') ``` Then looks for the text element inside each: ```javascript const textContent = field.querySelector('.text-content'); ``` If `.text-content` is missing, SpanManager silently skips the field and span annotation will not work. ## Registry The `display_registry` singleton provides: - `render(field_type, field_config, data)` — render a field - `get_supported_types()` — list all registered type names - `type_supports_span_target(field_type)` — check span target support - `get_span_target_types()` — list all types supporting span targets - `validate(field_type, field_config)` — validate config - `list_displays()` — metadata for all displays The registry wraps each display's `render()` output in `render_display_container()`, which adds the outer `.display-field` div, label, and `.display-field-content` wrapper. ## Instance Display Renderer `InstanceDisplayRenderer` in `instance_display.py`: 1. Reads `instance_display.fields` from config 2. Queries `display_registry.type_supports_span_target()` for span targets (no hardcoded list) 3. Warns if `span_target: true` is set on an unsupported type 4. Renders each field via `display_registry.render()` 5. Optionally wraps in resizable container (`_wrap_resizable()`) ## Adding a New Display Type 1. Create `my_display.py` with a class extending `BaseDisplay` 2. Set `name`, `required_fields`, `optional_fields`, `description` 3. If supporting span annotation: - Set `supports_span_target = True` - Use `render_span_wrapper()` in `render()` when `span_target` is True - Override `get_css_classes()` to add `span-target-field` 4. Register in `registry.py` via `DisplayDefinition` 5. Add to `__init__.py` exports 6. Add CSS to `styles.css` using `.display-type-{name}` convention 7. Write unit tests verifying render output 8. Write a contract enforcement test (see `test_display_span_contract.py`) ## Shared Utilities | Function | Location | Purpose | |----------|----------|---------| | `render_span_wrapper(field_key, inner_html, plain_text)` | `BaseDisplay` method | Standard `.text-content` wrapper | | `concatenate_dialogue_text(data, speaker_key, text_key)` | `base.py` module | Canonical dialogue→plain text conversion | | `render_display_container(inner_html, classes, attrs, label)` | `base.py` module | Standard outer container wrapper |