Source code for klea_utils.ui.web.nicegui.widgets

"""
Reusable custom NiceGUI widgets for Klea web interfaces.

File: klea_utils/ui/web/nicegui/widgets.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

from nicegui import ui


[docs] class ChatBubble(ui.element): """A custom chat message bubble with built-in actions. Replaces ``ui.chat_message`` with full control over the layout. Each bubble has collapsible text content, a timestamp, a copy button, and an expand / collapse toggle -- all flowing naturally inside the bubble (no CSS hacks). Usage inside a ``@ui.refreshable``:: ChatBubble( text="Hello", stamp="12:00", is_user=True, collapsed=False, idx=0, on_expand=lambda: print("toggle"), on_copy=lambda: print("copy"), ) """ def __init__( self, text: str, stamp: str, is_user: bool, collapsed: bool, idx: int, on_expand=None, on_copy=None, ) -> None: """Build the bubble. :param text: Message text (rendered as HTML). :param stamp: Timestamp string. :param is_user: ``True`` for user messages, ``False`` for bot. :param collapsed: ``True`` if the text is collapsed to 4 lines. :param idx: Message index (used for expand/collapse tracking). :param on_expand: Callable with no args, fired on expand/collapse click. :param on_copy: Callable with no args, fired on copy click. """ super().__init__("div") bg = ( "bg-green-50 dark:bg-green-900" if is_user else "bg-blue-50 dark:bg-blue-900" ) corner = "rounded-br-sm" if is_user else "rounded-bl-sm" align = "items-end" if is_user else "items-start" text_align = "text-right" if is_user else "text-left" bubble_w = "max-w-[85%]" if is_user else "w-full" bottom_align = "justify-end" if is_user else "justify-start" self.classes(f"w-full flex flex-col {align}") with ( self, ui.element("div").classes( f"flex flex-col rounded-2xl {bg} {corner} p-3 gap-1 {bubble_w}" ), ): text_cls = ( f"{text_align} msg-collapsed" if collapsed else f"{text_align} msg-expanded" ) with ui.element("div").classes(text_cls): # 'alerts' extra renders GitHub-style ``> [!WARNING]`` blocks # (used for the fallback / best-effort warnings) as callouts. ui.markdown(text, extras=["fenced-code-blocks", "tables", "alerts"]) with ui.row().classes(f"flex flex-row {bottom_align} items-center gap-1"): ui.label(stamp).classes("text-xs text-grey-5") if on_copy: ui.button(icon="content_copy").props("flat dense round size=sm").on( "click", on_copy ) if on_expand: icon = "expand_less" if not collapsed else "expand_more" ui.button(icon=icon).props("flat dense round size=sm").on( "click", on_expand )