heyllo_ai_chatbot 0.0.3+1 copy "heyllo_ai_chatbot: ^0.0.3+1" to clipboard
heyllo_ai_chatbot: ^0.0.3+1 copied to clipboard

An AI powered Flutter chatbot plugin for integrating into your flutter app.

example/lib/main.dart

// example/lib/main.dart
import 'package:flutter/material.dart';
// Assuming your package name is 'heyllo_ai_chatbot' based on the import
// Adjust if your package name is different
import 'package:heyllo_ai_chatbot/chat_plugin.dart';

// Assuming a ColorPicker widget exists (like the one provided in the original example)
// If not, you'll need to add a color picker package like flutter_colorpicker
// For simplicity, I'll keep the ColorPicker class from your example below.

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Chat Plugin Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const AdvancedChatDemo(),
    );
  }
}

class AdvancedChatDemo extends StatefulWidget {
  const AdvancedChatDemo({super.key});

  @override
  State<AdvancedChatDemo> createState() => _AdvancedChatDemoState();
}

class _AdvancedChatDemoState extends State<AdvancedChatDemo> {
  // --- Configuration ---
  static const domain = 'https://heyllo.co'; // Replace if needed
  static const chatbotId =
      'u12n4siq9ragdsftbpzgtktj'; // Replace with your actual chatbot ID

  // --- State Variables for Theming and Functionality ---
  Color _userBubbleColor = Colors.blue;
  Color _botBubbleColor = const Color(0xFFE1E1E1);
  Color _userTextColor = Colors.white;
  Color _botTextColor = Colors.black;
  double _bubbleRadius = 16.0;
  bool _showTimestamps = true; // Still controlled via ChatWidget param
  bool _showCitations = false; // New: Control citation visibility
  bool _isEnabled = true; // New: Control widget enable/disable state
  bool _usePresets = false;
  String _inputPlaceholder = 'Type a message...';

