flutter_ai_chat 0.1.0
flutter_ai_chat: ^0.1.0 copied to clipboard
AI chat interface with streaming responses and conversation management
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,
);
},
);
}
}