flutter_leap_sdk 0.2.0 copy "flutter_leap_sdk: ^0.2.0" to clipboard
flutter_leap_sdk: ^0.2.0 copied to clipboard

Flutter package for Liquid AI's LEAP SDK - Deploy small language models on mobile devices

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_leap_sdk/flutter_leap_sdk.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'dart:typed_data';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LEAP SDK Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MainTabScreen(),
    );
  }
}

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

  @override
  State<MainTabScreen> createState() => _MainTabScreenState();
}

class _MainTabScreenState extends State<MainTabScreen> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('LEAP SDK Demo'),
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.chat), text: 'Text Chat'),
              Tab(icon: Icon(Icons.image), text: 'Vision Chat'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            TextChatScreen(),
            VisionChatScreen(),
          ],
        ),
      ),
    );
  }
}

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

  @override
  State<TextChatScreen> createState() => _TextChatScreenState();
}

class _TextChatScreenState extends State<TextChatScreen> {
  String _status = 'Initializing...';
  bool _isDownloading = false;
  bool _isLoading = false;
  double _downloadProgress = 0.0;
  
  Conversation? _conversation;
  final List<ChatMessage> _messages = [];
  final TextEditingController _messageController = TextEditingController();
  bool _isGenerating = false;

  @override
  void initState() {
    super.initState();
    FlutterLeapSdkService.initialize();
    _checkStatus();
  }

  Future<void> _checkStatus() async {
    try {
      final isLoaded = FlutterLeapSdkService.modelLoaded;
      setState(() {
        if (isLoaded && _conversation != null) {
          _status = '🚀 Ready to chat! Try: "What\'s the weather in Paris?"';
        } else if (isLoaded) {
          _status = '✅ Model ready - click Load to start chat';
        } else {
          _status = '⬇️ Need to download model';
        }
      });
    } catch (e) {
      setState(() {
        _status = '❌ Error: $e';
      });
    }
  }

  Future<void> _downloadModel() async {
    setState(() {
      _isDownloading = true;
      _downloadProgress = 0.0;
    });

    try {
      await FlutterLeapSdkService.downloadModel(
        modelName: 'LFM2-350M',
        onProgress: (progress) {
          setState(() {
            _downloadProgress = progress.percentage / 100.0;
            if (progress.isComplete) {
              _isDownloading = false;
              _status = '✅ Downloaded! Loading model...';
              _loadModel();
            }
          });
        },
      );
    } catch (e) {
      setState(() {
        _isDownloading = false;
        _status = '❌ Download failed: $e';
      });
    }
  }

  Future<void> _loadModel() async {
    setState(() {
      _isLoading = true;
    });
    
    try {
      await FlutterLeapSdkService.loadModel(modelPath: 'LFM2-350M');
      
      // Create conversation through service (creates both Dart and native conversation)
      _conversation = await FlutterLeapSdkService.createConversation(
        systemPrompt: 'You are a helpful AI assistant.',
      );
      
      // Register function
      await _conversation!.registerFunction(
        LeapFunction(
          name: 'get_weather',
          description: 'Get weather information for a location',
          parameters: [
            LeapFunctionParameter(
              name: 'location',
              type: 'string',
              description: 'The city name',
              required: true,
            ),
          ],
          implementation: (args) async {
            final location = args['location'] as String;
            return {
              'location': location,
              'temperature': 22,
              'description': 'Sunny',
            };
          },
        ),
      );
      
      setState(() {
        _isLoading = false;
        _status = '🚀 Ready to chat! Try: "What\'s the weather in Paris?"';
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
        _status = '❌ Load failed: $e';
      });
    }
  }