  // --- Theme Presets ---
  final Map<String, ChatTheme> _presets = {
    'Default': const ChatTheme(), // Uses defaults derived from app theme
    'Dark Mode': const ChatTheme(
      userBubbleColor: Colors.indigo,
      botBubbleColor: Color(0xFF303030), // Slightly lighter dark bubble
      userTextStyle: TextStyle(color: Colors.white),
      botTextStyle: TextStyle(color: Colors.white),
      backgroundColor: Color(0xFF121212),
      loadingIndicatorColor: Colors.white70,
      sendButtonColor: Colors.indigoAccent,
      sendButtonDisabledColor: Colors.grey,
    ),
    'Bubbly': ChatTheme(
      userBubbleColor: Colors.pinkAccent,
      botBubbleColor: Colors.purple.shade50,
      userTextStyle: const TextStyle(color: Colors.white),
      botTextStyle: const TextStyle(color: Colors.deepPurple),
      userBubbleBorderRadius: BorderRadius.circular(24),
      botBubbleBorderRadius: BorderRadius.circular(24),
      sendButtonColor: Colors.pinkAccent,
    ),
    'Professional': ChatTheme(
      userBubbleColor: Colors.blueGrey.shade700,
      botBubbleColor: Colors.blueGrey.shade50,
      userTextStyle: const TextStyle(color: Colors.white),
      botTextStyle: TextStyle(color: Colors.blueGrey.shade900),
      userBubbleBorderRadius: BorderRadius.circular(8),
      botBubbleBorderRadius: BorderRadius.circular(8),
      sendButtonColor: Colors.blueGrey.shade700,
      inputDecoration: InputDecoration(
          hintText: 'Enter your message...',
          filled: true,
          fillColor: Colors.white, // Ensure input contrasts with background
          contentPadding:
              const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: BorderSide(color: Colors.blueGrey.shade200),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: BorderSide(color: Colors.blueGrey.shade700, width: 1.5),
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: BorderSide(color: Colors.blueGrey.shade200),
          )),
      backgroundColor:
          const Color(0xFFF5F5F5), // Light background for pro theme
    ),
    'Minimalist': const ChatTheme(
      userBubbleColor: Colors.black,
      botBubbleColor: Color(0xFFF0F0F0),
      userTextStyle: TextStyle(color: Colors.white),
      botTextStyle: TextStyle(color: Colors.black),
      sendButtonColor: Colors.black,
      inputDecoration: InputDecoration(
        hintText: 'Message',
        border: UnderlineInputBorder(),
        focusedBorder:
            UnderlineInputBorder(borderSide: BorderSide(color: Colors.black)),
        enabledBorder:
            UnderlineInputBorder(borderSide: BorderSide(color: Colors.grey)),
        contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 12),
      ),
      userBubbleBorderRadius: BorderRadius.zero, // Square corners
      botBubbleBorderRadius: BorderRadius.zero,
    ),
  };

  String _selectedPreset = 'Default';

  @override
  Widget build(BuildContext context) {
    // Build current theme based on customization options
    // Note: If _usePresets is true, it completely overrides custom settings.
    final ChatTheme currentTheme = _usePresets
        ? _presets[_selectedPreset]!
        : ChatTheme(
            // Custom theme settings
            userBubbleColor: _userBubbleColor,
            botBubbleColor: _botBubbleColor,
            userTextStyle: TextStyle(
                color: _userTextColor, fontSize: 14), // Example font size
            botTextStyle: TextStyle(color: _botTextColor, fontSize: 14),
            userBubbleBorderRadius: BorderRadius.circular(_bubbleRadius),
            botBubbleBorderRadius: BorderRadius.circular(_bubbleRadius),
            inputDecoration: InputDecoration(
              // Example basic input decoration
              hintText: _inputPlaceholder,
              border: const OutlineInputBorder(), // Basic border
              contentPadding:
                  const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
            ),
            // Ensure other theme properties like button colors fallback nicely if not set
            // sendButtonColor: ...,
            // backgroundColor: ...,
          );

    return Scaffold(
      appBar: AppBar(
        title: const Text('Chat Plugin Demo'),
        actions: [
          // Toggle Enable/Disable Button
          IconButton(
            icon: Icon(_isEnabled ? Icons.toggle_on : Icons.toggle_off,
                color: _isEnabled ? Colors.green : Colors.grey),
            tooltip: _isEnabled ? 'Disable Chat' : 'Enable Chat',
            onPressed: () {
              setState(() {
                _isEnabled = !_isEnabled;
                print("Chat Enabled: $_isEnabled");
              });
            },
          ),
          // Settings Button
          IconButton(
            icon: const Icon(Icons.settings),
            tooltip: 'Edit Theme',
            onPressed: () {
              _showThemeEditor();
            },
          ),
        ],
      ),
      body: Column(
        children: [
          // Theme selector chips (only shown when presets are active)
          if (_usePresets)
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Row(
                children: _presets.keys.map((String presetName) {
                  return Padding(
                    padding: const EdgeInsets.only(right: 8),
                    child: ChoiceChip(
                      label: Text(presetName),
                      selected: _selectedPreset == presetName,
                      onSelected: (bool selected) {
                        if (selected) {
                          setState(() {
                            _selectedPreset = presetName;
                          });
                        }
                      },
                    ),
                  );
                }).toList(),
              ),
            ),

          // Chat widget Area
          Expanded(
            // Use a key that changes when config changes force re-initialization
            child: ChatWidget(
              key: ValueKey(
                  '$domain/$chatbotId/$_isEnabled'), // Change key to force rebuild on critical changes
              domain: domain,
              chatbotId: chatbotId,
              theme: currentTheme, // Pass the calculated theme
              isEnabled: _isEnabled, // Pass the enable/disable state
              showTimestamps: _showTimestamps, // Pass timestamp visibility
              showCitations: _showCitations, // Pass citation visibility
              inputPlaceholder: _inputPlaceholder, // Pass placeholder override

              // Initial messages need the 'type' specified
              initialMessages: [
                ChatMessage(
                  message: 'Hello! How can I help you today?',
                  isUser: false,
                  type: 'content', // Explicitly set type
                  timestamp:
                      DateTime.now().subtract(const Duration(minutes: 5)),
                ),
                // Example error message (if needed)
                // ChatMessage(
                //   message: 'Example initial error message.',
                //   isUser: false,
                //   type: 'error',
                //   timestamp: DateTime.now().subtract(const Duration(minutes: 4)),
                // ),
              ],

              // --- Callbacks ---
              onMessageSent: (message) {
                print("Message Sent: $message");
              },
              // Optional: Use specific callbacks if needed, otherwise UI updates automatically
              onResponseReceived: (response) {
                print("Final Response Content Received: $response");
              },
              onCitationsReceived: (citations) {
                print("Citations Received: ${citations.length}");
                // You could potentially display these outside the chat bubble if desired
              },
              onThreadIdReceived: (threadId) {
                print("Thread ID Received: $threadId");
                // Store this ID elsewhere if needed for other API calls
              },
              onError: (error) {
                print("Chat Error: $error");
                // Show error to user if appropriate
                if (mounted) {
                  // Check if widget is still in tree
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text('Chat Error: $error'),
                      backgroundColor: Colors.redAccent,
                    ),
                  );
                }
              },
            ),
          ),
        ],
      ),
    );
  }

  // --- Theme Editor Modal ---
  void _showThemeEditor() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true, // Allows taller bottom sheet
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (context) {
        // Use StatefulBuilder to manage state within the bottom sheet independently
        return StatefulBuilder(
          builder: (context, setModalState) {
            return Container(
              constraints: BoxConstraints(
                  // Set max height
                  maxHeight: MediaQuery.of(context).size.height * 0.85),
              padding: const EdgeInsets.all(20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize:
                    MainAxisSize.min, // Take only needed vertical space
                children: [
                  // Header
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      const Text(
                        'Theme Editor',
                        style: TextStyle(
                            fontSize: 20, fontWeight: FontWeight.bold),
                      ),
                      IconButton(
                          onPressed: () => Navigator.pop(context),
                          icon: const Icon(Icons.close))
                    ],
                  ),
                  const SizedBox(height: 16),

                  // Toggle between presets and custom
                  Row(
                    children: [
                      const Text('Use preset themes:'),
                      const Spacer(),
                      Switch(
                        value: _usePresets,
                        onChanged: (value) {
                          // Update modal state AND main page state
                          setModalState(() => _usePresets = value);
                          setState(() => _usePresets = value);
                        },
                      ),
                    ],
                  ),
                  const Divider(height: 20),

                  // Content area (either presets or custom editor)
                  Expanded(
                    child: _usePresets
                        ? _buildPresetSelector(setModalState)
                        : _buildCustomThemeEditor(setModalState),
                  ),

                  // Apply button (optional, as changes apply live)
                  // SizedBox(
                  //   width: double.infinity,
                  //   child: ElevatedButton(
                  //     onPressed: () => Navigator.pop(context),
                  //     child: const Text('Close Editor'),
                  //   ),
                  // ),
                ],
              ),
            );
          },
        );
      },
    );
  }

  // --- Preset Selector (Inside Modal) ---
  Widget _buildPresetSelector(StateSetter setModalState) {
    return ListView(
      shrinkWrap: true,
      children: _presets.entries.map((entry) {
        final String name = entry.key;
        final ChatTheme theme = entry.value;

        // Use default colors if preset doesn't specify them
        Color userColor = theme.userBubbleColor ?? Colors.grey;
        Color botColor = theme.botBubbleColor ?? Colors.grey[300]!;

        return Card(
          elevation: _selectedPreset == name ? 4 : 1, // Highlight selected
          margin: const EdgeInsets.only(bottom: 10),
          child: RadioListTile<String>(
            title: Text(name),
            value: name,
            groupValue: _selectedPreset,
            secondary: Row(
              // Show color swatches
              mainAxisSize: MainAxisSize.min,
              children: [
                CircleAvatar(radius: 10, backgroundColor: userColor),
                const SizedBox(width: 4),
                CircleAvatar(radius: 10, backgroundColor: botColor),
              ],
            ),
            onChanged: (String? value) {
              if (value != null) {
                // Update modal state AND main page state
                setModalState(() => _selectedPreset = value);
                setState(() => _selectedPreset = value);
              }
            },
          ),
        );
      }).toList(),
    );
  }

  // --- Custom Theme Editor (Inside Modal) ---
  Widget _buildCustomThemeEditor(StateSetter setModalState) {
    // Helper function for color picker rows
    Widget buildColorRow(
        String label, Color currentColor, Function(Color) onColorChanged) {
      return Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('$label Color:'),
          GestureDetector(
            onTap: () async {
              final Color? pickedColor =
                  await showColorPicker(context, currentColor);
              if (pickedColor != null) {
                onColorChanged(pickedColor); // Updates state via callback
              }
            },
            child: Container(
              width: 36,
              height: 36,
              decoration: BoxDecoration(
                color: currentColor,
                shape: BoxShape.circle,
                border: Border.all(color: Theme.of(context).dividerColor),
              ),
            ),
          ),
        ],
      );
    }

    return SingleChildScrollView(
      // Make editor scrollable
      child: Padding(
        padding: EdgeInsets.only(
            bottom: MediaQuery.of(context)
                .viewInsets
                .bottom), // Adjust for keyboard
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Bubbles', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 12),
            buildColorRow("User Bubble", _userBubbleColor, (color) {
              setModalState(() => _userBubbleColor = color);
              setState(() => _userBubbleColor = color);
            }),
            const SizedBox(height: 8),
            buildColorRow("User Text", _userTextColor, (color) {
              setModalState(() => _userTextColor = color);
              setState(() => _userTextColor = color);
            }),
            const SizedBox(height: 16),
            buildColorRow("Bot Bubble", _botBubbleColor, (color) {
              setModalState(() => _botBubbleColor = color);
              setState(() => _botBubbleColor = color);
            }),
            const SizedBox(height: 8),
            buildColorRow("Bot Text", _botTextColor, (color) {
              setModalState(() => _botTextColor = color);
              setState(() => _botTextColor = color);
            }),

            const SizedBox(height: 20),
            Text('Appearance', style: Theme.of(context).textTheme.titleMedium),
            // Bubble corner radius
            Row(
              children: [
                const Text('Bubble Radius:'),
                Expanded(
                  child: Slider(
                    value: _bubbleRadius,
                    min: 0,
                    max: 30,
                    divisions: 30,
                    label: _bubbleRadius.round().toString(),
                    onChanged: (value) {
                      setModalState(() => _bubbleRadius = value);
                      setState(() => _bubbleRadius = value);
                    },
                  ),
                ),
                Text('${_bubbleRadius.toInt()}px'),
              ],
            ),

            const SizedBox(height: 20),
            Text('Input Field', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 8),
            TextFormField(
              initialValue: _inputPlaceholder,
              decoration: const InputDecoration(
                labelText: 'Input Placeholder Text',
                border: OutlineInputBorder(),
                hintText: 'e.g., Type your question',
              ),
              onChanged: (value) {
                // No need for setModalState if changes apply live to main page state
                setState(() => _inputPlaceholder = value);
              },
            ),

            const SizedBox(height: 20),
            Text('Options', style: Theme.of(context).textTheme.titleMedium),
            SwitchListTile(
              title: const Text('Show Timestamps'),
              value: _showTimestamps,
              onChanged: (value) {
                setModalState(() => _showTimestamps = value);
                setState(() => _showTimestamps = value);
              },
              dense: true,
            ),
            SwitchListTile(
              title: const Text('Show Citations'),
              value: _showCitations,
              onChanged: (value) {
                setModalState(() => _showCitations = value);
                setState(() => _showCitations = value);
              },
              dense: true,
            ),
            // Add other options like background color picker if needed
          ],
        ),
      ),
    );
  }

  // --- Simple Color Picker Dialog (Keep from original) ---
  Future<Color?> showColorPicker(
      BuildContext context, Color initialColor) async {
    Color pickedColor = initialColor;
    // Using showDialog for the color picker
    return await showDialog<Color>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Pick a color'),
          content: SingleChildScrollView(
            // Assuming ColorPicker widget exists and works like flutter_colorpicker's basic version
            child: ColorPicker(
              // Replace with actual ColorPicker widget if different
              pickerColor: initialColor,
              onColorChanged: (color) {
                pickedColor = color; // Update local variable inside dialog
              },
            ),
          ),
          actions: <Widget>[
            TextButton(
              child: const Text('Cancel'),
              onPressed: () =>
                  Navigator.of(context).pop(null), // Return null on cancel
            ),
            TextButton(
              child: const Text('Select'),
              onPressed: () => Navigator.of(context)
                  .pop(pickedColor), // Return the picked color
            ),
          ],
        );
      },
    );
  }
}

