| """ |
| Multiselect Layout |
| |
| Generates a form interface that allows users to select multiple options from a list |
| of choices. Features include: |
| - Multiple column layout support |
| - Keyboard shortcuts |
| - Required/optional validation |
| - Individual label requirements |
| - Tooltip support |
| - Video label support |
| - Free response option |
| """ |
|
|
| import logging |
| from collections.abc import Mapping |
|
|
| from potato.ai.ai_help_wrapper import get_ai_wrapper, get_dynamic_ai_help |
| from .identifier_utils import ( |
| safe_generate_layout, |
| generate_element_identifier, |
| generate_validation_attribute, |
| escape_html_content, |
| generate_layout_attributes, |
| display_label_text, |
| ) |
|
|
|
|
| logger = logging.getLogger(__name__) |
|
|
| def generate_multiselect_layout(annotation_scheme): |
| """ |
| Generate HTML for a multiple-choice selection interface. |
| |
| Args: |
| annotation_scheme (dict): Configuration including: |
| - name: Schema identifier |
| - description: Display description |
| - labels: List of label configurations, each either: |
| - str: Simple label text |
| - dict: Complex label with: |
| - name: Label identifier |
| - tooltip: Hover text description |
| - tooltip_file: Path to file containing tooltip text |
| - key_value: Keyboard shortcut key |
| - videopath: Path to video file (if video_as_label=True) |
| - display_config (dict): Optional display settings |
| - num_columns: Number of columns to arrange options (default: 1) |
| - label_requirement (dict): Optional validation settings |
| - required (bool): Whether any selection is mandatory |
| - required_label (str|list): Specific labels that must be selected |
| - sequential_key_binding (bool): Enable numeric key shortcuts |
| - video_as_label (bool): Use videos instead of text for labels |
| - has_free_response (dict): Optional free text input configuration |
| - instruction: Label for free response field |
| |
| Returns: |
| tuple: (html_string, key_bindings) |
| html_string: Complete HTML for the multiselect interface |
| key_bindings: List of (key, description) tuples for keyboard shortcuts |
| """ |
| return safe_generate_layout(annotation_scheme, _generate_multiselect_layout_internal) |
|
|
| def _generate_multiselect_layout_internal(annotation_scheme): |
| """ |
| Internal function to generate multiselect layout after validation. |
| """ |
| logger.debug(f"Generating multiselect layout for schema: {annotation_scheme['name']}") |
|
|
| |
| layout_attrs = generate_layout_attributes(annotation_scheme) |
|
|
| |
| schematic = f""" |
| <form id="{escape_html_content(annotation_scheme['name'])}" class="annotation-form multiselect shadcn-multiselect-container" action="javascript:void(0)" data-annotation-id="{escape_html_content(str(annotation_scheme.get("annotation_id", "")))}" data-annotation-type="multiselect" data-schema-name="{escape_html_content(annotation_scheme['name'])}" {layout_attrs}> |
| {get_ai_wrapper()} |
| <fieldset schema="{escape_html_content(annotation_scheme['name'])}"> |
| <legend class="shadcn-multiselect-title">{escape_html_content(annotation_scheme['description'])}</legend> |
| """ |
|
|
| |
| key2label = {} |
| label2key = {} |
| key_bindings = [] |
|
|
| |
| display_info = annotation_scheme.get("display_config", {}) |
| n_columns = display_info.get("num_columns", 1) |
| logger.debug(f"Using {n_columns} column layout") |
|
|
| |
| schematic += f'<div class="shadcn-multiselect-grid" style="grid-template-columns: repeat({n_columns}, 1fr);">' |
|
|
| |
| allocated_keys = annotation_scheme.get("_allocated_keys", None) |
| allocated_map = {} |
| if allocated_keys: |
| for entry in allocated_keys: |
| if entry.get("key"): |
| allocated_map[entry["label"]] = entry["key"] |
|
|
| |
| for i, label_data in enumerate(annotation_scheme["labels"], 1): |
| |
| label = label_data if isinstance(label_data, str) else label_data["name"] |
|
|
| |
| identifiers = generate_element_identifier(annotation_scheme["name"], label, "checkbox") |
| validation = generate_validation_attribute(annotation_scheme, label) |
|
|
| |
| label_value = label |
|
|
| |
| tooltip = "" |
| if isinstance(label_data, Mapping): |
| tooltip = _generate_tooltip(label_data) |
|
|
| |
| if "key_value" in label_data: |
| shortcut_key = str(label_data["key_value"]) |
| if shortcut_key in key2label: |
| logger.warning(f"Keyboard input conflict: {shortcut_key}") |
| continue |
| key2label[shortcut_key] = label |
| label2key[label] = shortcut_key |
| key_bindings.append((shortcut_key, f"{identifiers['schema']}: {label}")) |
| logger.debug(f"Added key binding '{shortcut_key}' for label '{label}'") |
|
|
| |
| if label in allocated_map and label not in label2key: |
| shortcut_key = allocated_map[label] |
| key2label[shortcut_key] = label |
| label2key[label] = shortcut_key |
| key_bindings.append((shortcut_key, f"{identifiers['schema']}: {label}")) |
| logger.debug(f"Added allocated key binding '{shortcut_key}' for label '{label}'") |
| elif not allocated_keys: |
| |
| if (annotation_scheme.get("sequential_key_binding") |
| and len(annotation_scheme["labels"]) <= 10 |
| and label not in label2key): |
| shortcut_key = str(i % 10) |
| key2label[shortcut_key] = label |
| label2key[label] = shortcut_key |
| key_bindings.append((shortcut_key, f"{identifiers['schema']}: {label}")) |
| logger.debug(f"Added sequential key binding '{shortcut_key}' for label '{label}'") |
|
|
| |
| label_content = _format_label_content(label_data, annotation_scheme) |
|
|
| |
| key_display = f'<span class="keybinding-badge shadcn-multiselect-key">{label2key[label].upper()}</span>' if label in label2key else '' |
|
|
| |
| data_key_attr = f' data-key="{label2key[label]}"' if label in label2key else '' |
|
|
| |
| schematic += f""" |
| <div class="shadcn-multiselect-item"> |
| <input class="{identifiers['schema']} shadcn-multiselect-checkbox annotation-input" |
| type="checkbox" |
| id="{identifiers['id']}" |
| name="{identifiers['name']}" |
| value="{escape_html_content(label_value)}" |
| label_name="{identifiers['label_name']}" |
| schema="{identifiers['schema']}" |
| onclick="whetherNone(this);registerAnnotation(this)" |
| validation="{validation}"{data_key_attr}> |
| <label for="{identifiers['id']}" {tooltip} schema="{identifiers['schema']}" class="shadcn-multiselect-label"> |
| {label_content} {key_display} |
| </label> |
| </div> |
| """ |
|
|
| schematic += "</div>" |
|
|
| |
| if annotation_scheme.get("has_free_response"): |
| schematic += _generate_free_response(annotation_scheme, n_columns) |
|
|
| schematic += "</fieldset></form>" |
|
|
| logger.info(f"Successfully generated multiselect layout for {annotation_scheme['name']} " |
| f"with {len(annotation_scheme['labels'])} options") |
| return schematic, key_bindings |
|
|
| def _generate_tooltip(label_data): |
| """ |
| Generate tooltip HTML attribute from label data. |
| |
| Args: |
| label_data (dict): Label configuration containing tooltip information |
| |
| Returns: |
| str: Tooltip HTML attribute or empty string if no tooltip |
| """ |
| tooltip_text = "" |
| if "tooltip" in label_data: |
| tooltip_text = label_data["tooltip"] |
| elif "tooltip_file" in label_data: |
| try: |
| with open(label_data["tooltip_file"], "rt", encoding="utf-8") as f: |
| tooltip_text = "".join(f.readlines()) |
| except Exception as e: |
| logger.error(f"Failed to read tooltip file: {e}") |
| return "" |
|
|
| if tooltip_text: |
| escaped_tooltip = escape_html_content(tooltip_text) |
| return f'data-toggle="tooltip" data-html="true" data-placement="top" title="{escaped_tooltip}"' |
| return "" |
|
|
| def _format_label_content(label_data, annotation_scheme): |
| """ |
| Format the label content, handling both text and video labels. |
| |
| Args: |
| label_data: Label configuration |
| annotation_scheme: Full annotation scheme configuration |
| |
| Returns: |
| str: Formatted label content (text or video HTML) |
| """ |
| if annotation_scheme.get("video_as_label") and isinstance(label_data, dict) and "videopath" in label_data: |
| |
| video_path = label_data["videopath"] |
| return f'<video src="{escape_html_content(video_path)}" controls style="max-width: 200px; max-height: 150px;"></video>' |
| else: |
| |
| |
| return escape_html_content( |
| display_label_text(label_data, annotation_scheme) |
| ) |
|
|
| def _generate_free_response(annotation_scheme, n_columns): |
| """ |
| Generate free response field for multiselect. |
| |
| Args: |
| annotation_scheme: Schema configuration |
| n_columns: Number of columns in the grid |
| |
| Returns: |
| str: HTML for free response field |
| """ |
| free_response_identifiers = generate_element_identifier(annotation_scheme["name"], "free_response", "text") |
| free_response_config = annotation_scheme["has_free_response"] |
| instruction = free_response_config.get("instruction", "Other (please specify)") if isinstance(free_response_config, dict) else "Other (please specify)" |
|
|
| return f""" |
| <div class="shadcn-multiselect-free-response" style="grid-column: 1 / -1;"> |
| <span class="shadcn-multiselect-label">{escape_html_content(instruction)}</span> |
| <input class="{free_response_identifiers['schema']} shadcn-multiselect-free-input annotation-input" |
| type="text" |
| id="{free_response_identifiers['id']}" |
| name="{free_response_identifiers['name']}" |
| schema="{free_response_identifiers['schema']}" |
| label_name="{free_response_identifiers['label_name']}"> |
| </div> |
| """ |
|
|