build method

  1. @override
Widget build(
  1. BuildContext context
)
override

Describes the part of the user interface represented by this widget.

The framework calls this method when this widget is inserted into the tree in a given BuildContext and when the dependencies of this widget change (e.g., an InheritedWidget referenced by this widget changes). This method can potentially be called in every frame and should not have any side effects beyond building a widget.

The framework replaces the subtree below this widget with the widget returned by this method, either by updating the existing subtree or by removing the subtree and inflating a new subtree, depending on whether the widget returned by this method can update the root of the existing subtree, as determined by calling Widget.canUpdate.

Typically implementations return a newly created constellation of widgets that are configured with information from this widget's constructor and from the given BuildContext.

The given BuildContext contains information about the location in the tree at which this widget is being built. For example, the context provides the set of inherited widgets for this location in the tree. A given widget might be built with multiple different BuildContext arguments over time if the widget is moved around the tree or if the widget is inserted into the tree in multiple places at once.

The implementation of this method must only depend on:

If a widget's build method is to depend on anything else, use a StatefulWidget instead.

See also:

  • StatelessWidget, which contains the discussion on performance considerations.

Implementation

@override
Widget build(BuildContext context) {


  final appTheme = Theme.of(context);
  final isUser = message.isUser;

  // Determine styles based on user/bot and error state
  final decoration = isError
      ? _getErrorBubbleDecoration(context, appTheme)
      : (isUser
          ? theme.getUserBubbleDecoration(appTheme)
          : theme.getBotBubbleDecoration(appTheme));

  final textStyle = isError
      ? _getErrorTextStyle(context, appTheme)
      : (isUser
          ? theme.getUserTextStyle(appTheme)
          : theme.getBotTextStyle(appTheme));

  // Default margins for the ALIGNED bubble (when no avatar is shown)
  final EdgeInsetsGeometry effectiveMargin = margin ??
      EdgeInsets.only(
        left: isUser ? 40 : 8, // Standard margin when no avatar
        right: isUser ? 8 : 40, // Standard margin when no avatar
        top: 4,
        bottom: 4,
      );

  // Default padding inside the bubble
  final effectivePadding =
      padding ?? const EdgeInsets.symmetric(vertical: 10, horizontal: 14);

  // --- Build citations widget ---
  Widget citationsWidget = const SizedBox.shrink(); // Empty by default
  final citations = message.citations;
  if (showCitations && citations != null && citations.isNotEmpty) {
    citationsWidget = Padding(
      padding: const EdgeInsets.only(top: 8.0), // Space above citations
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Optional "Sources:" title
          Padding(
            padding: const EdgeInsets.only(bottom: 4.0),
            child: Text(
              "Sources:",
              style: textStyle.copyWith(
                  fontSize: 11,
                  fontWeight: FontWeight.bold,
                  color: textStyle.color?.withAlpha(
                      (255 * 0.8).round()) // Slightly dimmer title
                  ),
            ),
          ),
          // List citations
          ...citations.map((citation) {
            final title = citation['title'] as String? ??
                citation['url'] as String? ??
                'Source'; // Use URL as title if title missing
            final url = citation['url'] as String?;
            bool isLink = url != null && url.isNotEmpty;

            return InkWell(
              onTap: isLink ? () => _launchUrl(url, context) : null,
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 2.0),
                child: Row(
                  // Use row for icon + text
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // Icon indicating it's a link or just a source
                    Icon(
                      isLink ? Icons.link : Icons.info_outline,
                      size: 12,
                      color: textStyle.color?.withAlpha((255 * 0.8).round()),
                    ),
                    const SizedBox(width: 4),
                    // Citation text (title)
                    Expanded(
                      child: Text(
                        title,
                        style: textStyle.copyWith(
                          fontSize: 11,
                          // Style link differently
                          color: isLink
                              ? (theme.sendButtonColor ??
                                  appTheme.colorScheme
                                      .primary) // Use theme accent or primary
                              : textStyle.color?.withAlpha(
                                  (255 * 0.7).round()), // Dim non-links
                          decoration: isLink
                              ? TextDecoration.underline
                              : TextDecoration.none,
                          decorationColor: isLink
                              ? (theme.sendButtonColor ??
                                  appTheme.colorScheme.primary)
                              : null,
                        ),
                        maxLines:
                            1, // Prevent long titles from wrapping excessively
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                  ],
                ),
              ),
            );
          }),
        ],
      ),
    );
  }
  // --- End citations widget ---

  // --- Build main bubble content ---
  Widget bubbleContentContainer = Container(
    // Renamed for clarity before InkWell wrap
    padding: effectivePadding,
    decoration: decoration,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start, // Align content left
      mainAxisSize: MainAxisSize.min, // Fit content height
      children: [
        // Sender name if provided and not user
        if (senderName != null && senderName!.isNotEmpty && !isUser)
          Padding(
            padding: const EdgeInsets.only(bottom: 4),
            child: Text(
              senderName!,
              style: textStyle.copyWith(
                fontWeight: FontWeight.bold,
                fontSize: 12,
                color: textStyle.color
                    ?.withAlpha((255 * 0.9).round()), // Slightly dimmer name
              ),
            ),
          ),

        // *** RENDER MARKDOWN FOR BOT, PLAIN TEXT FOR USER/ERROR ***
        if (message.message.isNotEmpty ||
            isError) // Show even if empty if it's an error message
          if (!isUser &&
              !isError) // Render Markdown only for non-user, non-error messages
            MarkdownBody(
              data: message.message, // Pass the markdown string here
              selectable: true, // Allow text selection
              styleSheet: MarkdownStyleSheet.fromTheme(appTheme).copyWith(
                // Base style on app theme
                p: textStyle, // Apply the bubble's base text style to paragraphs
                // You can customize other elements like headings, links, code blocks etc.
                // h1: textStyle.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
                // a: TextStyle(color: theme.sendButtonColor ?? appTheme.colorScheme.primary, decoration: TextDecoration.underline),
                // code: TextStyle(backgroundColor: appTheme.colorScheme.surfaceVariant, fontFamily: 'monospace'),
              ),
              onTapLink: (text, href, title) {
                // Handle link taps
                if (href != null) {
                  _launchUrl(href, context);
                }
              },
              // Add other configurations like image builders if needed
              // imageBuilder: (uri, title, alt) => Image.network(uri.toString()),
            )
          else // For user messages or errors, use SelectableText
            SelectableText(message.message, style: textStyle),
        // *** END MARKDOWN RENDERING ***

        // Typing indicator OR Citations/Timestamp
        if (message.isWaiting)
          Padding(
            padding: const EdgeInsets.only(top: 8),
            // Use the imported BubbleTypingIndicator
            child: BubbleTypingIndicator(
              color: theme.loadingIndicatorColor ??
                  (isUser
                          ? appTheme.colorScheme.onPrimary
                          : appTheme.colorScheme.primary)
                      .withAlpha((255 * 0.7).round()),
              bubbleSize: 8,
              spacing: 4,
            ),
          )
        else ...[
          // Show these only if not waiting
          // Citations widget (built above)
          citationsWidget,

          // Timestamp if enabled and available
          if (showTimestamp && message.timestamp != null)
            Align(
              alignment: Alignment.bottomRight,
              child: Padding(
                // Adjust top padding based on whether citations are shown
                padding: EdgeInsets.only(
                    top: (citationsWidget is SizedBox &&
                            citationsWidget.height == 0)
                        ? 4
                        : 8),
                child: Text(
                  _formatTimestamp(message.timestamp!),
                  style: textStyle.copyWith(
                    fontSize: 10,
                    fontWeight:
                        FontWeight.w300, // Lighter weight for timestamp
                    color: textStyle.color
                        ?.withAlpha((255 * 0.6).round()), // Dimmer timestamp
                  ),
                ),
              ),
            ),
        ]
      ],
    ),
  );
  // --- End main bubble content container ---

  // --- Add InkWell for tap/long-press AFTER content is built ---
  // This makes the whole bubble area tappable, including padding
  Widget bubbleInteractive = Material(
    // Material needed for InkWell splash and borderRadius clipping
    color: Colors.transparent, // Make material transparent
    // ignore: unnecessary_type_check
    borderRadius: (decoration is BoxDecoration)
        ? (decoration.borderRadius
            as BorderRadius?) // Cast BorderRadiusGeometry? to BorderRadius?
        : BorderRadius.circular(16), // Fallback if needed
    clipBehavior: Clip.antiAlias, // Ensure InkWell splash stays within bounds
    child: InkWell(
      onTap: onTap,
      // Add default long press for copying if no custom one is provided
      onLongPress: onLongPress ??
          (isError || message.message.isEmpty
              ? null
              : () => _copyToClipboard(context)),
      // borderRadius applied to Material above
      child: bubbleContentContainer, // Wrap the content container
    ),
  );
  // --- End InkWell ---

  // --- Row for Avatar + Bubble ---
  if (showAvatar) {
    final effectiveAvatar = avatar ?? _defaultAvatar(isUser, appTheme);
    // Define specific padding for the Row when avatar is shown
    final double verticalPadding = (effectiveMargin is EdgeInsets)
        ? effectiveMargin.top // Use top/bottom from original margin
        : 4.0; // Default vertical padding
    final EdgeInsets avatarRowPadding = EdgeInsets.symmetric(
      horizontal: 8.0, // Standard horizontal padding when avatar is shown
      vertical: verticalPadding,
    );

    return Padding(
      padding: avatarRowPadding, // Use the calculated EdgeInsets
      child: Row(
        mainAxisAlignment:
            isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
        crossAxisAlignment:
            CrossAxisAlignment.end, // Align avatar bottom with bubble bottom
        children: isUser
            ? [
                Flexible(
                    child: bubbleInteractive), // Bubble takes available space
                const SizedBox(width: 8),
                effectiveAvatar, // Avatar on the right for user
              ]
            : [
                effectiveAvatar, // Avatar on the left for bot
                const SizedBox(width: 8),
                Flexible(child: bubbleInteractive),
              ],
      ),
    );
  }
  // --- End Avatar Row ---

  // --- Bubble only (no avatar) ---
  // Align the bubble itself left or right
  return Align(
    alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
    child: Container(
      // Constrain max width to prevent bubble taking full screen width
      constraints: BoxConstraints(
        maxWidth: MediaQuery.of(context).size.width * 0.75,
      ),
      margin:
          effectiveMargin, // Use the original margin (passed in or default)
      child: bubbleInteractive, // The InkWell-wrapped content
    ),
  );
  // --- End Bubble only ---
}