// --- Simple Color Picker Widget (Keep from original example) ---
/// Simple color picker for demonstration purposes.
/// In a real app, consider using a package like flutter_colorpicker.
class ColorPicker extends StatefulWidget {
  final Color pickerColor;
  final ValueChanged<Color> onColorChanged;

  const ColorPicker({
    super.key,
    required this.pickerColor,
    required this.onColorChanged,
  });

  @override
  State<ColorPicker> createState() => _ColorPickerState();
}

class _ColorPickerState extends State<ColorPicker> {
  late HSVColor _currentHsvColor;

  @override
  void initState() {
    super.initState();
    _currentHsvColor = HSVColor.fromColor(widget.pickerColor);
  }

  // Update when the initial color changes externally
  @override
  void didUpdateWidget(ColorPicker oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.pickerColor != oldWidget.pickerColor) {
      _currentHsvColor = HSVColor.fromColor(widget.pickerColor);
    }
  }

  @override
  Widget build(BuildContext context) {
    Widget slider(
        String label, double value, double max, Function(double) onChanged) {
      return Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(label),
          Slider(
            value: value,
            min: 0,
            max: max,
            onChanged: onChanged,
          ),
        ],
      );
    }

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // Hue slider
        slider('Hue', _currentHsvColor.hue, 360, (value) {
          setState(() {
            _currentHsvColor = _currentHsvColor.withHue(value);
            widget.onColorChanged(_currentHsvColor.toColor());
          });
        }),

        // Saturation slider
        slider('Saturation', _currentHsvColor.saturation, 1, (value) {
          setState(() {
            _currentHsvColor = _currentHsvColor.withSaturation(value);
            widget.onColorChanged(_currentHsvColor.toColor());
          });
        }),

        // Value slider
        slider('Value (Brightness)', _currentHsvColor.value, 1, (value) {
          setState(() {
            _currentHsvColor = _currentHsvColor.withValue(value);
            widget.onColorChanged(_currentHsvColor.toColor());
          });
        }),

        // Alpha slider (optional)
        // slider('Alpha', _currentHsvColor.alpha, 1, (value) { ... }),

        // Preview
        const SizedBox(height: 10),
        Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            color: _currentHsvColor.toColor(),
            shape: BoxShape.circle,
            border: Border.all(color: Theme.of(context).dividerColor),
          ),
        ),
      ],
    );
  }
}