raptrai 0.1.0
raptrai: ^0.1.0 copied to clipboard
The opinionated AI UI + runtime toolkit for Flutter. Like shadcn/ui for AI apps. Multi-provider support (OpenAI, Anthropic, Google), beautiful components, and production-ready features.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:raptrai/raptrai.dart';
void main() {
// Configure analytics for debugging
RaptrAIAnalytics.configure(
onEvent: (event) {
debugPrint('[Analytics] ${event.name}: ${event.toJson()}');
},
enabled: true,
);
runApp(const RaptrAIExampleApp());
}
// =============================================================================
// MOCK PROVIDER - Simulates AI responses with streaming
// =============================================================================
class MockAIProvider extends RaptrAIProvider {
MockAIProvider({this.responseDelay = const Duration(milliseconds: 50)});
final Duration responseDelay;
bool _isCancelled = false;
static const _responses = [
"I'd be happy to help you with that! Let me think about this for a moment.\n\nBased on my analysis, here are some key points to consider:\n\n1. **First Point**: This is an important consideration that affects the overall outcome.\n\n2. **Second Point**: You might want to look into this aspect as well.\n\n3. **Third Point**: Finally, don't forget about this crucial element.\n\nIs there anything specific you'd like me to elaborate on?",
"That's a great question! Here's what I can tell you:\n\n```dart\n// Example code snippet\nclass Example {\n void doSomething() {\n print('Hello, World!');\n }\n}\n```\n\nThis demonstrates the basic structure you might use. Would you like me to explain any part in more detail?",
"Let me break this down for you:\n\n## Overview\nThis topic involves several interconnected concepts that work together.\n\n## Key Concepts\n- **Concept A**: The foundation of everything\n- **Concept B**: Builds upon A to add functionality\n- **Concept C**: The practical application\n\n## Conclusion\nUnderstanding these relationships is crucial for effective implementation.",
"I've analyzed your request and here's my response:\n\nThe solution involves a few steps:\n\n1. First, you'll need to set up the basic structure\n2. Then, implement the core logic\n3. Finally, add error handling and edge cases\n\nWould you like me to provide more specific guidance on any of these steps?",
"Thanks for asking! RaptrAI is a comprehensive Flutter framework for building AI-powered applications. It includes:\n\n- **60+ UI Components** - Beautiful, composable widgets\n- **Multi-Provider Support** - OpenAI, Anthropic, Google\n- **Conversation Management** - With branching support\n- **Tool/Function Calling** - Register and execute tools\n- **Persistence Layer** - Offline-first with cloud sync\n- **Business Features** - Usage tracking, analytics\n\nThe library follows a modern design aesthetic with a zinc color palette.",
];
@override
String get name => 'Mock AI';
@override
String get id => 'mock';
@override
String get defaultModel => 'mock-gpt-4';
@override
List<RaptrAIModelInfo> get availableModels => const [
RaptrAIModelInfo(
id: 'mock-gpt-4',
name: 'Mock GPT-4',
description: 'Simulated GPT-4 responses',
contextWindow: 128000,
supportsVision: true,
supportsTools: true,
inputPricePerMillion: 10.0,
outputPricePerMillion: 30.0,
),
RaptrAIModelInfo(
id: 'mock-gpt-3.5',
name: 'Mock GPT-3.5',
description: 'Simulated GPT-3.5 responses',
contextWindow: 16000,
supportsVision: false,
supportsTools: true,
inputPricePerMillion: 0.5,
outputPricePerMillion: 1.5,
),
RaptrAIModelInfo(
id: 'mock-claude',
name: 'Mock Claude',
description: 'Simulated Claude responses',
contextWindow: 200000,
supportsVision: true,
supportsTools: true,
inputPricePerMillion: 3.0,
outputPricePerMillion: 15.0,
),
];
@override
Stream<RaptrAIChunk> chat({
required List<RaptrAIMessage> messages,
required String model,
List<RaptrAIToolDefinition>? tools,
RaptrAIChatConfig config = RaptrAIChatConfig.defaults,
}) async* {
_isCancelled = false;
// Simulate initial delay
await Future.delayed(const Duration(milliseconds: 300));
if (_isCancelled) return;
// Check if tools are available and should be called
if (tools != null && tools.isNotEmpty) {
final lastMessage = messages.lastWhere((m) => m.role == RaptrAIRole.user);
final content = lastMessage.content.toLowerCase();
// Check for weather tool trigger
if (content.contains('weather')) {
yield const RaptrAIChunk(
content: "Let me check the weather for you.\n\n",
);
await Future.delayed(const Duration(milliseconds: 200));
yield RaptrAIChunk(
toolCalls: [
const RaptrAIToolCallDelta(
index: 0,
id: 'call_weather_1',
name: 'get_weather',
argumentsDelta: '{"location": "San Francisco", "unit": "celsius"}',
),
],
finishReason: RaptrAIFinishReason.toolCalls,
);
return;
}
// Check for calculator tool trigger
if (content.contains('calculate') || content.contains('math')) {
yield const RaptrAIChunk(
content: "Let me calculate that for you.\n\n",
);
await Future.delayed(const Duration(milliseconds: 200));
yield RaptrAIChunk(
toolCalls: [
const RaptrAIToolCallDelta(
index: 0,
id: 'call_calc_1',
name: 'calculator',
argumentsDelta: '{"expression": "2 + 2 * 3"}',
),
],
finishReason: RaptrAIFinishReason.toolCalls,
);
return;
}
// Check for search tool trigger
if (content.contains('search') || content.contains('find')) {
yield const RaptrAIChunk(
content: "Let me search for that information.\n\n",
);
await Future.delayed(const Duration(milliseconds: 200));
yield RaptrAIChunk(
toolCalls: [
const RaptrAIToolCallDelta(
index: 0,
id: 'call_search_1',
name: 'web_search',
argumentsDelta: '{"query": "Flutter best practices 2025"}',
),
],
finishReason: RaptrAIFinishReason.toolCalls,
);
return;
}
}
// Select a random response
final random = Random();
final response = _responses[random.nextInt(_responses.length)];
// Stream the response character by character
final promptTokens = messages.fold<int>(0, (sum, m) => sum + m.content.length ~/ 4);
for (int i = 0; i < response.length; i++) {
if (_isCancelled) return;
yield RaptrAIChunk(content: response[i]);
await Future.delayed(responseDelay);
}
// Final chunk with usage
yield RaptrAIChunk(
finishReason: RaptrAIFinishReason.stop,
usage: RaptrAIUsage(
promptTokens: promptTokens,
completionTokens: response.length ~/ 4,
totalTokens: promptTokens + response.length ~/ 4,
),
);
}
@override
void cancel() {
_isCancelled = true;
}
@override
Future<int> countTokens(List<RaptrAIMessage> messages, {String? model}) async {
return messages.fold<int>(0, (sum, m) => sum + m.content.length ~/ 4);
}
@override
Future<bool> validate() async => true;
}
// =============================================================================
// MOCK STORAGE - Uses library's RaptrAIMemoryStorage with sample data
// =============================================================================
/// Creates and initializes a memory storage with sample conversations.
Future<RaptrAIMemoryStorage> createMockStorage() async {
final storage = RaptrAIMemoryStorage();
await storage.initialize();
// Add some sample conversations
final now = DateTime.now();
await storage.saveConversation(RaptrAIConversation(
id: 'conv_sample_1',
title: 'Welcome Chat',
messages: [
RaptrAIConversationMessage.user('Hello! What can you do?'),
RaptrAIConversationMessage.assistant(
'Hi! I\'m RaptrAI assistant. I can help you with coding, answer questions, and much more!',
),
],
createdAt: now.subtract(const Duration(days: 1)),
updatedAt: now.subtract(const Duration(days: 1)),
));
await storage.saveConversation(RaptrAIConversation(
id: 'conv_sample_2',
title: 'Flutter Tips',
messages: [
RaptrAIConversationMessage.user('Give me some Flutter performance tips'),
RaptrAIConversationMessage.assistant(
'Here are some Flutter performance tips:\n\n1. Use const constructors\n2. Implement ListView.builder for long lists\n3. Cache expensive computations\n4. Use RepaintBoundary wisely',
),
],
createdAt: now.subtract(const Duration(hours: 5)),
updatedAt: now.subtract(const Duration(hours: 5)),
));
return storage;
}
// =============================================================================
// MOCK TOOLS - Sample tool implementations
// =============================================================================
RaptrAIToolRegistry createMockToolRegistry() {
final registry = RaptrAIToolRegistry();
// Weather tool
registry.register(
RaptrAIToolBuilder('get_weather')
.description('Get current weather for a location')
.addStringParam('location', description: 'City name', required: true)
.addEnumParam('unit', ['celsius', 'fahrenheit'], defaultValue: 'celsius')
.handler((args) async {
await Future.delayed(const Duration(seconds: 1));
final location = args['location'] as String;
final unit = args['unit'] as String? ?? 'celsius';
final random = Random();
final temp = unit == 'celsius' ? 15 + random.nextInt(20) : 60 + random.nextInt(35);
final conditions = ['sunny', 'cloudy', 'rainy', 'partly cloudy'];
return {
'location': location,
'temperature': temp,
'unit': unit,
'condition': conditions[random.nextInt(conditions.length)],
'humidity': 40 + random.nextInt(40),
'wind_speed': 5 + random.nextInt(20),
};
})
.build(),
);
// Calculator tool
registry.register(
RaptrAIToolBuilder('calculator')
.description('Perform mathematical calculations')
.addStringParam('expression', description: 'Math expression to evaluate', required: true)
.handler((args) async {
await Future.delayed(const Duration(milliseconds: 500));
final expression = args['expression'] as String;
// Simple evaluation (in production, use a proper math parser)
try {
// This is a mock - just return a plausible result
final result = 8; // 2 + 2 * 3 = 8
return {
'expression': expression,
'result': result,
'formatted': '$expression = $result',
};
} catch (e) {
return {
'expression': expression,
'error': 'Could not evaluate expression',
};
}
})
.build(),
);
// Web search tool
registry.register(
RaptrAIToolBuilder('web_search')
.description('Search the web for information')
.addStringParam('query', description: 'Search query', required: true)
.addIntegerParam('max_results', description: 'Maximum results', defaultValue: 5)
.handler((args) async {
await Future.delayed(const Duration(seconds: 1));
final query = args['query'] as String;
return {
'query': query,
'results': [
{
'title': 'Flutter Documentation',
'url': 'https://flutter.dev/docs',
'snippet': 'Official Flutter documentation and guides.',
},
{
'title': 'Dart Language Tour',
'url': 'https://dart.dev/guides/language/language-tour',
'snippet': 'A tour of all the major Dart language features.',
},
{
'title': 'Flutter Best Practices 2025',
'url': 'https://example.com/flutter-best-practices',
'snippet': 'Top Flutter development practices and patterns.',
},
],
'total_results': 3,
};
})
.build(),
);
return registry;
}
// =============================================================================
// MAIN APP
// =============================================================================
class RaptrAIExampleApp extends StatefulWidget {
const RaptrAIExampleApp({super.key});
@override
State<RaptrAIExampleApp> createState() => _RaptrAIExampleAppState();
}
class _RaptrAIExampleAppState extends State<RaptrAIExampleApp> {
ThemeMode _themeMode = ThemeMode.dark;
void _toggleTheme() {
setState(() {
_themeMode = _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RaptrAI Demo',
debugShowCheckedModeBanner: false,
theme: RaptrAITheme.light(),
darkTheme: RaptrAITheme.dark(),
themeMode: _themeMode,
home: MainScreen(onToggleTheme: _toggleTheme),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({required this.onToggleTheme, super.key});
final VoidCallback onToggleTheme;
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
late final MockAIProvider _mockProvider;
RaptrAIMemoryStorage? _mockStorage;
late final RaptrAIToolRegistry _toolRegistry;
late final RaptrAIUsageTracker _usageTracker;
bool _isInitialized = false;
@override
void initState() {
super.initState();
_mockProvider = MockAIProvider();
_toolRegistry = createMockToolRegistry();
_usageTracker = RaptrAIUsageTracker(
limits: const RaptrAIUsageLimits(
maxTokensPerDay: 100000,
maxRequestsPerMinute: 60,
maxCostPerDay: 10.0,
),
onUsageUpdate: (usage) {
debugPrint('[Usage] Tokens: ${usage.totalTokens}, Cost: \$${usage.estimatedCost.toStringAsFixed(4)}');
},
onLimitExceeded: (limitType) {
debugPrint('[Usage] Limit exceeded: $limitType');
},
);
_initStorage();
}
Future<void> _initStorage() async {
_mockStorage = await createMockStorage();
if (mounted) {
setState(() => _isInitialized = true);
}
}
@override
Widget build(BuildContext context) {
if (!_isInitialized || _mockStorage == null) {
return Scaffold(
appBar: AppBar(title: const Text('RaptrAI Demo')),
body: const Center(child: CircularProgressIndicator()),
);
}
final screens = [
FullChatDemo(
provider: _mockProvider,
storage: _mockStorage!,
toolRegistry: _toolRegistry,
usageTracker: _usageTracker,
),
const AssistantUIDemo(),
const ChatDemoScreen(),
ComponentsScreen(usageTracker: _usageTracker),
ToolsDemo(toolRegistry: _toolRegistry),
DefaultsDemo(usageTracker: _usageTracker, toolRegistry: _toolRegistry),
];
return Scaffold(
appBar: AppBar(
title: const Text('RaptrAI Demo'),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
Theme.of(context).brightness == Brightness.dark
? Icons.light_mode
: Icons.dark_mode,
),
onPressed: widget.onToggleTheme,
tooltip: 'Toggle theme',
),
],
),
body: screens[_currentIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) => setState(() => _currentIndex = index),
destinations: const [
NavigationDestination(
icon: Icon(Icons.smart_toy_outlined),
selectedIcon: Icon(Icons.smart_toy),
label: 'Full Chat',
),
NavigationDestination(
icon: Icon(Icons.assistant_outlined),
selectedIcon: Icon(Icons.assistant),
label: 'Assistant',
),
NavigationDestination(
icon: Icon(Icons.chat_outlined),
selectedIcon: Icon(Icons.chat),
label: 'Chat UI',
),
NavigationDestination(
icon: Icon(Icons.widgets_outlined),
selectedIcon: Icon(Icons.widgets),
label: 'Components',
),
NavigationDestination(
icon: Icon(Icons.build_outlined),
selectedIcon: Icon(Icons.build),
label: 'Tools',
),
NavigationDestination(
icon: Icon(Icons.auto_fix_high_outlined),
selectedIcon: Icon(Icons.auto_fix_high),
label: 'Defaults',
),
],
),
);
}
}
// =============================================================================
// FULL CHAT DEMO - Complete integrated experience
// =============================================================================
class FullChatDemo extends StatefulWidget {
const FullChatDemo({
required this.provider,
required this.storage,
required this.toolRegistry,
required this.usageTracker,
super.key,
});
final RaptrAIProvider provider;
final RaptrAIStorage storage;
final RaptrAIToolRegistry toolRegistry;
final RaptrAIUsageTracker usageTracker;
@override
State<FullChatDemo> createState() => _FullChatDemoState();
}
class _FullChatDemoState extends State<FullChatDemo> {
String? _selectedConversationId;
List<RaptrAIConversation> _conversations = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadConversations();
}
Future<void> _loadConversations() async {
await widget.storage.initialize();
final list = await widget.storage.listConversations();
if (mounted) {
setState(() {
_conversations = list.conversations;
_isLoading = false;
});
}
}
void _handleNewThread() {
setState(() => _selectedConversationId = null);
}
void _handleSelectThread(RaptrAIThreadData thread) {
setState(() => _selectedConversationId = thread.id);
}
Future<void> _handleDeleteThread(RaptrAIThreadData thread) async {
await widget.storage.deleteConversation(thread.id);
await _loadConversations();
if (_selectedConversationId == thread.id) {
setState(() => _selectedConversationId = null);
}
}
void _handleConversationChanged(RaptrAIConversation conversation) {
_loadConversations();
if (_selectedConversationId == null) {
setState(() => _selectedConversationId = conversation.id);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
final screenWidth = MediaQuery.of(context).size.width;
final showSidebar = screenWidth >= 768;
return Row(
children: [
// Sidebar
if (showSidebar)
SizedBox(
width: 280,
child: RaptrAIThreadList(
threads: _conversations.map((c) => RaptrAIThreadData(
id: c.id,
title: c.title ?? 'New conversation',
preview: c.messages.isNotEmpty ? c.messages.last.content : null,
isActive: c.id == _selectedConversationId,
)).toList(),
selectedThreadId: _selectedConversationId,
onNewThread: _handleNewThread,
onSelectThread: _handleSelectThread,
onDeleteThread: _handleDeleteThread,
header: _buildUsageHeader(),
),
),
if (showSidebar) const VerticalDivider(width: 1),
// Main chat area
Expanded(
child: RaptrAIChat(
key: ValueKey(_selectedConversationId),
provider: widget.provider,
model: 'mock-gpt-4',
systemPrompt: 'You are RaptrAI, a helpful AI assistant powered by the RaptrAI Flutter framework. Be helpful, concise, and friendly.',
storage: widget.storage,
tools: widget.toolRegistry.definitions,
toolRegistry: widget.toolRegistry,
usageTracker: widget.usageTracker,
conversationId: _selectedConversationId,
welcomeGreeting: 'Welcome to RaptrAI!',
welcomeSubtitle: 'The complete AI framework for Flutter',
suggestions: const [
RaptrAISuggestion(
title: "What's the weather",
subtitle: 'in San Francisco?',
icon: Icons.cloud_outlined,
),
RaptrAISuggestion(
title: 'Calculate something',
subtitle: 'with the math tool',
icon: Icons.calculate_outlined,
),
RaptrAISuggestion(
title: 'Search the web',
subtitle: 'for Flutter tips',
icon: Icons.search,
),
RaptrAISuggestion(
title: 'Tell me about',
subtitle: 'RaptrAI features',
icon: Icons.info_outline,
),
],
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${error.message}'),
backgroundColor: RaptrAIColors.error,
),
);
},
onConversationChanged: _handleConversationChanged,
),
),
],
);
}
Widget _buildUsageHeader() {
return ListenableBuilder(
listenable: widget.usageTracker,
builder: (context, _) {
final usage = widget.usageTracker.currentUsage;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? RaptrAIColors.darkSurface
: RaptrAIColors.lightSurface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? RaptrAIColors.darkBorder
: RaptrAIColors.lightBorder,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.analytics_outlined, size: 16),
const SizedBox(width: 8),
Text(
'Usage Today',
style: Theme.of(context).textTheme.labelMedium,
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Tokens: ${usage.totalTokens}'),
Text('\$${usage.estimatedCost.toStringAsFixed(4)}'),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: (usage.totalTokens / 100000).clamp(0.0, 1.0),
backgroundColor: Theme.of(context).brightness == Brightness.dark
? RaptrAIColors.darkBorder
: RaptrAIColors.lightBorder,
),
],
),
);
},
);
}
}
// =============================================================================
// ASSISTANT UI DEMO - Thread-based chat experience
// =============================================================================
class AssistantUIDemo extends StatefulWidget {
const AssistantUIDemo({super.key});
@override
State<AssistantUIDemo> createState() => _AssistantUIDemoState();
}
class _AssistantUIDemoState extends State<AssistantUIDemo> {
final List<_Message> _messages = [];
bool _isGenerating = false;
String _streamingContent = '';
String? _selectedThreadId;
final List<RaptrAIThreadData> _threads = [
const RaptrAIThreadData(
id: '1',
title: 'Getting Started',
preview: 'How can I help you today?',
),
const RaptrAIThreadData(
id: '2',
title: 'Flutter Questions',
preview: 'Tell me about state management...',
),
const RaptrAIThreadData(
id: '3',
title: 'Code Review',
preview: 'Can you review this PR?',
),
];
RaptrAIModel _selectedModel = const RaptrAIModel(
id: 'gpt-4',
name: 'GPT-4',
description: 'Most capable model',
icon: Icons.auto_awesome,
);
final List<RaptrAIModel> _models = const [
RaptrAIModel(
id: 'gpt-4',
name: 'GPT-4',
description: 'Most capable model',
icon: Icons.auto_awesome,
),
RaptrAIModel(
id: 'gpt-3.5',
name: 'GPT-3.5 Turbo',
description: 'Fast and efficient',
icon: Icons.bolt,
),
RaptrAIModel(
id: 'claude',
name: 'Claude 3',
description: 'Anthropic model',
icon: Icons.psychology,
),
];
Future<void> _sendMessage(String text, List<RaptrAIAttachment> attachments) async {
if (text.trim().isEmpty) return;
setState(() {
_messages.add(_Message(content: text, role: RaptrAIMessageRole.user));
_isGenerating = true;
_streamingContent = '';
});
// Simulate streaming response
const response = 'Thanks for your message! This is a demo response from RaptrAI. '
'The components you see here feature a modern design system '
'with Inter font and zinc color palette for optimal readability.\n\n'
'**Key Features:**\n'
'- Beautiful, composable widgets\n'
'- Multi-provider AI support\n'
'- Tool/function calling\n'
'- Conversation branching';
for (int i = 0; i < response.length; i++) {
if (!_isGenerating) break;
await Future.delayed(const Duration(milliseconds: 20));
if (mounted) {
setState(() {
_streamingContent = response.substring(0, i + 1);
});
}
}
if (mounted && _isGenerating) {
setState(() {
_isGenerating = false;
_messages.add(_Message(content: response, role: RaptrAIMessageRole.assistant));
_streamingContent = '';
});
}
}
void _handleSuggestionTap(RaptrAISuggestion suggestion) {
final text = suggestion.subtitle != null
? '${suggestion.title} ${suggestion.subtitle}'
: suggestion.title;
_sendMessage(text, []);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final showSidebar = screenWidth >= 768;
return Row(
children: [
// Thread List Sidebar
if (showSidebar)
RaptrAIThreadList(
threads: _threads,
selectedThreadId: _selectedThreadId,
onNewThread: () {
setState(() {
_selectedThreadId = null;
_messages.clear();
});
},
onSelectThread: (thread) {
setState(() => _selectedThreadId = thread.id);
},
onDeleteThread: (thread) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deleted: ${thread.title}')),
);
},
header: RaptrAIModelSelector(
models: _models,
selectedModel: _selectedModel,
onModelSelected: (model) {
setState(() => _selectedModel = model);
},
),
),
// Main Thread Area
Expanded(
child: RaptrAIThread(
welcome: RaptrAIThreadWelcome(
greeting: 'Hello there!',
subtitle: 'How can I help you today?',
suggestions: const [
RaptrAISuggestion(
title: "What's the weather",
subtitle: 'in San Francisco?',
icon: Icons.cloud_outlined,
),
RaptrAISuggestion(
title: 'Explain React hooks',
subtitle: 'like useState and useEffect',
icon: Icons.code,
),
RaptrAISuggestion(
title: 'Write a poem',
subtitle: 'about the ocean',
icon: Icons.edit_note,
),
RaptrAISuggestion(
title: 'Help me debug',
subtitle: 'my Flutter app',
icon: Icons.bug_report_outlined,
),
],
onSuggestionTap: _handleSuggestionTap,
),
messages: [
..._messages.map((m) {
if (m.role == RaptrAIMessageRole.user) {
return RaptrAIUserMessage(content: m.content);
}
return RaptrAIAssistantMessage(
content: m.content,
isStreaming: false,
onRegenerate: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Regenerating...')),
);
},
);
}),
if (_isGenerating && _streamingContent.isNotEmpty)
RaptrAIAssistantMessage(
content: _streamingContent,
isStreaming: true,
),
],
composer: RaptrAIComposer(
placeholder: 'Send a message...',
onSend: _sendMessage,
isGenerating: _isGenerating,
onStop: () {
setState(() => _isGenerating = false);
},
onAddAttachment: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Add attachment tapped')),
);
},
),
),
),
],
);
}
}
class _Message {
final String content;
final RaptrAIMessageRole role;
_Message({required this.content, required this.role});
}
// =============================================================================
// CHAT DEMO SCREEN - Simple chat bubbles
// =============================================================================
class ChatDemoScreen extends StatefulWidget {
const ChatDemoScreen({super.key});
@override
State<ChatDemoScreen> createState() => _ChatDemoScreenState();
}
class _ChatDemoScreenState extends State<ChatDemoScreen> {
final List<_ChatMessage> _messages = [
_ChatMessage(
content: "Hello! I'm RaptrAI assistant. How can I help you today?",
isUser: false,
),
];
bool _isTyping = false;
void _sendMessage(String text) {
if (text.trim().isEmpty) return;
setState(() {
_messages.add(_ChatMessage(content: text, isUser: true));
_isTyping = true;
});
// Simulate AI response
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_isTyping = false;
_messages.add(_ChatMessage(
content: 'Thanks for your message! This is a demo response from '
'RaptrAI. The components you see here are all from the raptrai '
'Flutter package - a complete UI framework for AI interfaces.',
isUser: false,
));
});
}
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Suggestions at top if few messages
if (_messages.length <= 2) ...[
Padding(
padding: const EdgeInsets.all(16),
child: RaptrAIPromptSuggestions(
suggestions: const [
'Tell me about RaptrAI',
'Show me components',
'How does theming work?',
],
onSuggestionTap: _sendMessage,
),
),
],
// Messages
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _messages.length + (_isTyping ? 1 : 0),
itemBuilder: (context, index) {
if (index == _messages.length && _isTyping) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: RaptrAITypingIndicator(),
);
}
final message = _messages[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: message.isUser
? RaptrAIChatBubble.user(content: message.content)
: RaptrAIChatBubble.assistant(content: message.content),
);
},
),
),
// Input
Padding(
padding: const EdgeInsets.all(16),
child: RaptrAIPromptInput(
hintText: 'Type a message...',
onSubmit: _sendMessage,
),
),
],
);
}
}
class _ChatMessage {
final String content;
final bool isUser;
_ChatMessage({required this.content, required this.isUser});
}
// =============================================================================
// COMPONENTS SCREEN - UI component showcase
// =============================================================================
class ComponentsScreen extends StatelessWidget {
const ComponentsScreen({required this.usageTracker, super.key});
final RaptrAIUsageTracker usageTracker;
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Usage Stats Section
const _SectionHeader(title: 'Usage Statistics'),
_UsageStatsCard(usageTracker: usageTracker),
const SizedBox(height: 24),
// Tool UI Section
const _SectionHeader(title: 'Tool UI Components'),
const RaptrAIToolCallWidget(
toolCall: RaptrAIToolCallData(
id: '1',
name: 'get_weather',
arguments: {'location': 'San Francisco', 'unit': 'celsius'},
status: RaptrAIToolCallStatus.completed,
result: 'Temperature: 18°C, Partly cloudy',
),
initiallyExpanded: true,
),
const SizedBox(height: 12),
const RaptrAIToolCallWidget(
toolCall: RaptrAIToolCallData(
id: '2',
name: 'web_search',
arguments: {'query': 'Flutter best practices 2025'},
status: RaptrAIToolCallStatus.running,
),
),
const SizedBox(height: 12),
const RaptrAIToolCallWidget(
toolCall: RaptrAIToolCallData(
id: '3',
name: 'calculator',
arguments: {'expression': '(2 + 3) * 4'},
status: RaptrAIToolCallStatus.failed,
error: 'Division by zero error',
),
),
const SizedBox(height: 24),
// Buttons Section
const _SectionHeader(title: 'Buttons'),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
RaptrAIButton(label: 'Primary', onPressed: () {}),
RaptrAIButton.secondary(label: 'Secondary', onPressed: () {}),
RaptrAIButton.outlined(label: 'Outlined', onPressed: () {}),
RaptrAIButton.ghost(label: 'Ghost', onPressed: () {}),
RaptrAIButton.danger(label: 'Danger', onPressed: () {}),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
RaptrAIButton(
label: 'With Icon',
icon: Icons.star,
onPressed: () {},
),
RaptrAIButton(
label: 'Loading',
isLoading: true,
onPressed: () {},
),
RaptrAIButton(
label: 'Disabled',
disabled: true,
onPressed: () {},
),
],
),
const SizedBox(height: 24),
// Badges Section
const _SectionHeader(title: 'Badges'),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
const RaptrAIBadge(label: 'Default'),
RaptrAIBadge.success(label: 'Success'),
RaptrAIBadge.warning(label: 'Warning'),
RaptrAIBadge.error(label: 'Error'),
RaptrAIBadge.info(label: 'Info'),
RaptrAIBadge.accent(label: 'Accent'),
],
),
const SizedBox(height: 24),
// Alerts Section
const _SectionHeader(title: 'Alerts'),
RaptrAIAlert.info(
title: 'Information',
message: 'This is an informational alert with helpful details.',
),
const SizedBox(height: 8),
RaptrAIAlert.success(
title: 'Success',
message: 'Operation completed successfully!',
),
const SizedBox(height: 8),
RaptrAIAlert.warning(
title: 'Warning',
message: 'Please review before proceeding.',
),
const SizedBox(height: 8),
RaptrAIAlert.error(
title: 'Error',
message: 'Something went wrong. Please try again.',
),
const SizedBox(height: 24),
// Cards Section
const _SectionHeader(title: 'Cards'),
RaptrAICard(
style: RaptrAICardStyle.bordered,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Bordered Card'),
),
),
const SizedBox(height: 8),
RaptrAICard(
style: RaptrAICardStyle.elevated,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Elevated Card'),
),
),
const SizedBox(height: 8),
RaptrAICard(
style: RaptrAICardStyle.filled,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Filled Card'),
),
),
const SizedBox(height: 24),
// Branch Picker
const _SectionHeader(title: 'Branch Picker'),
Row(
children: [
RaptrAIBranchPicker(
currentIndex: 2,
totalBranches: 5,
onPrevious: () {},
onNext: () {},
),
],
),
const SizedBox(height: 24),
// Code Block Section
const _SectionHeader(title: 'Code Block'),
const RaptrAICodeBlock(
code: '''// RaptrAI Example
import 'package:raptrai/raptrai.dart';
void main() {
final provider = MockAIProvider();
RaptrAIChat(
provider: provider,
systemPrompt: 'You are a helpful assistant.',
tools: toolRegistry.definitions,
);
}''',
language: 'dart',
),
const SizedBox(height: 24),
// Typing Indicators Section
const _SectionHeader(title: 'Typing Indicators'),
const Row(
children: [
RaptrAITypingIndicator(),
SizedBox(width: 32),
RaptrAIPulsingIndicator(),
],
),
const SizedBox(height: 24),
// Streaming Text Section
const _SectionHeader(title: 'Streaming Text'),
const RaptrAIStreamingText(
text: 'This text has a blinking cursor effect...',
showCursor: true,
),
const SizedBox(height: 24),
// Copy Button Section
const _SectionHeader(title: 'Copy Button'),
Row(
children: [
RaptrAICopyButton(
textToCopy: 'Hello, World!',
onCopied: () {},
),
const SizedBox(width: 8),
const Text('Click to copy "Hello, World!"'),
],
),
const SizedBox(height: 40),
],
);
}
}
class _UsageStatsCard extends StatelessWidget {
const _UsageStatsCard({required this.usageTracker});
final RaptrAIUsageTracker usageTracker;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: usageTracker,
builder: (context, _) {
final usage = usageTracker.currentUsage;
final limits = usageTracker.getLimitStatus();
final isDark = Theme.of(context).brightness == Brightness.dark;
return RaptrAICard(
style: RaptrAICardStyle.bordered,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.analytics,
color: RaptrAIColors.accent,
),
const SizedBox(width: 8),
Text(
'API Usage Statistics',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
_UsageRow(
label: 'Total Tokens',
value: '${usage.totalTokens}',
icon: Icons.token,
),
_UsageRow(
label: 'Prompt Tokens',
value: '${usage.promptTokens}',
icon: Icons.input,
),
_UsageRow(
label: 'Completion Tokens',
value: '${usage.completionTokens}',
icon: Icons.output,
),
_UsageRow(
label: 'Requests',
value: '${usage.requests}',
icon: Icons.send,
),
_UsageRow(
label: 'Estimated Cost',
value: '\$${usage.estimatedCost.toStringAsFixed(4)}',
icon: Icons.attach_money,
),
if (limits.isNotEmpty) ...[
const Divider(height: 24),
Text(
'Limits',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
...limits.map((status) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_getLimitName(status.limitType)),
Text(
'${status.current} / ${status.limit}',
style: TextStyle(
color: status.isExceeded
? RaptrAIColors.error
: null,
),
),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: status.percentUsed.clamp(0.0, 1.0),
backgroundColor: isDark
? RaptrAIColors.darkBorder
: RaptrAIColors.lightBorder,
valueColor: AlwaysStoppedAnimation(
status.percentUsed > 0.9
? RaptrAIColors.error
: status.percentUsed > 0.7
? RaptrAIColors.warning
: RaptrAIColors.success,
),
),
],
),
)),
],
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: RaptrAIButton.outlined(
label: 'Reset Stats',
icon: Icons.refresh,
onPressed: () => usageTracker.reset(),
),
),
],
),
],
),
),
);
},
);
}
String _getLimitName(RaptrAILimitType type) {
switch (type) {
case RaptrAILimitType.dailyTokens:
return 'Daily Tokens';
case RaptrAILimitType.requestsPerMinute:
return 'Requests/Min';
case RaptrAILimitType.dailyCost:
return 'Daily Cost';
case RaptrAILimitType.requestTokens:
return 'Request Tokens';
}
}
}
class _UsageRow extends StatelessWidget {
const _UsageRow({
required this.label,
required this.value,
required this.icon,
});
final String label;
final String value;
final IconData icon;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(
icon,
size: 16,
color: isDark ? RaptrAIColors.darkTextMuted : RaptrAIColors.lightTextMuted,
),
const SizedBox(width: 8),
Expanded(child: Text(label)),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
),
);
}
}
// =============================================================================
// TOOLS DEMO SCREEN
// =============================================================================
class ToolsDemo extends StatefulWidget {
const ToolsDemo({required this.toolRegistry, super.key});
final RaptrAIToolRegistry toolRegistry;
@override
State<ToolsDemo> createState() => _ToolsDemoState();
}
class _ToolsDemoState extends State<ToolsDemo> {
final List<_ToolExecution> _executions = [];
bool _isExecuting = false;
Future<void> _executeTool(String toolName, Map<String, dynamic> args) async {
setState(() => _isExecuting = true);
final toolCall = RaptrAIToolCall(
id: 'call_${DateTime.now().millisecondsSinceEpoch}',
name: toolName,
arguments: args,
);
setState(() {
_executions.insert(0, _ToolExecution(
toolCall: toolCall,
status: RaptrAIToolCallStatus.running,
));
});
final result = await widget.toolRegistry.execute(toolCall);
setState(() {
_executions[0] = _ToolExecution(
toolCall: toolCall,
status: result.success ? RaptrAIToolCallStatus.completed : RaptrAIToolCallStatus.failed,
result: result.success ? result.data.toString() : null,
error: result.error,
);
_isExecuting = false;
});
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
const _SectionHeader(title: 'Available Tools'),
...widget.toolRegistry.tools.map((tool) => Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(_getToolIcon(tool.name)),
title: Text(tool.name),
subtitle: Text(tool.description),
trailing: IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: _isExecuting
? null
: () => _showToolDialog(context, tool),
),
),
)),
const SizedBox(height: 24),
const _SectionHeader(title: 'Quick Execute'),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
RaptrAIButton(
label: 'Get Weather',
icon: Icons.cloud,
disabled: _isExecuting,
onPressed: () => _executeTool('get_weather', {
'location': 'San Francisco',
'unit': 'celsius',
}),
),
RaptrAIButton(
label: 'Calculate',
icon: Icons.calculate,
disabled: _isExecuting,
onPressed: () => _executeTool('calculator', {
'expression': '2 + 2 * 3',
}),
),
RaptrAIButton(
label: 'Web Search',
icon: Icons.search,
disabled: _isExecuting,
onPressed: () => _executeTool('web_search', {
'query': 'Flutter 2025',
}),
),
],
),
const SizedBox(height: 24),
const _SectionHeader(title: 'Execution History'),
if (_executions.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('No executions yet. Try running a tool!'),
),
)
else
..._executions.map((exec) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: RaptrAIToolCallWidget(
toolCall: RaptrAIToolCallData(
id: exec.toolCall.id,
name: exec.toolCall.name,
arguments: exec.toolCall.arguments,
status: exec.status,
result: exec.result,
error: exec.error,
),
initiallyExpanded: true,
),
)),
],
);
}
IconData _getToolIcon(String toolName) {
switch (toolName) {
case 'get_weather':
return Icons.cloud;
case 'calculator':
return Icons.calculate;
case 'web_search':
return Icons.search;
default:
return Icons.build;
}
}
void _showToolDialog(BuildContext context, RaptrAIRegisteredTool tool) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Execute ${tool.name}'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(tool.description),
const SizedBox(height: 16),
Text(
'Parameters:',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
Text(tool.definition.parameters.toString()),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// Execute with default args
_executeTool(tool.name, _getDefaultArgs(tool.name));
},
child: const Text('Execute'),
),
],
),
);
}
Map<String, dynamic> _getDefaultArgs(String toolName) {
switch (toolName) {
case 'get_weather':
return {'location': 'New York', 'unit': 'fahrenheit'};
case 'calculator':
return {'expression': '10 * 5 + 2'};
case 'web_search':
return {'query': 'Dart programming language'};
default:
return {};
}
}
}
class _ToolExecution {
final RaptrAIToolCall toolCall;
final RaptrAIToolCallStatus status;
final String? result;
final String? error;
_ToolExecution({
required this.toolCall,
required this.status,
this.result,
this.error,
});
}
// =============================================================================
// HELPER WIDGETS
// =============================================================================
class _SectionHeader extends StatelessWidget {
const _SectionHeader({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
);
}
}
// =============================================================================
// DEFAULTS DEMO - Showcase opinionated default UX widgets
// =============================================================================
class DefaultsDemo extends StatefulWidget {
const DefaultsDemo({
required this.usageTracker,
required this.toolRegistry,
super.key,
});
final RaptrAIUsageTracker usageTracker;
final RaptrAIToolRegistry toolRegistry;
@override
State<DefaultsDemo> createState() => _DefaultsDemoState();
}
class _DefaultsDemoState extends State<DefaultsDemo> {
bool _showRetryBanner = false;
bool _showAutoRetryBanner = false;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Introduction
RaptrAICard(
style: RaptrAICardStyle.filled,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.auto_fix_high, color: RaptrAIColors.accent),
const SizedBox(width: 8),
Text(
'Opinionated Default UX',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
'RaptrAI provides opinionated default widgets for common UX patterns like errors, usage limits, retries, and tool approvals. These are ready to use out of the box.',
style: TextStyle(
color: isDark
? RaptrAIColors.darkTextSecondary
: RaptrAIColors.lightTextSecondary,
),
),
],
),
),
),
const SizedBox(height: 24),
// Error States Section
const _SectionHeader(title: 'Error States'),
const Text(
'Full-screen error state with retry button:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
Container(
height: 280,
decoration: BoxDecoration(
border: Border.all(
color: isDark ? RaptrAIColors.darkBorder : RaptrAIColors.lightBorder,
),
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: RaptrAIErrorState(
error: const RaptrAIException(
message: 'Failed to connect to the AI service',
code: 'connection_error',
provider: 'OpenAI',
statusCode: 503,
),
onRetry: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Retry triggered!')),
);
},
),
),
),
const SizedBox(height: 16),
const Text(
'Inline error widget:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
RaptrAIErrorInline(
message: 'Rate limit exceeded. Please wait before sending more messages.',
onRetry: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Retry triggered!')),
);
},
),
const SizedBox(height: 24),
// Usage Limit Section
const _SectionHeader(title: 'Usage Limits'),
const Text(
'Full-screen usage limit reached:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
Container(
height: 320,
decoration: BoxDecoration(
border: Border.all(
color: isDark ? RaptrAIColors.darkBorder : RaptrAIColors.lightBorder,
),
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: RaptrAIUsageLimitReached(
limitType: RaptrAILimitType.dailyTokens,
tracker: widget.usageTracker,
onUpgrade: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Upgrade button pressed!')),
);
},
),
),
),
const SizedBox(height: 16),
const Text(
'Usage limit banner:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
RaptrAIUsageLimitBanner(
message: 'You\'re approaching your daily token limit (80% used).',
action: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('View usage details')),
);
},
actionLabel: 'View',
onDismiss: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Banner dismissed')),
);
},
),
const SizedBox(height: 24),
// Retry Banners Section
const _SectionHeader(title: 'Retry Banners'),
const Text(
'Manual retry banner:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
if (_showRetryBanner)
RaptrAIRetryBanner(
message: 'Connection lost. Your message was not sent.',
onRetry: () {
setState(() => _showRetryBanner = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Retrying...')),
);
},
onDismiss: () {
setState(() => _showRetryBanner = false);
},
)
else
RaptrAIButton.outlined(
label: 'Show Retry Banner',
icon: Icons.refresh,
onPressed: () => setState(() => _showRetryBanner = true),
),
const SizedBox(height: 16),
const Text(
'Auto-retry banner with countdown:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
if (_showAutoRetryBanner)
RaptrAIAutoRetryBanner(
message: 'Request failed. Retrying automatically...',
retryDelay: const Duration(seconds: 5),
onRetry: () {
setState(() => _showAutoRetryBanner = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Auto-retry triggered!')),
);
},
onCancel: () {
setState(() => _showAutoRetryBanner = false);
},
)
else
RaptrAIButton.outlined(
label: 'Show Auto-Retry Banner',
icon: Icons.timer,
onPressed: () => setState(() => _showAutoRetryBanner = true),
),
const SizedBox(height: 24),
// Tool Approval Section
const _SectionHeader(title: 'Tool Approval'),
const Text(
'Inline tool approval card:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
RaptrAIToolApprovalCard(
toolCall: const RaptrAIToolCall(
id: 'call_demo_1',
name: 'get_weather',
arguments: {'location': 'San Francisco', 'unit': 'celsius'},
),
toolDescription: 'Fetches current weather data for the specified location.',
onApprove: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tool approved!')),
);
},
onDeny: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tool denied!')),
);
},
),
const SizedBox(height: 16),
const Text(
'Tool approval dialog:',
style: TextStyle(fontSize: 13),
),
const SizedBox(height: 8),
RaptrAIButton(
label: 'Show Approval Dialog',
icon: Icons.extension,
onPressed: () {
showDialog(
context: context,
builder: (_) => RaptrAIToolApprovalDialog(
toolCall: const RaptrAIToolCall(
id: 'call_demo_2',
name: 'web_search',
arguments: {'query': 'Flutter best practices 2025', 'max_results': 10},
),
toolDescription: 'Searches the web for relevant information.',
onApprove: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tool approved!')),
);
},
onDeny: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tool denied!')),
);
},
),
);
},
),
const SizedBox(height: 24),
// Approval Modes Section
const _SectionHeader(title: 'Tool Approval Modes'),
RaptrAICard(
style: RaptrAICardStyle.bordered,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ApprovalModeRow(
mode: RaptrAIToolApprovalMode.auto,
description: 'Never ask, execute tools automatically',
icon: Icons.flash_on,
),
const Divider(height: 24),
_ApprovalModeRow(
mode: RaptrAIToolApprovalMode.firstUse,
description: 'Ask on first use of each tool',
icon: Icons.verified_user,
),
const Divider(height: 24),
_ApprovalModeRow(
mode: RaptrAIToolApprovalMode.always,
description: 'Always ask before executing tools',
icon: Icons.security,
),
const Divider(height: 24),
_ApprovalModeRow(
mode: RaptrAIToolApprovalMode.never,
description: 'Never execute tools (deny all)',
icon: Icons.block,
),
],
),
),
),
const SizedBox(height: 24),
// Code Example Section
const _SectionHeader(title: 'Usage Example'),
const RaptrAICodeBlock(
code: '''// Enable opinionated defaults in RaptrAIChat
RaptrAIChat(
provider: openai,
// Error handling
onError: (error) {
// Errors automatically show RaptrAIErrorState
},
// Usage tracking with limits
usageTracker: RaptrAIUsageTracker(
limits: RaptrAIUsageLimits(
maxTokensPerDay: 100000,
maxRequestsPerMinute: 60,
),
onLimitExceeded: (type) {
// Shows RaptrAIUsageLimitReached
},
),
// Tool approval
toolApprovalMode: RaptrAIToolApprovalMode.firstUse,
)''',
language: 'dart',
),
const SizedBox(height: 40),
],
);
}
}
class _ApprovalModeRow extends StatelessWidget {
const _ApprovalModeRow({
required this.mode,
required this.description,
required this.icon,
});
final RaptrAIToolApprovalMode mode;
final String description;
final IconData icon;
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Row(
children: [
Icon(
icon,
size: 20,
color: RaptrAIColors.accent,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
mode.name,
style: const TextStyle(fontWeight: FontWeight.w600),
),
Text(
description,
style: TextStyle(
fontSize: 13,
color: isDark
? RaptrAIColors.darkTextMuted
: RaptrAIColors.lightTextMuted,
),
),
],
),
),
],
);
}
}