  Future<void> _sendMessage() async {
    if (_messageController.text.trim().isEmpty || _conversation == null) return;

    final userMessage = _messageController.text.trim();
    _messageController.clear();

    setState(() {
      _messages.add(ChatMessage.user(userMessage));
      _isGenerating = true;
    });

    try {
      String assistantResponse = '';
      bool hasFunctionCalls = false;
      
      await for (final response in _conversation!.generateResponseStructured(userMessage)) {
        if (response is MessageResponseChunk) {
          assistantResponse += response.text;
          setState(() {
            if (_messages.isEmpty || _messages.last.role != MessageRole.assistant) {
              _messages.add(ChatMessage.assistant(''));
            }
            _messages[_messages.length - 1] = ChatMessage.assistant(assistantResponse);
          });
        } else if (response is MessageResponseFunctionCalls) {
          hasFunctionCalls = true;
          // Execute functions
          final results = <Map<String, dynamic>>[];
          for (final call in response.functionCalls) {
            try {
              final result = await _conversation!.executeFunction(call);
              results.add({'call': call.toMap(), 'result': result, 'success': true});
            } catch (e) {
              results.add({'call': call.toMap(), 'error': e.toString(), 'success': false});
            }
          }
          
          // Add results and show in UI
          _conversation!.addFunctionResults(results);
          
          setState(() {
            // Update current assistant response if exists
            if (_messages.isNotEmpty && _messages.last.role == MessageRole.assistant) {
              _messages[_messages.length - 1] = ChatMessage.assistant(assistantResponse);
            }
            
            // Add function results as separate message
            String functionInfo = '🔧 Function Results:\n';
            for (final result in results) {
              final success = result['success'] as bool;
              
              if (success) {
                final functionResult = result['result'] as Map<String, dynamic>;
                functionInfo += '📍 Weather in ${functionResult['location']}: ${functionResult['temperature']}°C, ${functionResult['description']}\n';
              } else {
                functionInfo += '❌ Error: ${result['error']}\n';
              }
            }
            _messages.add(ChatMessage.assistant(functionInfo));
          });
        } else if (response is MessageResponseComplete) {
          // Final response after function calls
          if (hasFunctionCalls && response.message.content.isNotEmpty) {
            setState(() {
              // Add final response as separate message
              _messages.add(ChatMessage.assistant('💬 ${response.message.content}'));
            });
          }
          break;
        }
      }
    } catch (e) {
      setState(() {
        _messages.add(ChatMessage.assistant('Error: $e'));
      });
    } finally {
      setState(() {
        _isGenerating = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('LEAP SDK Demo'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          // Status section
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            color: Colors.grey.shade100,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Status: $_status', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                const SizedBox(height: 8),
                if (_isDownloading)
                  Column(
                    children: [
                      LinearProgressIndicator(value: _downloadProgress),
                      const SizedBox(height: 4),
                      Text('Downloading: ${(_downloadProgress * 100).toStringAsFixed(1)}%'),
                    ],
                  )
                else if (_conversation == null) ...[
                  Row(
                    children: [
                      ElevatedButton(
                        onPressed: _downloadModel,
                        child: const Text('Download Model'),
                      ),
                      const SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: _isLoading ? null : _loadModel,
                        child: _isLoading 
                            ? const SizedBox(
                                width: 16,
                                height: 16,
                                child: CircularProgressIndicator(strokeWidth: 2),
                              )
                            : const Text('Load Model'),
                      ),
                    ],
                  ),
                ],
              ],
            ),
          ),
          
          // Chat section
          if (_conversation != null) ...[
            // Messages
            Expanded(
              child: ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _messages.length,
                itemBuilder: (context, index) {
                  final message = _messages[index];
                  final isUser = message.role == MessageRole.user;
                  
                  return Container(
                    margin: const EdgeInsets.symmetric(vertical: 4),
                    padding: const EdgeInsets.all(12),
                    decoration: BoxDecoration(
                      color: isUser ? Colors.blue.shade100 : Colors.grey.shade100,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          isUser ? 'You' : 'Assistant',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: isUser ? Colors.blue.shade800 : Colors.grey.shade800,
                          ),
                        ),
                        const SizedBox(height: 4),
                        Text(message.content),
                      ],
                    ),
                  );
                },
              ),
            ),
            
            // Input section
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.white,
                border: Border(top: BorderSide(color: Colors.grey.shade300)),
              ),
              child: Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: _messageController,
                      decoration: const InputDecoration(
                        hintText: 'Type a message... (try: "What\'s the weather in Paris?")',
                        border: OutlineInputBorder(),
                      ),
                      onSubmitted: (_) => _sendMessage(),
                      enabled: !_isGenerating,
                    ),
                  ),
                  const SizedBox(width: 8),
                  ElevatedButton(
                    onPressed: _isGenerating ? null : _sendMessage,
                    child: _isGenerating
                        ? const SizedBox(
                            width: 20,
                            height: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : const Icon(Icons.send),
                  ),
                ],
              ),
            ),
          ],
        ],
      ),
    );
  }
}

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

  @override
  State<VisionChatScreen> createState() => _VisionChatScreenState();
}

