Spaces:
Paused
Paused
File size: 6,272 Bytes
dc4d6fd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | # 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
<div class="text-content"
id="text-content-{field_key}"
data-original-text="{escaped_plain_text}"
style="position: relative; padding-top: 24px;">
{content HTML}
</div>
```
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/<id>` 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 |
|