""" Spreadsheet Display Component Renders tabular data with support for row-based or cell-based annotation. Usage: In instance_display config: fields: - key: data_table type: spreadsheet display_options: annotation_mode: row show_headers: true max_height: 400 """ from typing import Dict, Any, List, Optional import html import logging from .base import BaseDisplay logger = logging.getLogger(__name__) class SpreadsheetDisplay(BaseDisplay): """ Display type for tabular/spreadsheet data. Renders data as an HTML table with support for row-based or cell-based annotation modes. """ name = "spreadsheet" required_fields = ["key"] optional_fields = { "annotation_mode": "row", # "row", "cell", or "range" "show_headers": True, # Show column headers "max_height": 400, # Max container height "max_width": None, # Max container width "striped": True, # Alternating row colors "hoverable": True, # Highlight row on hover "sortable": False, # Enable column sorting "filterable": False, # Enable column filtering "selectable": True, # Enable row/cell selection "compact": False, # Compact table styling # New styling options "border_style": "default", # "default", "bordered", "minimal", "rounded", "none" "header_style": "default", # "default", "dark", "primary", "gradient", "light", "transparent" "custom_class": None, # Additional CSS classes for the table "custom_css": None, # Inline CSS styles for the table } description = "Spreadsheet/table display with row or cell annotation" supports_span_target = False # No .text-content wrapper; table data not compatible with span offsets # Valid values for style options VALID_BORDER_STYLES = ["default", "bordered", "minimal", "rounded", "none"] VALID_HEADER_STYLES = ["default", "dark", "primary", "gradient", "light", "transparent"] def validate_config(self, field_config: Dict[str, Any]) -> List[str]: """ Validate spreadsheet display configuration. Returns: List of validation error messages (empty if valid) """ errors = super().validate_config(field_config) options = field_config.get("display_options", {}) # Validate border_style border_style = options.get("border_style", "default") if border_style not in self.VALID_BORDER_STYLES: errors.append( f"Invalid border_style '{border_style}'. " f"Must be one of: {', '.join(self.VALID_BORDER_STYLES)}" ) # Validate header_style header_style = options.get("header_style", "default") if header_style not in self.VALID_HEADER_STYLES: errors.append( f"Invalid header_style '{header_style}'. " f"Must be one of: {', '.join(self.VALID_HEADER_STYLES)}" ) # Validate annotation_mode annotation_mode = options.get("annotation_mode", "row") valid_modes = ["row", "cell", "range"] if annotation_mode not in valid_modes: errors.append( f"Invalid annotation_mode '{annotation_mode}'. " f"Must be one of: {', '.join(valid_modes)}" ) return errors def render(self, field_config: Dict[str, Any], data: Any) -> str: """ Render spreadsheet data. Args: field_config: Display configuration data: Either a dict with extracted content, list of lists, or list of dicts Returns: HTML string for rendering """ options = self.get_display_options(field_config) field_key = field_config.get("key", "spreadsheet") # Handle different data formats if isinstance(data, dict): # Pre-extracted FormatOutput data if "rendered_html" in data: return self._wrap_content(data["rendered_html"], options, field_key) # Extract from metadata rows = data.get("rows", []) headers = data.get("headers", data.get("metadata", {}).get("headers", [])) elif isinstance(data, list): if data and isinstance(data[0], dict): # List of dictionaries headers = list(data[0].keys()) if data else [] rows = [[row.get(h, "") for h in headers] for row in data] else: # List of lists rows = data headers = [] else: return f'
| ') for col_idx, header in enumerate(headers): sortable_attr = 'data-sortable="true"' if options.get("sortable") else "" parts.append( f' | ' f'{html.escape(str(header))} | ' ) parts.append('
|---|---|
| ' f'' f' | ' ) for col_idx, cell in enumerate(row): cell_value = str(cell) if cell is not None else "" cell_classes = ["spreadsheet-cell"] if mode == "cell": cell_classes.append("selectable-cell") cell_ref = self._get_cell_ref(row_idx, col_idx) parts.append( f'' f'{html.escape(cell_value)}' f' | ' ) parts.append('