class _VisionChatScreenState extends State<VisionChatScreen> {
  String _status = 'Initializing...';
  bool _isDownloading = false;
  bool _isLoading = false;
  double _downloadProgress = 0.0;
  
  Conversation? _conversation;
  final TextEditingController _messageController = TextEditingController();
  bool _isGenerating = false;
  String _currentResponse = '';
  
  File? _selectedImage;
  final ImagePicker _picker = ImagePicker();

  @override
  void initState() {
    super.initState();
    FlutterLeapSdkService.initialize();
    _checkStatus();
  }

  Future<void> _checkStatus() async {
    try {
      final isLoaded = FlutterLeapSdkService.modelLoaded;
      final currentModel = FlutterLeapSdkService.currentModel;
      final isVisionModel = currentModel.contains('VL') || currentModel.contains('Vision');
      
      setState(() {
        if (isLoaded && isVisionModel && _conversation != null) {
          _status = '🖼️ Vision model ready! Select an image and ask about it.';
        } else if (isLoaded && !isVisionModel) {
          _status = '⚠️ Please load a vision model (LFM2-VL-1.6B) for image processing';
        } else if (isLoaded) {
          _status = '✅ Model ready - click Load to start vision chat';
        } else {
          _status = '⬇️ Need to download vision model';
        }
      });
    } catch (e) {
      setState(() {
        _status = '❌ Error: $e';
      });
    }
  }

  Future<void> _downloadModel() async {
    setState(() {
      _isDownloading = true;
      _downloadProgress = 0.0;
    });

    try {
      await FlutterLeapSdkService.downloadModel(
        modelName: 'LFM2-VL-1.6B (Vision)',
        onProgress: (progress) {
          setState(() {
            _downloadProgress = progress.percentage / 100.0;
            if (progress.isComplete) {
              _isDownloading = false;
              _status = '✅ Downloaded! Loading vision model...';
              _loadModel();
            }
          });
        },
      );
    } catch (e) {
      setState(() {
        _isDownloading = false;
        _status = '❌ Download failed: $e';
      });
    }
  }

  Future<void> _loadModel() async {
    setState(() {
      _isLoading = true;
    });
    
    try {
      await FlutterLeapSdkService.loadModel(modelPath: 'LFM2-VL-1.6B (Vision)');
      
      _conversation = await FlutterLeapSdkService.createConversation(
        systemPrompt: 'You are a helpful AI assistant that can see and analyze images. Describe what you see in detail and answer questions about the images.',
      );
      
      setState(() {
        _isLoading = false;
        _status = '🖼️ Vision model ready! Select an image and ask about it.';
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
        _status = '❌ Load failed: $e';
      });
    }
  }

  Future<void> _pickImage() async {
    try {
      final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
      if (image != null) {
        setState(() {
          _selectedImage = File(image.path);
        });
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to pick image: $e')),
      );
    }
  }

  Future<void> _sendMessage() async {
    if (_messageController.text.trim().isEmpty || _conversation == null) return;
    if (_selectedImage == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please select an image first')),
      );
      return;
    }

    final userMessage = _messageController.text.trim();
    _messageController.clear();

    setState(() {
      _currentResponse = '';
      _isGenerating = true;
    });

