Source code for streamlit_rich_message_history.components

"""
Component handling for the streamlit_rich_message_history package.

This module defines the core MessageComponent class that detects, processes, and
renders different types of content in a Streamlit application.
"""

import traceback
from typing import Any, Optional, Union

import matplotlib.pyplot as plt
import pandas as pd
import plotly.graph_objects as go
import streamlit as st

from .enums import ComponentRegistry, ComponentType


[docs] class MessageComponent: """ Base class for all message components with automatic type detection. This class handles the automatic detection, proper rendering, and error handling for different types of content within a message in a Streamlit application. Attributes: content: The actual content to be displayed component_type: The type of component (automatically detected if not specified) title: Optional title for the component description: Optional description text for the component expanded: Whether expandable sections should be expanded by default kwargs: Additional keyword arguments for rendering """
[docs] def __init__( self, content: Any, component_type: Optional[ComponentType] = None, title: Optional[str] = None, description: Optional[str] = None, expanded: bool = False, **kwargs, ): """ Initialize a new message component. Args: content: The content to be displayed component_type: Manually specify the component type (auto-detected if None) title: Optional title for the component (creates an expander if provided) description: Optional description text for the component expanded: Whether expandable sections should be expanded by default **kwargs: Additional keyword arguments that control rendering behavior Special flags include: - is_error: Treat string content as an error message - is_code: Treat string content as code with syntax highlighting - language: The programming language for code highlighting - is_metric: Treat numeric content as a metric - is_table: Treat content as a static table - is_json: Treat dictionaries or lists as JSON data - is_html: Treat string content as HTML """ self.content = content self.kwargs = kwargs if component_type is None: component_type = self._detect_component_type(content) self.component_type = component_type self.title = title self.description = description self.expanded = expanded
def _detect_component_type(self, content: Any) -> ComponentType: """ Detect the appropriate component type based on content. This method uses a combination of registered custom detectors and built-in type detection logic to determine the most appropriate component type for the given content. Args: content: The content to detect the type for Returns: ComponentType: The detected component type """ # First try custom detectors for comp_type in ComponentRegistry._type_detectors: detector = ComponentRegistry.get_detector(comp_type) if detector and detector(content, self.kwargs): return comp_type # Then do built-in detection logic if isinstance(content, (list, tuple)) and not self.kwargs.get( "is_table", False ): return ( ComponentType.LIST if isinstance(content, list) else ComponentType.TUPLE ) elif isinstance(content, dict) and not self.kwargs.get("is_json", False): return ComponentType.DICT if isinstance(content, str): if self.kwargs.get("is_error", False): return ComponentType.ERROR elif self.kwargs.get("is_code", False): return ComponentType.CODE elif self.kwargs.get("is_html", False): return ComponentType.HTML else: return ComponentType.TEXT elif isinstance(content, pd.DataFrame): return ComponentType.DATAFRAME elif isinstance(content, pd.Series): return ComponentType.SERIES elif isinstance(content, plt.Figure): return ComponentType.MATPLOTLIB_FIGURE elif isinstance(content, go.Figure) or ( isinstance(content, dict) and isinstance(getattr(content, "data", None), (list, tuple)) ): return ComponentType.PLOTLY_FIGURE elif isinstance(content, (int, float)) and not self.kwargs.get( "is_metric", False ): return ComponentType.NUMBER elif self.kwargs.get("is_metric", False): return ComponentType.METRIC elif self.kwargs.get("is_table", False): return ComponentType.TABLE elif isinstance(content, (dict, list)) and self.kwargs.get("is_json", False): return ComponentType.JSON else: return ComponentType.TEXT
[docs] def render(self): """ Render the component with appropriate context. If a title is provided, the component is wrapped in an expander. If a description is provided, it's shown before the content. """ if self.title: with st.expander(self.title, expanded=self.expanded): if self.description: st.markdown(self.description) self._render_content() else: if self.description: st.markdown(self.description) self._render_content()
def _render_content(self): """ Render the component based on its detected type. This method handles the rendering of all built-in component types and delegates to custom renderers for custom component types. It also includes error handling to prevent component rendering errors from breaking the entire application. """ try: # First check if there's a custom renderer custom_renderer = ComponentRegistry.get_renderer(self.component_type) if custom_renderer: custom_renderer(self.content, self.kwargs) return # Standard component rendering if ( self.component_type == ComponentType.LIST or self.component_type == ComponentType.TUPLE ): for idx, item in enumerate(self.content): self._render_collection_item(item, idx) elif self.component_type == ComponentType.DICT: for key, value in self.content.items(): self._render_collection_item(value, key) else: if self.component_type == ComponentType.TEXT: st.markdown(self.content) elif self.component_type == ComponentType.ERROR: st.error(self.content) elif self.component_type == ComponentType.CODE: language = self.kwargs.get("language", "python") st.code(self.content, language=language) elif self.component_type == ComponentType.DATAFRAME: use_container_width = self.kwargs.get("use_container_width", True) height = self.kwargs.get("height", None) st.dataframe( self.content, use_container_width=use_container_width, height=height, ) elif self.component_type == ComponentType.SERIES: st.dataframe(self.content.to_frame()) elif self.component_type == ComponentType.MATPLOTLIB_FIGURE: st.pyplot(self.content) elif self.component_type == ComponentType.PLOTLY_FIGURE: use_container_width = self.kwargs.get("use_container_width", True) height = self.kwargs.get("height", None) st.plotly_chart( self.content, use_container_width=use_container_width, height=height, ) elif self.component_type == ComponentType.NUMBER: format_str = self.kwargs.get("format", None) if format_str: st.write( f"{self.title or 'Result'}: {format_str.format(self.content)}" ) else: st.write(f"{self.title or 'Result'}: {self.content}") elif self.component_type == ComponentType.METRIC: delta = self.kwargs.get("delta", None) delta_color = self.kwargs.get("delta_color", "normal") st.metric( label=self.title or "Metric", value=self.content, delta=delta, delta_color=delta_color, ) elif self.component_type == ComponentType.TABLE: st.table(self.content) elif self.component_type == ComponentType.JSON: st.json(self.content) elif self.component_type == ComponentType.HTML: height = self.kwargs.get("height", None) scrolling = self.kwargs.get("scrolling", False) st.html(self.content, height=height, scrolling=scrolling) else: st.write(str(self.content)) except Exception as e: error_message = f"Error rendering component of type {self.component_type.value}: {str(e)}" stack_trace = traceback.format_exc() st.error(error_message) with st.expander("Stack Trace", expanded=False): st.code(stack_trace, language="python") # Try to show the original content as simple text if possible with st.expander("Component Content (Debug View)", expanded=False): try: if hasattr(self.content, "__repr__"): st.code(repr(self.content), language="python") else: st.code(str(self.content), language="python") except Exception as e: st.error(f"Unable to display component content: {e}") def _render_collection_item( self, item: Any, index: Optional[Union[int, str]] = None ): """ Render a single item from a collection. Args: item: The item to render index: Optional index or key for error reporting """ try: # Create a new MessageComponent for the item item_component = MessageComponent(item) # Render the item item_component._render_content() except Exception as e: if isinstance(index, (int, str)): index_str = f" at index/key '{index}'" else: index_str = "" error_message = f"Error rendering collection item{index_str}: {str(e)}" st.error(error_message) with st.expander("Item Debug View", expanded=False): try: st.code(repr(item), language="python") except Exception as e: st.error(f"Unable to display item content: {e}")