flutter_ai_chat 0.1.0 copy "flutter_ai_chat: ^0.1.0" to clipboard
flutter_ai_chat: ^0.1.0 copied to clipboard

AI chat interface with streaming responses and conversation management

example/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_ai_chat/flutter_ai_chat.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:provider/provider.dart';

/// Example application showcasing all features of Flutter AI Chat.
///
/// This example demonstrates:
/// - AI Chat Interface with streaming responses
/// - Conversation management (create, select, delete)
/// - Light/dark theme support
/// - Responsive design
/// - .env file support for API keys
/// - Error handling and loading states
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Load environment variables from .env file
  try {
    await dotenv.load(fileName: '.env');
  } catch (e) {
    // .env file not found or other error - user can set API key manually in the app
    debugPrint('Warning: Could not load .env file: $e');
    debugPrint('You can set your API key in the app settings.');
  }

  runApp(const ExampleApp());
}

/// Main example application widget with theme management.
class ExampleApp extends StatefulWidget {
  const ExampleApp({super.key});

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  ThemeMode _themeMode = ThemeMode.system;

  void _toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.light
          ? ThemeMode.dark
          : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) => MultiProvider(
    providers: [
      ChangeNotifierProvider(
        create: (_) {
          final provider = ChatProvider();
          // Set API key from environment if available
          try {
            final apiKey = dotenv.env['OPENAI_API_KEY'];
            if (apiKey != null && apiKey.isNotEmpty) {
              provider.setApiKey(apiKey);
            }
          } catch (e) {
            // dotenv not initialized or .env file not found
            debugPrint('Could not read API key from .env: $e');
          }
          return provider;
        },
      ),
    ],
    child: MaterialApp(
      title: 'Flutter AI Chat - Example',
      theme: AppTheme.lightTheme,
      darkTheme: AppTheme.darkTheme,
      themeMode: _themeMode,
      home: ExampleChatScreen(onThemeToggle: _toggleTheme),
      debugShowCheckedModeBanner: false,
    ),
  );
}

/// Example chat screen showcasing all features.
///
/// Features demonstrated:
/// - Full chat interface with streaming responses
/// - Conversation sidebar
/// - Theme toggle
/// - API key configuration
/// - Responsive layout
class ExampleChatScreen extends StatefulWidget {
  final VoidCallback onThemeToggle;

  const ExampleChatScreen({super.key, required this.onThemeToggle});

  @override
  State<ExampleChatScreen> createState() => _ExampleChatScreenState();
}

class _ExampleChatScreenState extends State<ExampleChatScreen> {
  final TextEditingController _apiKeyController = TextEditingController();

  @override
  void dispose() {
    _apiKeyController.dispose();
    super.dispose();
  }

