""" Constant Sum / Points Allocation Layout Generates number inputs (or sliders) constrained to sum to a fixed total. Forces annotators to make relative comparisons between categories. Research basis: - Louviere et al. (2015) "Best-Worst Scaling: Theory, Methods and Applications" Cambridge University Press - Thurstone (1927) paired-comparison law of comparative judgment """ import logging 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, generate_tooltip_html, ) logger = logging.getLogger(__name__) DEFAULT_TOTAL_POINTS = 100 DEFAULT_MIN_PER_ITEM = 0 DEFAULT_INPUT_TYPE = "number" def generate_constant_sum_layout(annotation_scheme): """ Generate HTML for a constant sum / points allocation interface. Args: annotation_scheme (dict): Configuration including: - name: Schema identifier - description: Display description - labels: List of category names - total_points: Sum budget (default 100) - min_per_item: Minimum per item (default 0) - input_type: "number" or "slider" (default "number") Returns: tuple: (html_string, key_bindings) """ return safe_generate_layout(annotation_scheme, _generate_constant_sum_layout_internal) def _generate_constant_sum_layout_internal(annotation_scheme): schema_name = annotation_scheme["name"] safe_schema = escape_html_content(schema_name) description = annotation_scheme["description"] labels = annotation_scheme.get("labels", []) total_points = annotation_scheme.get("total_points", DEFAULT_TOTAL_POINTS) min_per_item = annotation_scheme.get("min_per_item", DEFAULT_MIN_PER_ITEM) input_type = annotation_scheme.get("input_type", DEFAULT_INPUT_TYPE) layout_attrs = generate_layout_attributes(annotation_scheme) validation = generate_validation_attribute(annotation_scheme) if not labels: raise ValueError(f"constant_sum schema '{schema_name}' requires 'labels'") # Normalize labels label_names = [] for lbl in labels: if isinstance(lbl, str): label_names.append(lbl) elif isinstance(lbl, dict) and "name" in lbl: label_names.append(lbl["name"]) else: raise ValueError(f"Invalid label format: {lbl}") html = f"""
{get_ai_wrapper()}
{escape_html_content(description)}
Allocated: 0 / {total_points} Remaining: {total_points}
""" for i, lbl in enumerate(label_names): safe_lbl = escape_html_content(lbl) identifiers = generate_element_identifier(schema_name, lbl, "number") tooltip_html = "" if isinstance(labels[i], dict): tooltip_html = generate_tooltip_html(labels[i]) if input_type == "slider": html += f"""
{min_per_item}
""" else: html += f"""
""" html += f"""
""" key_bindings = [] logger.info(f"Generated constant_sum layout for {schema_name} with {len(label_names)} categories") return html, key_bindings