flutter_llama 1.1.2 copy "flutter_llama: ^1.1.2" to clipboard
flutter_llama: ^1.1.2 copied to clipboard

Flutter plugin for running LLM inference with llama.cpp and GGUF models on Android and iOS

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_llama/flutter_llama.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:flutter/services.dart';
import 'screens/model_manager_screen.dart';
import 'screens/model_picker_screen.dart';
import 'services/model_downloader.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shridhar Multimodal Chat',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF10A37F),
          brightness: Brightness.light,
        ),
        useMaterial3: true,
        scaffoldBackgroundColor: Colors.white,
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.white,
          foregroundColor: Colors.black87,
          elevation: 0,
          centerTitle: true,
        ),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF10A37F),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        scaffoldBackgroundColor: const Color(0xFF343541),
        appBarTheme: const AppBarTheme(
          backgroundColor: Color(0xFF343541),
          foregroundColor: Colors.white,
          elevation: 0,
          centerTitle: true,
        ),
      ),
      themeMode: ThemeMode.dark,
      home: const ChatScreen(),
    );
  }
}

class Message {
  final String text;
  final bool isUser;
  final List<String>? imagePaths;
  final DateTime timestamp;

  Message({
    required this.text,
    required this.isUser,
    this.imagePaths,
    DateTime? timestamp,
  }) : timestamp = timestamp ?? DateTime.now();
}

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

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final FlutterLlama _llama = FlutterLlama.instance;
  final TextEditingController _messageController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  final List<Message> _messages = [];
  final List<String> _selectedImages = [];

  bool _isModelLoaded = false;
  bool _isGenerating = false;
  String _modelPath = '';
  String _currentResponse = '';

  @override
  void initState() {
    super.initState();
    _loadModelFromAssets();
  }

  @override
  void dispose() {
    _messageController.dispose();
    _scrollController.dispose();
    _llama.unloadModel();
    super.dispose();
  }

  /// Выбор модели
  Future<void> _pickModel() async {
    try {
      FilePickerResult? result = await FilePicker.platform.pickFiles(
        type: FileType.custom,
        allowedExtensions: ['gguf', 'safetensors'],
      );

      if (result != null && result.files.single.path != null) {
        _modelPath = result.files.single.path!;
        await _loadModel();
      }
    } catch (e) {
      _addSystemMessage('Ошибка выбора файла: $e');
    }
  }

  /// Открыть менеджер моделей
  Future<void> _openModelManager() async {
    final modelId = await Navigator.push<String>(
      context,
      MaterialPageRoute(builder: (context) => const ModelManagerScreen()),
    );

    if (modelId != null) {
      // Пользователь выбрал модель из менеджера
      await _loadDownloadedModel(modelId);
    }
  }

  /// Открыть новый пикер моделей с lazy loading
  Future<void> _openModelPicker() async {
    final model = await Navigator.push<PresetModel>(
      context,
      MaterialPageRoute(builder: (context) => const ModelPickerScreen()),
    );

    if (model != null) {
      // Модель уже загружена через ModelPickerScreen
      setState(() {
        _isModelLoaded = true;
        _addSystemMessage(
          'Модель ${model.name} готова к использованию!\n'
          'Источник: ${model.source.displayName}\n'
          'Размер: ${model.size}',
        );
      });
    }
  }

  /// Загрузка скачанной модели
  Future<void> _loadDownloadedModel(String modelId) async {
    try {
      setState(() {
        _addSystemMessage('Поиск модели $modelId...');
      });

      // Получаем путь к модели
      final modelPath = await ModelDownloader.getModelPath(
        modelId,
        'adapter_model.safetensors',
      );

      if (modelPath != null) {
        _modelPath = modelPath;
        await _loadModel();
      } else {
        _addSystemMessage(
          'Модель не найдена. Пожалуйста, скачайте её сначала.',
        );
      }
    } catch (e) {
      _addSystemMessage('Ошибка загрузки модели: $e');
    }
  }

  /// Загрузка модели из assets
  Future<void> _loadModelFromAssets() async {
    try {
      final documentsDir = await getApplicationDocumentsDirectory();
      final modelFile = File(
        '${documentsDir.path}/shridhar_8k_multimodal.gguf',
      );

      // Копируем модель из assets, если её нет
      if (!await modelFile.exists()) {
        setState(() {
          _addSystemMessage('Копирую модель из assets...');
        });

        final byteData = await rootBundle.load(
          'assets/models/braindler-q2_k.gguf',
        );
        await modelFile.writeAsBytes(byteData.buffer.asUint8List());
      }

      _modelPath = modelFile.path;
      await _loadModel();
    } catch (e) {
      _addSystemMessage(
        'Ошибка загрузки модели: $e\nИспользуйте кнопку "Менеджер моделей" для скачивания моделей с Hugging Face.',
      );
    }
  }

  /// Загрузка модели
  Future<void> _loadModel() async {
    try {
      setState(() {
        _addSystemMessage('Загружаю мультимодальную модель Shridhar...');
      });

      final config = LlamaConfig(
        modelPath: _modelPath,
        nThreads: 8,
        nGpuLayers: -1, // Все слои на GPU
        contextSize: 8192, // 8K контекст
        batchSize: 512,
        useGpu: true,
        verbose: false,
      );

      final success = await _llama.loadModel(config);

      if (success) {
        final info = await _llama.getModelInfo();
        setState(() {
          _isModelLoaded = true;
          _addSystemMessage(
            'Модель Shridhar 8K Multimodal загружена успешно!\n'
            'Параметры: ${info?['nParams'] ?? 'N/A'}\n'
            'Слои: ${info?['nLayers'] ?? 'N/A'}\n'
            'Контекст: ${info?['contextSize'] ?? 'N/A'} токенов\n\n'
            'Поддерживаемые языки: 🇷🇺 Русский, 🇪🇸 Испанский, 🇮🇳 Хинди, 🇹🇭 Тайский\n'
            'Категории: ИКАРОС, Джив Джаго, Love Destiny, Медитация, Йога',
          );
        });
      } else {
        _addSystemMessage('Не удалось загрузить модель');
      }
    } catch (e) {
      _addSystemMessage('Ошибка: $e');
    }
  }

  void _addSystemMessage(String text) {
    setState(() {
      _messages.add(Message(text: text, isUser: false));
    });
    _scrollToBottom();
  }

  void _scrollToBottom() {
    Future.delayed(const Duration(milliseconds: 100), () {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  /// Выбор изображений
  Future<void> _pickImages() async {
    try {
      FilePickerResult? result = await FilePicker.platform.pickFiles(
        type: FileType.image,
        allowMultiple: true,
      );

      if (result != null) {
        setState(() {
          _selectedImages.addAll(
            result.files
                .map((file) => file.path!)
                .where((path) => path.isNotEmpty),
          );
        });
      }
    } catch (e) {
      _addSystemMessage('Ошибка выбора изображений: $e');
    }
  }

  /// Удаление изображения
  void _removeImage(int index) {
    setState(() {
      _selectedImages.removeAt(index);
    });
  }

  /// Отправка сообщения
  Future<void> _sendMessage() async {
    if (!_isModelLoaded) {
      _addSystemMessage('Пожалуйста, дождитесь загрузки модели');
      return;
    }

    final text = _messageController.text.trim();
    if (text.isEmpty && _selectedImages.isEmpty) {
      return;
    }

    // Добавляем сообщение пользователя
    final userMessage = Message(
      text: text.isEmpty ? '[Изображение]' : text,
      isUser: true,
      imagePaths: _selectedImages.isNotEmpty
          ? List.from(_selectedImages)
          : null,
    );

    setState(() {
      _messages.add(userMessage);
      _messageController.clear();
      _isGenerating = true;
      _currentResponse = '';
    });

    _scrollToBottom();

    // Формируем промпт с учётом мультимодальности
    String prompt = text;
    if (_selectedImages.isNotEmpty) {
      prompt = '[IMAGE] $text';
    }

    try {
      final params = GenerationParams(
        prompt: prompt,
        temperature: 0.7,
        topP: 0.9,
        topK: 40,
        maxTokens: 512,
        repeatPenalty: 1.1,
      );

      // Генерация с потоковым выводом
      final assistantMessage = Message(text: '', isUser: false);
      setState(() {
        _messages.add(assistantMessage);
        _selectedImages.clear();
      });

      await for (final token in _llama.generateStream(params)) {
        setState(() {
          _currentResponse += token;
          _messages[_messages.length - 1] = Message(
            text: _currentResponse,
            isUser: false,
          );
        });

        _scrollToBottom();
      }
    } catch (e) {
      setState(() {
        _messages.add(Message(text: 'Ошибка генерации: $e', isUser: false));
      });
    } finally {
      setState(() {
        _isGenerating = false;
        _currentResponse = '';
      });
    }
  }

  /// Очистка чата
  void _clearChat() {
    setState(() {
      _messages.clear();
      _addSystemMessage('Чат очищен');
    });
  }

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    final bgColor = isDark ? const Color(0xFF343541) : Colors.white;
    final userBubbleColor = isDark
        ? const Color(0xFF10A37F)
        : const Color(0xFF10A37F);
    final aiBubbleColor = isDark
        ? const Color(0xFF444654)
        : const Color(0xFFF7F7F8);

    return Scaffold(
      appBar: AppBar(
        title: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 32,
              height: 32,
              decoration: BoxDecoration(
                color: const Color(0xFF10A37F),
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Icon(
                Icons.auto_awesome,
                color: Colors.white,
                size: 20,
              ),
            ),
            const SizedBox(width: 12),
            const Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  'Shridhar Multimodal',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
                ),
                Text(
                  '8K Context',
                  style: TextStyle(fontSize: 10, color: Colors.grey),
                ),
              ],
            ),
          ],
        ),
        actions: [
          IconButton(
            icon: const Icon(Icons.download),
            onPressed: _openModelPicker,
            tooltip: '🦙 Скачать модель (HF/Ollama)',
          ),
          IconButton(
            icon: const Icon(Icons.cloud_download_outlined),
            onPressed: _openModelManager,
            tooltip: 'Менеджер моделей',
          ),
          if (_isModelLoaded)
            IconButton(
              icon: const Icon(Icons.delete_outline),
              onPressed: _clearChat,
              tooltip: 'Очистить чат',
            ),
          IconButton(
            icon: Icon(_isModelLoaded ? Icons.check_circle : Icons.file_open),
            color: _isModelLoaded ? Colors.green : Colors.grey,
            onPressed: _isModelLoaded ? null : _pickModel,
            tooltip: _isModelLoaded ? 'Модель загружена' : 'Выбрать модель',
          ),
        ],
      ),
      body: Column(
        children: [
          // Список сообщений
          Expanded(
            child: _messages.isEmpty
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Container(
                          width: 80,
                          height: 80,
                          decoration: BoxDecoration(
                            color: const Color(0xFF10A37F).withOpacity(0.1),
                            borderRadius: BorderRadius.circular(20),
                          ),
                          child: const Icon(
                            Icons.auto_awesome,
                            size: 40,
                            color: Color(0xFF10A37F),
                          ),
                        ),
                        const SizedBox(height: 24),
                        const Text(
                          'Shridhar 8K Multimodal',
                          style: TextStyle(
                            fontSize: 24,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          'Мультиязычная духовная модель',
                          style: TextStyle(
                            fontSize: 14,
                            color: Colors.grey[600],
                          ),
                        ),
                        const SizedBox(height: 32),
                        Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 48),
                          child: Wrap(
                            spacing: 12,
                            runSpacing: 12,
                            alignment: WrapAlignment.center,
                            children: [
                              _buildFeatureChip('🇷🇺 Русский', isDark),
                              _buildFeatureChip('🇪🇸 Испанский', isDark),
                              _buildFeatureChip('🇮🇳 Хинди', isDark),
                              _buildFeatureChip('🇹🇭 Тайский', isDark),
                              _buildFeatureChip('🧘 Медитация', isDark),
                              _buildFeatureChip('🎵 ИКАРОС', isDark),
                              _buildFeatureChip('🎬 Love Destiny', isDark),
                            ],
                          ),
                        ),
                      ],
                    ),
                  )
                : ListView.builder(
                    controller: _scrollController,
                    padding: const EdgeInsets.all(16),
                    itemCount: _messages.length,
                    itemBuilder: (context, index) {
                      final message = _messages[index];
                      return _buildMessageBubble(
                        message,
                        userBubbleColor,
                        aiBubbleColor,
                        isDark,
                      );
                    },
                  ),
          ),

          // Предпросмотр выбранных изображений
          if (_selectedImages.isNotEmpty)
            Container(
              height: 100,
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              decoration: BoxDecoration(
                color: aiBubbleColor,
                border: Border(
                  top: BorderSide(color: Colors.grey.withOpacity(0.2)),
                ),
              ),
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: _selectedImages.length,
                itemBuilder: (context, index) {
                  return Padding(
                    padding: const EdgeInsets.only(right: 8),
                    child: Stack(
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.circular(8),
                          child: Image.file(
                            File(_selectedImages[index]),
                            width: 80,
                            height: 80,
                            fit: BoxFit.cover,
                          ),
                        ),
                        Positioned(
                          top: 4,
                          right: 4,
                          child: GestureDetector(
                            onTap: () => _removeImage(index),
                            child: Container(
                              padding: const EdgeInsets.all(4),
                              decoration: const BoxDecoration(
                                color: Colors.black54,
                                shape: BoxShape.circle,
                              ),
                              child: const Icon(
                                Icons.close,
                                size: 16,
                                color: Colors.white,
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                  );
                },
              ),
            ),

          // Поле ввода
          Container(
            decoration: BoxDecoration(
              color: bgColor,
              border: Border(
                top: BorderSide(color: Colors.grey.withOpacity(0.2)),
              ),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            child: Row(
              children: [
                // Кнопка добавления изображения
                IconButton(
                  icon: const Icon(Icons.add_photo_alternate),
                  onPressed: _isGenerating ? null : _pickImages,
                  color: const Color(0xFF10A37F),
                ),
                const SizedBox(width: 8),

                // Текстовое поле
                Expanded(
                  child: Container(
                    decoration: BoxDecoration(
                      color: aiBubbleColor,
                      borderRadius: BorderRadius.circular(24),
                    ),
                    child: TextField(
                      controller: _messageController,
                      maxLines: null,
                      enabled: !_isGenerating && _isModelLoaded,
                      decoration: const InputDecoration(
                        hintText: 'Напишите сообщение...',
                        border: InputBorder.none,
                        contentPadding: EdgeInsets.symmetric(
                          horizontal: 20,
                          vertical: 12,
                        ),
                      ),
                      onSubmitted: (_) => _sendMessage(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),

                // Кнопка отправки
                Container(
                  decoration: BoxDecoration(
                    color: _isGenerating
                        ? Colors.grey
                        : const Color(0xFF10A37F),
                    shape: BoxShape.circle,
                  ),
                  child: IconButton(
                    icon: Icon(
                      _isGenerating ? Icons.stop : Icons.send,
                      color: Colors.white,
                    ),
                    onPressed: !_isModelLoaded || _isGenerating
                        ? null
                        : _sendMessage,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildFeatureChip(String label, bool isDark) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: isDark ? const Color(0xFF444654) : const Color(0xFFF7F7F8),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: Colors.grey.withOpacity(0.2)),
      ),
      child: Text(label, style: const TextStyle(fontSize: 12)),
    );
  }

  Widget _buildMessageBubble(
    Message message,
    Color userBubbleColor,
    Color aiBubbleColor,
    bool isDark,
  ) {
    final isUser = message.isUser;
    final hasImages =
        message.imagePaths != null && message.imagePaths!.isNotEmpty;

    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: Row(
        mainAxisAlignment: isUser
            ? MainAxisAlignment.end
            : MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (!isUser) ...[
            Container(
              width: 32,
              height: 32,
              decoration: BoxDecoration(
                color: const Color(0xFF10A37F),
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Icon(
                Icons.auto_awesome,
                color: Colors.white,
                size: 16,
              ),
            ),
            const SizedBox(width: 12),
          ],
          Flexible(
            child: Column(
              crossAxisAlignment: isUser
                  ? CrossAxisAlignment.end
                  : CrossAxisAlignment.start,
              children: [
                if (hasImages)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 8),
                    child: Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: message.imagePaths!.map((imagePath) {
                        return ClipRRect(
                          borderRadius: BorderRadius.circular(12),
                          child: Image.file(
                            File(imagePath),
                            width: 200,
                            height: 200,
                            fit: BoxFit.cover,
                          ),
                        );
                      }).toList(),
                    ),
                  ),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 12,
                  ),
                  decoration: BoxDecoration(
                    color: isUser ? userBubbleColor : aiBubbleColor,
                    borderRadius: BorderRadius.circular(18),
                  ),
                  child: Text(
                    message.text,
                    style: TextStyle(
                      color: isUser ? Colors.white : null,
                      fontSize: 15,
                      height: 1.4,
                    ),
                  ),
                ),
              ],
            ),
          ),
          if (isUser) ...[
            const SizedBox(width: 12),
            Container(
              width: 32,
              height: 32,
              decoration: BoxDecoration(
                color: Colors.grey[600],
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Icon(Icons.person, color: Colors.white, size: 16),
            ),
          ],
        ],
      ),
    );
  }
}
12
likes
140
points
600
downloads

Documentation

API reference

Publisher

verified publisherai.nativemind.net

Weekly Downloads

Flutter plugin for running LLM inference with llama.cpp and GGUF models on Android and iOS

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, http, path, path_provider, plugin_platform_interface

More

Packages that depend on flutter_llama

Packages that implement flutter_llama