    try {
      final imageBytes = await _selectedImage!.readAsBytes();
      
      // Clear previous response and start streaming
      setState(() {
        _currentResponse = '';
      });
      
      // Generate response with image (vision models don't stream)
      final response = await _conversation!.generateResponseWithImage(userMessage, imageBytes);
      
      setState(() {
        _currentResponse = response;
      });
      
    } catch (e) {
      setState(() {
        _currentResponse = 'Error: $e';
      });
    } finally {
      setState(() {
        _isGenerating = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Status section
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(16),
          color: Colors.grey.shade100,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Status: $_status', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
              const SizedBox(height: 8),
              if (_isDownloading)
                Column(
                  children: [
                    LinearProgressIndicator(value: _downloadProgress),
                    const SizedBox(height: 4),
                    Text('Downloading: ${(_downloadProgress * 100).toStringAsFixed(1)}%'),
                  ],
                )
              else if (_conversation == null) ...[
                Row(
                  children: [
                    ElevatedButton(
                      onPressed: _downloadModel,
                      child: const Text('Download Vision Model'),
                    ),
                    const SizedBox(width: 8),
                    ElevatedButton(
                      onPressed: _isLoading ? null : _loadModel,
                      child: _isLoading 
                          ? const SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(strokeWidth: 2),
                            )
                          : const Text('Load Vision Model'),
                    ),
                  ],
                ),
              ],
            ],
          ),
        ),
        
        // Chat section
        if (_conversation != null) ...[
          // Image selection
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            color: Colors.blue.shade50,
            child: Column(
              children: [
                if (_selectedImage != null) ...[
                  Row(
                    children: [
                      ClipRRect(
                        borderRadius: BorderRadius.circular(4),
                        child: Image.file(
                          _selectedImage!,
                          height: 32,
                          width: 32,
                          fit: BoxFit.cover,
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          _selectedImage!.path.split('/').last,
                          style: const TextStyle(fontSize: 14),
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                ],
                Row(
                  children: [
                    ElevatedButton.icon(
                      onPressed: _pickImage,
                      icon: const Icon(Icons.photo_library),
                      label: Text(_selectedImage == null ? 'Select Image' : 'Change Image'),
                    ),
                    if (_selectedImage != null) ...[
                      const SizedBox(width: 8),
                      TextButton(
                        onPressed: () => setState(() => _selectedImage = null),
                        child: const Text('Clear'),
                      ),
                    ],
                  ],
                ),
              ],
            ),
          ),
          
          // Response Output
          Expanded(
            child: Container(
              margin: const EdgeInsets.all(16),
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.grey.shade50,
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: Colors.grey.shade300),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Icon(Icons.smart_toy, color: Colors.grey.shade600, size: 20),
                      const SizedBox(width: 8),
                      Text(
                        'Vision Assistant Response:',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Colors.grey.shade700,
                          fontSize: 16,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 12),
                  Expanded(
                    child: SingleChildScrollView(
                      child: _isGenerating
                          ? Column(
                              children: [
                                const CircularProgressIndicator(),
                                const SizedBox(height: 16),
                                Text(
                                  'Analyzing image...',
                                  style: TextStyle(
                                    color: Colors.grey.shade600,
                                    fontStyle: FontStyle.italic,
                                  ),
                                ),
                              ],
                            )
                          : _currentResponse.isEmpty
                              ? Text(
                                  'Select an image and ask a question to see the response here.',
                                  style: TextStyle(
                                    color: Colors.grey.shade500,
                                    fontStyle: FontStyle.italic,
                                  ),
                                )
                              : SelectableText(
                                  _currentResponse,
                                  style: const TextStyle(
                                    fontSize: 16,
                                    height: 1.5,
                                  ),
                                ),
                    ),
                  ),
                ],
              ),
            ),
          ),
          
          // Input section
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              border: Border(top: BorderSide(color: Colors.grey.shade300)),
            ),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: const InputDecoration(
                      hintText: 'Ask about the image... (try: "What do you see in this image?")',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _sendMessage(),
                    enabled: !_isGenerating,
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: _isGenerating ? null : _sendMessage,
                  child: _isGenerating
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Icon(Icons.send),
                ),
              ],
            ),
          ),
        ],
      ],
    );
  }
}
13
likes
0
points
243
downloads

Publisher

verified publisherhawier.dev

Weekly Downloads

Flutter package for Liquid AI's LEAP SDK - Deploy small language models on mobile devices

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

dio, flutter, path_provider

More

Packages that depend on flutter_leap_sdk

Packages that implement flutter_leap_sdk