davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
18 kB
"""
Pairwise Comparison Layout
Generates a form interface for comparing two items side by side.
Features include:
- Binary mode: Click on preferred tile
- Scale mode: Slider between items (-N to +N)
- Optional tie/no-preference button
- Keyboard shortcuts (1/2/0)
- Support for items_key or inline items configuration
"""
import logging
from typing import Dict, Any, Tuple, List
from potato.ai.ai_help_wrapper import get_ai_wrapper
from .identifier_utils import (
safe_generate_layout,
generate_element_identifier,
generate_validation_attribute,
escape_html_content,
generate_layout_attributes
)
logger = logging.getLogger(__name__)
def generate_pairwise_layout(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]:
"""
Generate HTML for a pairwise comparison interface.
Args:
annotation_scheme (dict): Configuration including:
- name: Schema identifier
- description: Display description
- mode: "binary" (default) or "scale"
- items_key: Key in instance data containing items to compare
- items: Inline items config (alternative to items_key)
- show_labels: Whether to show A/B labels (default: true)
- labels: Custom labels for A/B (default: ["A", "B"])
- allow_tie: Show tie/no-preference option (default: false)
- tie_label: Custom tie button text (default: "No preference")
- sequential_key_binding: Enable keyboard shortcuts (default: true)
- label_requirement (dict): Optional validation settings
For scale mode:
- scale.min: Minimum value (e.g., -3 for "A much better")
- scale.max: Maximum value (e.g., +3 for "B much better")
- scale.step: Step increment (default: 1)
- scale.labels.min: Label for min value
- scale.labels.max: Label for max value
- scale.labels.center: Label for center (default: "Equal")
Returns:
tuple: (html_string, key_bindings)
html_string: Complete HTML for the pairwise interface
key_bindings: List of (key, description) tuples for keyboard shortcuts
"""
return safe_generate_layout(annotation_scheme, _generate_pairwise_layout_internal)
def _generate_pairwise_layout_internal(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]:
"""
Internal function to generate pairwise layout after validation.
"""
logger.debug(f"Generating pairwise layout for schema: {annotation_scheme['name']}")
mode = annotation_scheme.get("mode", "binary")
schema_name = annotation_scheme["name"]
if mode == "scale":
return _generate_scale_mode(annotation_scheme)
elif mode == "multi_dimension":
return _generate_multi_dimension_mode(annotation_scheme)
else:
return _generate_binary_mode(annotation_scheme)
def _generate_binary_mode(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]:
"""
Generate binary mode pairwise interface (clickable tiles).
"""
schema_name = annotation_scheme["name"]
description = annotation_scheme["description"]
# Get configuration options
show_labels = annotation_scheme.get("show_labels", True)
labels = annotation_scheme.get("labels", ["A", "B"])
if len(labels) < 2:
labels = ["A", "B"]
allow_tie = annotation_scheme.get("allow_tie", False)
tie_label = annotation_scheme.get("tie_label", "No preference")
# Get items config
items_key = annotation_scheme.get("items_key", "text")
# Validation attribute
validation = generate_validation_attribute(annotation_scheme)
# Key bindings
key_bindings = []
enable_keybindings = annotation_scheme.get("sequential_key_binding", True)
# Build the HTML
escaped_schema = escape_html_content(schema_name)
escaped_description = escape_html_content(description)
escaped_items_key = escape_html_content(items_key)
# Data attributes for JavaScript initialization
data_attrs = f'data-annotation-type="pairwise" data-schema-name="{escaped_schema}" data-mode="binary" data-items-key="{escaped_items_key}"'
# Layout attributes for grid positioning
layout_attrs = generate_layout_attributes(annotation_scheme)
# Tile labels
label_a = escape_html_content(labels[0])
label_b = escape_html_content(labels[1])
shortcut_a = "[1]" if enable_keybindings else ""
shortcut_b = "[2]" if enable_keybindings else ""
data_key_a = 'data-key="1"' if enable_keybindings else ""
data_key_b = 'data-key="2"' if enable_keybindings else ""
schematic = f"""
<form id="{escaped_schema}" class="annotation-form pairwise pairwise-binary" action="javascript:void(0)" data-annotation-id="{escape_html_content(str(annotation_scheme.get('annotation_id', '')))}" {data_attrs} {layout_attrs}>
{get_ai_wrapper()}
<fieldset schema="{escaped_schema}">
<legend class="pairwise-question">{escaped_description}</legend>
<!-- Compact selection tiles -->
<div class="pairwise-selection-container">
<div class="pairwise-tile" data-value="A" data-schema="{escaped_schema}" tabindex="0" {data_key_a}>
<span class="pairwise-tile-label">{label_a}</span>
<span class="pairwise-tile-shortcut">{shortcut_a}</span>
</div>
<div class="pairwise-tile" data-value="B" data-schema="{escaped_schema}" tabindex="0" {data_key_b}>
<span class="pairwise-tile-label">{label_b}</span>
<span class="pairwise-tile-shortcut">{shortcut_b}</span>
</div>
</div>
"""
# Optional tie button
if allow_tie:
escaped_tie_label = escape_html_content(tie_label)
shortcut_tie = "[0]" if enable_keybindings else ""
data_key_tie = 'data-key="0"' if enable_keybindings else ""
schematic += f"""
<div class="pairwise-extra-options">
<button type="button" class="pairwise-tie-btn" data-value="tie" data-schema="{escaped_schema}" {data_key_tie}>
{escaped_tie_label} {shortcut_tie}
</button>
</div>
"""
# Hidden input for form submission
schematic += f"""
<input type="hidden" class="pairwise-value annotation-input"
name="{escaped_schema}"
schema="{escaped_schema}"
label_name="selection"
validation="{validation}"
value="">
"""
# Add justification section if configured
schematic += _generate_justification_html(annotation_scheme, schema_name)
schematic += """
</fieldset>
</form>
"""
# Add key bindings
if enable_keybindings:
key_bindings.append(("1", f"{schema_name}: {labels[0]}"))
key_bindings.append(("2", f"{schema_name}: {labels[1]}"))
if allow_tie:
key_bindings.append(("0", f"{schema_name}: {tie_label}"))
logger.info(f"Successfully generated pairwise binary layout for {schema_name}")
return schematic, key_bindings
def _generate_scale_mode(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]:
"""
Generate scale mode pairwise interface (slider between items).
"""
schema_name = annotation_scheme["name"]
description = annotation_scheme["description"]
# Get configuration options
show_labels = annotation_scheme.get("show_labels", True)
labels = annotation_scheme.get("labels", ["A", "B"])
if len(labels) < 2:
labels = ["A", "B"]
# Get scale configuration
scale_config = annotation_scheme.get("scale", {})
min_value = scale_config.get("min", -3)
max_value = scale_config.get("max", 3)
step = scale_config.get("step", 1)
default_value = scale_config.get("default", 0)
# Scale labels
scale_labels = scale_config.get("labels", {})
min_label = scale_labels.get("min", f"{labels[0]} is better")
max_label = scale_labels.get("max", f"{labels[1]} is better")
center_label = scale_labels.get("center", "Equal")
# Get items config
items_key = annotation_scheme.get("items_key", "text")
# Validation attribute
validation = generate_validation_attribute(annotation_scheme)
# Generate identifiers
identifiers = generate_element_identifier(schema_name, "scale", "range")
# Build the HTML
escaped_schema = escape_html_content(schema_name)
escaped_description = escape_html_content(description)
escaped_items_key = escape_html_content(items_key)
# Data attributes for JavaScript initialization
data_attrs = f'data-annotation-type="pairwise" data-schema-name="{escaped_schema}" data-mode="scale" data-items-key="{escaped_items_key}"'
# Layout attributes for grid positioning
layout_attrs = generate_layout_attributes(annotation_scheme)
# Escaped labels
label_a = escape_html_content(labels[0])
label_b = escape_html_content(labels[1])
schematic = f"""
<form id="{escaped_schema}" class="annotation-form pairwise pairwise-scale" action="javascript:void(0)" data-annotation-id="{escape_html_content(str(annotation_scheme.get('annotation_id', '')))}" {data_attrs} {layout_attrs}>
{get_ai_wrapper()}
<fieldset schema="{escaped_schema}">
<legend class="pairwise-question">{escaped_description}</legend>
<!-- Compact rating scale slider -->
<div class="pairwise-scale-widget">
<div class="pairwise-scale-labels">
<span class="pairwise-scale-label-min">{label_a}: {escape_html_content(min_label)}</span>
<span class="pairwise-scale-label-center">{escape_html_content(center_label)}</span>
<span class="pairwise-scale-label-max">{label_b}: {escape_html_content(max_label)}</span>
</div>
<input type="range" class="pairwise-scale-slider annotation-input"
name="{escaped_schema}"
schema="{escaped_schema}"
label_name="scale_value"
min="{min_value}" max="{max_value}" step="{step}" value="{default_value}"
validation="{validation}"
oninput="updatePairwiseScaleDisplay(this);"
onchange="registerAnnotation(this);">
<div class="pairwise-scale-value-display">
<span class="pairwise-scale-current-value">{default_value}</span>
</div>
<div class="pairwise-scale-ticks">
"""
# Generate tick marks
tick_values = []
val = min_value
while val <= max_value:
tick_values.append(val)
val += step
for tick in tick_values:
percent = ((tick - min_value) / (max_value - min_value)) * 100 if max_value != min_value else 0
is_center = tick == 0
tick_class = "pairwise-scale-tick center" if is_center else "pairwise-scale-tick"
schematic += f'<span class="{tick_class}" style="left: {percent}%">{tick}</span>'
schematic += """
</div>
</div>
</fieldset>
</form>
"""
# No keyboard shortcuts for scale mode (uses slider)
key_bindings = []
logger.info(f"Successfully generated pairwise scale layout for {schema_name}")
return schematic, key_bindings
def _generate_justification_html(annotation_scheme: Dict[str, Any], schema_name: str) -> str:
"""
Generate justification section HTML (reason checkboxes + rationale textarea).
Used by both binary and multi_dimension modes when ``justification`` config is present.
"""
justification = annotation_scheme.get("justification")
if not justification:
return ""
reason_categories = justification.get("reason_categories", [])
min_chars = justification.get("min_rationale_chars", 0)
placeholder = justification.get("rationale_placeholder", "Explain your preference...")
required = justification.get("required", False)
escaped_schema = escape_html_content(schema_name)
# Reason category checkboxes
reasons_html = ""
for reason in reason_categories:
escaped_reason = escape_html_content(reason)
reasons_html += f"""
<label><input type="checkbox" class="pairwise-reason-cb"
data-schema="{escaped_schema}" value="{escaped_reason}">
{escaped_reason}</label>"""
req_attr = 'data-required="true"' if required else ''
html = f"""
<div class="pairwise-justification" data-schema="{escaped_schema}" {req_attr}>
<div class="pairwise-justification-title">Justification</div>
"""
if reasons_html:
html += f"""
<div class="pairwise-reason-categories">{reasons_html}</div>
"""
html += f"""
<textarea class="pairwise-rationale-textarea"
data-schema="{escaped_schema}"
data-min-chars="{min_chars}"
placeholder="{escape_html_content(placeholder)}"
oninput="updatePairwiseRationaleCounter(this);"></textarea>
<div class="pairwise-rationale-counter" data-schema="{escaped_schema}">
0 / {min_chars} characters
</div>
<input type="hidden" class="annotation-input pairwise-justification-value"
name="{escaped_schema}:::justification"
schema="{escaped_schema}"
label_name="justification"
value="">
</div>
"""
return html
def _generate_multi_dimension_mode(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]:
"""
Generate multi-dimension pairwise interface.
Each dimension gets its own A/B tile row with a dimension label.
Hidden inputs use ``{schema}:::{dimension}`` naming.
"""
schema_name = annotation_scheme["name"]
description = annotation_scheme["description"]
dimensions = annotation_scheme.get("dimensions", [])
if not dimensions:
raise ValueError(f"pairwise multi_dimension mode requires 'dimensions' list for schema '{schema_name}'")
labels = annotation_scheme.get("labels", ["A", "B"])
if len(labels) < 2:
labels = ["A", "B"]
items_key = annotation_scheme.get("items_key", "text")
validation = generate_validation_attribute(annotation_scheme)
layout_attrs = generate_layout_attributes(annotation_scheme)
escaped_schema = escape_html_content(schema_name)
escaped_items_key = escape_html_content(items_key)
label_a = escape_html_content(labels[0])
label_b = escape_html_content(labels[1])
data_attrs = (
f'data-annotation-type="pairwise" data-schema-name="{escaped_schema}" '
f'data-mode="multi_dimension" data-items-key="{escaped_items_key}"'
)
schematic = f"""
<form id="{escaped_schema}" class="annotation-form pairwise pairwise-multi-dimension"
action="javascript:void(0)"
data-annotation-id="{escape_html_content(str(annotation_scheme.get('annotation_id', '')))}"
{data_attrs} {layout_attrs}>
{get_ai_wrapper()}
<fieldset schema="{escaped_schema}">
<legend class="pairwise-question">{escape_html_content(description)}</legend>
"""
key_bindings = []
for i, dim in enumerate(dimensions):
dim_name = dim.get("name", f"dimension_{i}")
dim_desc = dim.get("description", "")
allow_tie = dim.get("allow_tie", annotation_scheme.get("allow_tie", False))
tie_label = dim.get("tie_label", annotation_scheme.get("tie_label", "Tie"))
escaped_dim = escape_html_content(dim_name)
dim_schema_key = f"{escaped_schema}:::{escaped_dim}"
tie_btn_html = ""
if allow_tie:
tie_btn_html = f"""
<button type="button" class="pairwise-tie-btn"
data-value="tie" data-schema="{escaped_schema}"
data-dimension="{escaped_dim}">
{escape_html_content(tie_label)}
</button>"""
schematic += f"""
<div class="pairwise-dimension-row" data-dimension="{escaped_dim}">
<div class="pairwise-dimension-label">{escape_html_content(dim_name.replace('_', ' ').title())}</div>
<div class="pairwise-dimension-description">{escape_html_content(dim_desc)}</div>
<div class="pairwise-dimension-tiles">
<div class="pairwise-tile" data-value="A" data-schema="{escaped_schema}"
data-dimension="{escaped_dim}" tabindex="0">
<span class="pairwise-tile-label">{label_a}</span>
</div>
<div class="pairwise-tile" data-value="B" data-schema="{escaped_schema}"
data-dimension="{escaped_dim}" tabindex="0">
<span class="pairwise-tile-label">{label_b}</span>
</div>
{tie_btn_html}
</div>
<input type="hidden" class="pairwise-value annotation-input pairwise-dim-input"
name="{dim_schema_key}"
schema="{escaped_schema}"
label_name="{escaped_dim}"
data-dimension="{escaped_dim}"
validation="{validation}"
value="">
</div>
"""
# Add justification section if configured
schematic += _generate_justification_html(annotation_scheme, schema_name)
schematic += """
</fieldset>
</form>
"""
logger.info(f"Successfully generated pairwise multi_dimension layout for {schema_name}")
return schematic, key_bindings