  /// Shows dialog to configure API key.
  void _showApiKeyConfiguration() {
    final chatProvider = context.read<ChatProvider>();
    _apiKeyController.clear();

    showDialog<void>(
      context: context,
      builder: (final BuildContext context) => AlertDialog(
        title: const Text('Configure API Key'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Enter your OpenAI API key to enable AI chat functionality.',
              style: TextStyle(fontSize: 14),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _apiKeyController,
              decoration: const InputDecoration(
                labelText: 'OpenAI API Key',
                hintText: 'sk-...',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              autofocus: true,
            ),
            const SizedBox(height: 8),
            Text(
              'You can also set OPENAI_API_KEY in a .env file.',
              style: TextStyle(
                fontSize: 12,
                color: Theme.of(
                  context,
                ).colorScheme.onSurface.withValues(alpha: 0.6),
              ),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              if (_apiKeyController.text.trim().isNotEmpty) {
                chatProvider.setApiKey(_apiKeyController.text.trim());
                Navigator.pop(context);
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text('API key configured successfully!'),
                    backgroundColor: Colors.green,
                  ),
                );
              }
            },
            child: const Text('Save'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    body: Consumer<ChatProvider>(
      builder: (context, chatProvider, child) => Row(
        children: [
          // Sidebar - visible on larger screens
          if (MediaQuery.of(context).size.width > 600)
            SizedBox(
              width: 300,
              child: SidebarWidget(
                conversations: chatProvider.conversations,
                currentConversation: chatProvider.currentConversation,
                onConversationSelected: (final String conversationId) {
                  chatProvider.selectConversation(conversationId);
                },
                onNewConversation: () {
                  chatProvider.createNewConversation();
                },
                onConversationDeleted: (final String conversationId) {
                  chatProvider.deleteConversation(conversationId);
                },
              ),
            ),

          // Main chat area
          Expanded(
            child: Column(
              children: [
                // App bar with theme toggle and settings
                AppBar(
                  title: Text(
                    chatProvider.currentConversation?.title ?? 'New Chat',
                    style: const TextStyle(fontSize: 18),
                  ),
                  actions: [
                    // Theme toggle
                    IconButton(
                      icon: Icon(
                        Theme.of(context).brightness == Brightness.dark
                            ? Icons.light_mode
                            : Icons.dark_mode,
                      ),
                      onPressed: widget.onThemeToggle,
                      tooltip: 'Toggle theme',
                    ),
                    // API key configuration
                    IconButton(
                      icon: const Icon(Icons.settings),
                      onPressed: _showApiKeyConfiguration,
                      tooltip: 'Configure API Key',
                    ),
                    // Menu button for mobile
                    if (MediaQuery.of(context).size.width <= 600)
                      IconButton(
                        icon: const Icon(Icons.menu),
                        onPressed: () {
                          Scaffold.of(context).openDrawer();
                        },
                      ),
                    // New conversation button
                    IconButton(
                      icon: const Icon(Icons.add),
                      onPressed: () {
                        chatProvider.createNewConversation();
                      },
                      tooltip: 'New Conversation',
                    ),
                  ],
                ),

                // Error message display
                if (chatProvider.error.isNotEmpty)
                  Container(
                    width: double.infinity,
                    padding: const EdgeInsets.all(12),
                    color: Theme.of(context).colorScheme.errorContainer,
                    child: Row(
                      children: [
                        Icon(
                          Icons.error_outline,
                          color: Theme.of(context).colorScheme.onErrorContainer,
                        ),
                        const SizedBox(width: 8),
                        Expanded(
                          child: Text(
                            chatProvider.error,
                            style: TextStyle(
                              color: Theme.of(
                                context,
                              ).colorScheme.onErrorContainer,
                            ),
                          ),
                        ),
                        IconButton(
                          icon: const Icon(Icons.close),
                          onPressed: () => chatProvider.clearError(),
                          color: Theme.of(context).colorScheme.onErrorContainer,
                        ),
                      ],
                    ),
                  ),

                // Chat messages area
                Expanded(
                  child: chatProvider.currentConversation == null
                      ? _buildWelcomeScreen(context, chatProvider)
                      : _buildChatMessages(context, chatProvider),
                ),

                // Chat input
                if (chatProvider.currentConversation != null)
                  ChatInputWidget(
                    onMessageSent: (final String message) {
                      chatProvider.sendMessage(message);
                    },
                    isLoading: chatProvider.isLoading,
                  ),
              ],
            ),
          ),
        ],
      ),
    ),
    // Drawer for mobile
    drawer: MediaQuery.of(context).size.width <= 600
        ? Drawer(
            child: Consumer<ChatProvider>(
              builder: (context, chatProvider, child) => SidebarWidget(
                conversations: chatProvider.conversations,
                currentConversation: chatProvider.currentConversation,
                onConversationSelected: (final String conversationId) {
                  chatProvider.selectConversation(conversationId);
                  Navigator.pop(context);
                },
                onNewConversation: () {
                  chatProvider.createNewConversation();
                },
                onConversationDeleted: (final String conversationId) {
                  chatProvider.deleteConversation(conversationId);
                },
              ),
            ),
          )
        : null,
  );

  /// Builds the welcome screen shown when no conversation is selected.
  Widget _buildWelcomeScreen(
    final BuildContext context,
    final ChatProvider chatProvider,
  ) => Center(
    child: Padding(
      padding: const EdgeInsets.all(24.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.chat_bubble_outline,
            size: 80,
            color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.6),
          ),
          const SizedBox(height: 24),
          Text(
            'Welcome to Flutter AI Chat',
            style: Theme.of(context).textTheme.headlineSmall?.copyWith(
              color: Theme.of(context).colorScheme.onSurface,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),
          Text(
            'Start a new conversation to begin chatting with AI',
            style: Theme.of(context).textTheme.bodyLarge?.copyWith(
              color: Theme.of(
                context,
              ).colorScheme.onSurface.withValues(alpha: 0.7),
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 32),
          ElevatedButton.icon(
            onPressed: () {
              chatProvider.createNewConversation();
            },
            icon: const Icon(Icons.add),
            label: const Text('Start New Chat'),
            style: ElevatedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            ),
          ),
          const SizedBox(height: 16),
          OutlinedButton.icon(
            onPressed: _showApiKeyConfiguration,
            icon: const Icon(Icons.settings),
            label: const Text('Configure API Key'),
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            ),
          ),
        ],
      ),
    ),
  );

  /// Builds the chat messages list.
  Widget _buildChatMessages(
    final BuildContext context,
    final ChatProvider chatProvider,
  ) {
    final messages = chatProvider.currentConversation!.messages;

    if (messages.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.chat_bubble_outline,
              size: 64,
              color: Theme.of(
                context,
              ).colorScheme.onSurface.withValues(alpha: 0.5),
            ),
            const SizedBox(height: 16),
            Text(
              'No messages yet. Start the conversation!',
              style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                color: Theme.of(
                  context,
                ).colorScheme.onSurface.withValues(alpha: 0.7),
              ),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: messages.length,
      itemBuilder: (final BuildContext context, final int index) {
        final message = messages[index];
        return ChatMessageWidget(
          message: message,
          isLast: index == messages.length - 1,
        );
      },
    );
  }
}