flutter_local_llm 1.0.0 copy "flutter_local_llm: ^1.0.0" to clipboard
flutter_local_llm: ^1.0.0 copied to clipboard

High-performance, production-grade Flutter plugin for on-device local LLM inference using llama.cpp via Dart FFI and Native Assets.

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_local_llm/flutter_local_llm.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const LocalLlmDemoApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Local LLM Studio',
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.dark,
      darkTheme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xFF0B0F19),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF6366F1),
          secondary: Color(0xFF06B6D4),
          surface: Color(0xFF131B2E),
        ),
        appBarTheme: const AppBarTheme(
          backgroundColor: Color(0xFF0B0F19),
          elevation: 0,
          scrolledUnderElevation: 0,
        ),
      ),
      home: const MainStudioScreen(),
    );
  }
}

class GgufModelPreset {
  final String name;
  final String parameterSize;
  final String quantization;
  final String fileSize;
  final String url;
  final String filename;
  final ChatTemplate template;
  final String? sha256;

  const GgufModelPreset({
    required this.name,
    required this.parameterSize,
    required this.quantization,
    required this.fileSize,
    required this.url,
    required this.filename,
    required this.template,
    this.sha256,
  });
}

const List<GgufModelPreset> kModelPresets = [
  GgufModelPreset(
    name: 'SmolLM-135M Instruct',
    parameterSize: '135M',
    quantization: 'Q4_K_M',
    fileSize: '89 MB',
    url: 'https://huggingface.co/HuggingFaceTB/SmolLM-135M-Instruct-GGUF/resolve/main/smollm-135m-instruct-q4_k_m.gguf',
    filename: 'smollm-135m-instruct-q4_k_m.gguf',
    template: ChatMlTemplate(),
  ),
  GgufModelPreset(
    name: 'Qwen2.5-0.5B Instruct',
    parameterSize: '0.5B',
    quantization: 'Q4_K_M',
    fileSize: '390 MB',
    url: 'https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf',
    filename: 'qwen2.5-0.5b-instruct-q4_k_m.gguf',
    template: ChatMlTemplate(),
  ),
  GgufModelPreset(
    name: 'TinyLlama 1.1B Chat',
    parameterSize: '1.1B',
    quantization: 'Q4_K_M',
    fileSize: '669 MB',
    url: 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf',
    filename: 'tinyllama-1.1b-chat-q4_k_m.gguf',
    template: ChatMlTemplate(),
  ),
];

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

  @override
  State<MainStudioScreen> createState() => _MainStudioScreenState();
}

class _MainStudioScreenState extends State<MainStudioScreen> {
  int _currentIndex = 0;

  // Engine state
  LocalLlmEngine? _engine;
  LlmSession? _session;
  String? _loadedModelName;
  bool _isLoadingModel = false;

  // Hyperparameters
  double _temperature = 0.7;
  double _topP = 0.9;
  int _maxTokens = 512;
  int _gpuLayers = 99;
  int _contextSize = 2048;
  bool _jsonGrammarMode = false;
  ChatTemplate _activeTemplate = const ChatMlTemplate();

  // Chat UI state
  final List<ChatMessage> _messages = [];
  final TextEditingController _promptController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  String _currentStreamResponse = '';
  bool _isGenerating = false;

  // Telemetry HUD
  double _lastTokensPerSec = 0.0;
  Duration _lastTtft = Duration.zero;
  int _lastTokenCount = 0;

  // Downloader state
  ModelDownloader? _downloader;
  DownloadProgress? _downloadProgress;
  String? _activeDownloadingFilename;

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

  @override
  void dispose() {
    _downloader?.dispose();
    _session?.dispose();
    _engine?.dispose();
    _promptController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  Future<String> _getModelsDirectory() async {
    final docDir = await getApplicationDocumentsDirectory();
    final dir = Directory('${docDir.path}/gguf_models');
    if (!await dir.exists()) {
      await dir.create(recursive: true);
    }
    return dir.path;
  }

  Future<void> _initDefaultModelIfAvailable() async {
    final dirPath = await _getModelsDirectory();
    for (final preset in kModelPresets) {
      final file = File('$dirPath/${preset.filename}');
      if (await file.exists()) {
        await _loadModel(file.path, preset.name, preset.template);
        break;
      }
    }
  }

  Future<void> _loadModel(String path, String name, ChatTemplate template) async {
    setState(() {
      _isLoadingModel = true;
    });

    try {
      _session?.dispose();
      _engine?.dispose();

      final engine = await LocalLlmEngine.loadModel(
        modelPath: path,
        params: ModelParams(
          contextSize: _contextSize,
          gpuLayers: _gpuLayers,
        ),
      );

      final session = engine.createSession(
        defaultTemplate: template,
      );

      setState(() {
        _engine = engine;
        _session = session;
        _loadedModelName = name;
        _activeTemplate = template;
      });

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Successfully loaded $name (Hardware Acceleration Active)'),
            backgroundColor: const Color(0xFF10B981),
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Failed to load model: $e'),
            backgroundColor: const Color(0xFFEF4444),
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() {
          _isLoadingModel = false;
        });
      }
    }
  }

  Future<void> _startDownload(GgufModelPreset preset) async {
    final dirPath = await _getModelsDirectory();
    final destPath = '$dirPath/${preset.filename}';

    _downloader = ModelDownloader();
    setState(() {
      _activeDownloadingFilename = preset.filename;
    });

    final stream = _downloader!.download(
      url: preset.url,
      destinationPath: destPath,
      expectedSha256: preset.sha256,
      allowResume: true,
    );

    stream.listen(
      (progress) {
        setState(() {
          _downloadProgress = progress;
        });
        if (progress.status == DownloadStatus.completed) {
          _loadModel(destPath, preset.name, preset.template);
        }
      },
      onError: (e) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Download failed: $e'),
              backgroundColor: const Color(0xFFEF4444),
            ),
          );
        }
      },
    );
  }

  void _sendMessage() {
    final text = _promptController.text.trim();
    if (text.isEmpty || _session == null || _isGenerating) return;

    final userMessage = ChatMessage.user(text);
    setState(() {
      _messages.add(userMessage);
      _currentStreamResponse = '';
      _isGenerating = true;
      _promptController.clear();
    });

    _scrollToBottom();

    String? grammar;
    if (_jsonGrammarMode) {
      grammar = GrammarHelper.genericJsonGrammar();
    }

    final stream = _session!.chat(
      [userMessage],
      params: SamplingParams(
        temperature: _temperature,
        topP: _topP,
        maxTokens: _maxTokens,
      ),
      template: _activeTemplate,
      jsonSchemaGrammar: grammar,
      onMetrics: (metrics) {
        setState(() {
          _lastTokensPerSec = metrics.tokensPerSecond;
          _lastTtft = metrics.timeToFirstToken;
          _lastTokenCount = metrics.totalTokens;
        });
      },
    );

    stream.listen(
      (token) {
        setState(() {
          _currentStreamResponse += token;
        });
        _scrollToBottom();
      },
      onError: (err) {
        setState(() {
          _isGenerating = false;
          _messages.add(ChatMessage.assistant('Error: $err'));
        });
      },
      onDone: () {
        setState(() {
          _messages.add(ChatMessage.assistant(_currentStreamResponse));
          _currentStreamResponse = '';
          _isGenerating = false;
        });
        _scrollToBottom();
      },
    );
  }

  void _cancelGeneration() {
    _session?.cancel();
    setState(() {
      if (_currentStreamResponse.isNotEmpty) {
        _messages.add(ChatMessage.assistant('$_currentStreamResponse [Cancelled]'));
        _currentStreamResponse = '';
      }
      _isGenerating = false;
    });
  }

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent + 100,
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOut,
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            Container(
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: const Color(0xFF6366F1).withValues(alpha: 0.2),
                borderRadius: BorderRadius.circular(10),
                border: Border.all(color: const Color(0xFF6366F1).withValues(alpha: 0.4)),
              ),
              child: const Icon(Icons.psychology, color: Color(0xFF818CF8), size: 20),
            ),
            const SizedBox(width: 12),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Local LLM Studio',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
                ),
                Text(
                  _loadedModelName ?? 'No model loaded',
                  style: TextStyle(
                    fontSize: 12,
                    color: _loadedModelName != null ? const Color(0xFF34D399) : Colors.grey,
                  ),
                ),
              ],
            ),
          ],
        ),
        actions: [
          if (_loadedModelName != null)
            Padding(
              padding: const EdgeInsets.only(right: 8.0),
              child: Chip(
                avatar: const Icon(Icons.bolt, color: Color(0xFFF59E0B), size: 16),
                label: Text(
                  _lastTokensPerSec > 0 ? '${_lastTokensPerSec.toStringAsFixed(1)} t/s' : 'Ready',
                  style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
                ),
                backgroundColor: const Color(0xFF1E293B),
                side: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
              ),
            ),
        ],
      ),
      body: IndexedStack(
        index: _currentIndex,
        children: [
          _buildChatView(),
          _buildModelHubView(),
          _buildSettingsView(),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) => setState(() => _currentIndex = index),
        backgroundColor: const Color(0xFF0F172A),
        indicatorColor: const Color(0xFF6366F1).withValues(alpha: 0.3),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.chat_bubble_outline),
            selectedIcon: Icon(Icons.chat_bubble, color: Color(0xFF818CF8)),
            label: 'Chat',
          ),
          NavigationDestination(
            icon: Icon(Icons.cloud_download_outlined),
            selectedIcon: Icon(Icons.cloud_download, color: Color(0xFF818CF8)),
            label: 'Model Hub',
          ),
          NavigationDestination(
            icon: Icon(Icons.tune_outlined),
            selectedIcon: Icon(Icons.tune, color: Color(0xFF818CF8)),
            label: 'Parameters',
          ),
        ],
      ),
    );
  }

  Widget _buildChatView() {
    return Column(
      children: [
        // Live Telemetry HUD Bar
        if (_loadedModelName != null) _buildTelemetryHud(),

        // Messages List
        Expanded(
          child: _messages.isEmpty && _currentStreamResponse.isEmpty
              ? _buildEmptyState()
              : ListView.builder(
                  controller: _scrollController,
                  padding: const EdgeInsets.all(16),
                  itemCount: _messages.length + (_currentStreamResponse.isNotEmpty ? 1 : 0),
                  itemBuilder: (context, index) {
                    if (index < _messages.length) {
                      return _buildMessageBubble(_messages[index]);
                    } else {
                      return _buildStreamingBubble();
                    }
                  },
                ),
        ),

        // Input Area
        _buildInputBar(),
      ],
    );
  }

  Widget _buildTelemetryHud() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: const Color(0xFF131B2E),
        border: Border(bottom: BorderSide(color: Colors.white.withValues(alpha: 0.05))),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          _buildHudMetric('Speed', '${_lastTokensPerSec.toStringAsFixed(1)} tok/s', Icons.speed),
          _buildHudMetric('TTFT', '${_lastTtft.inMilliseconds} ms', Icons.timer_outlined),
          _buildHudMetric('Tokens', '$_lastTokenCount eval', Icons.generating_tokens_outlined),
          _buildHudMetric(
            'Format',
            _jsonGrammarMode ? 'JSON Mode' : _activeTemplate.name,
            Icons.code,
          ),
        ],
      ),
    );
  }

  Widget _buildHudMetric(String label, String value, IconData icon) {
    return Row(
      children: [
        Icon(icon, size: 14, color: const Color(0xFF818CF8)),
        const SizedBox(width: 4),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
            Text(
              value,
              style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildEmptyState() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              padding: const EdgeInsets.all(24),
              decoration: BoxDecoration(
                color: const Color(0xFF1E293B).withValues(alpha: 0.6),
                shape: BoxShape.circle,
                border: Border.all(color: const Color(0xFF6366F1).withValues(alpha: 0.3)),
              ),
              child: const Icon(Icons.auto_awesome, color: Color(0xFF818CF8), size: 48),
            ),
            const SizedBox(height: 24),
            Text(
              _loadedModelName != null ? 'Ready for On-Device Chat' : 'Download a Model to Start',
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white),
            ),
            const SizedBox(height: 8),
            Text(
              _loadedModelName != null
                  ? 'Private, zero-latency inference running locally with GPU acceleration.'
                  : 'Head over to the Model Hub tab to download a high-performance GGUF model.',
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 14, color: Colors.grey),
            ),
            if (_loadedModelName != null) ...[
              const SizedBox(height: 24),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                alignment: WrapAlignment.center,
                children: [
                  _buildQuickPromptChip('Explain quantum computing simply'),
                  _buildQuickPromptChip('Write a Flutter Riverpod counter'),
                  _buildQuickPromptChip('Generate JSON user profile'),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildQuickPromptChip(String prompt) {
    return ActionChip(
      label: Text(prompt, style: const TextStyle(fontSize: 12, color: Colors.white70)),
      backgroundColor: const Color(0xFF1E293B),
      side: BorderSide(color: const Color(0xFF6366F1).withValues(alpha: 0.3)),
      onPressed: () {
        _promptController.text = prompt;
        _sendMessage();
      },
    );
  }

  Widget _buildMessageBubble(ChatMessage message) {
    final isUser = message.role == ChatMessageRole.user;
    return Align(
      alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 6),
        padding: const EdgeInsets.all(14),
        constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
        decoration: BoxDecoration(
          color: isUser ? const Color(0xFF6366F1) : const Color(0xFF1E293B),
          borderRadius: BorderRadius.only(
            topLeft: const Radius.circular(16),
            topRight: const Radius.circular(16),
            bottomLeft: Radius.circular(isUser ? 16 : 4),
            bottomRight: Radius.circular(isUser ? 4 : 16),
          ),
          border: Border.all(
            color: isUser
                ? Colors.transparent
                : Colors.white.withValues(alpha: 0.08),
          ),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(
                  isUser ? Icons.person : Icons.smart_toy,
                  size: 14,
                  color: isUser ? Colors.white70 : const Color(0xFF818CF8),
                ),
                const SizedBox(width: 6),
                Text(
                  isUser ? 'You' : (_loadedModelName ?? 'Assistant'),
                  style: TextStyle(
                    fontSize: 11,
                    fontWeight: FontWeight.bold,
                    color: isUser ? Colors.white70 : const Color(0xFF818CF8),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 6),
            SelectableText(
              message.content,
              style: const TextStyle(fontSize: 14, height: 1.4, color: Colors.white),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStreamingBubble() {
    return Align(
      alignment: Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 6),
        padding: const EdgeInsets.all(14),
        constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
        decoration: BoxDecoration(
          color: const Color(0xFF1E293B),
          borderRadius: const BorderRadius.only(
            topLeft: Radius.circular(16),
            topRight: Radius.circular(16),
            bottomLeft: Radius.circular(4),
            bottomRight: Radius.circular(16),
          ),
          border: Border.all(color: const Color(0xFF6366F1).withValues(alpha: 0.4)),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const SizedBox(
                  width: 12,
                  height: 12,
                  child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF818CF8)),
                ),
                const SizedBox(width: 8),
                Text(
                  _loadedModelName ?? 'Generating...',
                  style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF818CF8)),
                ),
              ],
            ),
            const SizedBox(height: 6),
            SelectableText(
              _currentStreamResponse,
              style: const TextStyle(fontSize: 14, height: 1.4, color: Colors.white),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInputBar() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: const Color(0xFF0F172A),
        border: Border(top: BorderSide(color: Colors.white.withValues(alpha: 0.08))),
      ),
      child: SafeArea(
        child: Row(
          children: [
            if (_isGenerating)
              IconButton(
                icon: const Icon(Icons.stop_circle, color: Color(0xFFEF4444)),
                tooltip: 'Stop generation',
                onPressed: _cancelGeneration,
              )
            else
              IconButton(
                icon: const Icon(Icons.delete_outline, color: Colors.grey),
                tooltip: 'Clear history',
                onPressed: () {
                  _session?.clearHistory();
                  setState(() => _messages.clear());
                },
              ),
            Expanded(
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                decoration: BoxDecoration(
                  color: const Color(0xFF1E293B),
                  borderRadius: BorderRadius.circular(24),
                  border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
                ),
                child: TextField(
                  controller: _promptController,
                  decoration: const InputDecoration(
                    hintText: 'Type a message or instruction...',
                    hintStyle: TextStyle(color: Colors.grey, fontSize: 14),
                    border: InputBorder.none,
                  ),
                  style: const TextStyle(color: Colors.white, fontSize: 14),
                  onSubmitted: (_) => _sendMessage(),
                ),
              ),
            ),
            const SizedBox(width: 8),
            IconButton.filled(
              icon: Icon(_isGenerating ? Icons.hourglass_top : Icons.arrow_upward),
              style: IconButton.styleFrom(
                backgroundColor: const Color(0xFF6366F1),
                foregroundColor: Colors.white,
              ),
              onPressed: _isGenerating || _session == null ? null : _sendMessage,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildModelHubView() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        const Text(
          'Curated GGUF Models',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
        ),
        const SizedBox(height: 8),
        const Text(
          'Optimized lightweight models ready for on-device inference with zero cloud dependency.',
          style: TextStyle(fontSize: 13, color: Colors.grey),
        ),
        const SizedBox(height: 16),
        ...kModelPresets.map((preset) => _buildPresetCard(preset)),
      ],
    );
  }

  Widget _buildPresetCard(GgufModelPreset preset) {
    final isLoaded = _loadedModelName == preset.name;
    final isDownloading = _activeDownloadingFilename == preset.filename &&
        _downloadProgress?.status == DownloadStatus.downloading;

    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: const Color(0xFF131B2E),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: isLoaded
              ? const Color(0xFF10B981)
              : Colors.white.withValues(alpha: 0.08),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                preset.name,
                style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
              ),
              if (isLoaded)
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: const Color(0xFF10B981).withValues(alpha: 0.2),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: const Text(
                    'Active Model',
                    style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xFF34D399)),
                  ),
                ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              _buildBadge(preset.parameterSize, const Color(0xFF818CF8)),
              const SizedBox(width: 6),
              _buildBadge(preset.quantization, const Color(0xFF06B6D4)),
              const SizedBox(width: 6),
              _buildBadge(preset.fileSize, const Color(0xFFF59E0B)),
            ],
          ),
          const SizedBox(height: 12),
          if (isDownloading && _downloadProgress != null) ...[
            ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: LinearProgressIndicator(
                value: _downloadProgress!.progress,
                backgroundColor: const Color(0xFF1E293B),
                color: const Color(0xFF6366F1),
              ),
            ),
            const SizedBox(height: 6),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  _downloadProgress!.downloadedFormatted,
                  style: const TextStyle(fontSize: 11, color: Colors.grey),
                ),
                Text(
                  _downloadProgress!.speedFormatted,
                  style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF818CF8)),
                ),
              ],
            ),
          ] else
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                ElevatedButton.icon(
                  icon: Icon(isLoaded ? Icons.check : Icons.download),
                  label: Text(isLoaded ? 'Loaded' : 'Download & Run'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: isLoaded ? const Color(0xFF10B981) : const Color(0xFF6366F1),
                    foregroundColor: Colors.white,
                  ),
                  onPressed: isLoaded || _isLoadingModel ? null : () => _startDownload(preset),
                ),
              ],
            ),
        ],
      ),
    );
  }

  Widget _buildBadge(String text, Color color) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.15),
        borderRadius: BorderRadius.circular(6),
      ),
      child: Text(
        text,
        style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color),
      ),
    );
  }

  Widget _buildSettingsView() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        const Text(
          'Inference Parameters',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
        ),
        const SizedBox(height: 16),
        _buildSliderTile(
          title: 'Temperature',
          subtitle: 'Controls response creativity vs determinism',
          value: _temperature,
          min: 0.0,
          max: 1.5,
          divisions: 15,
          onChanged: (val) => setState(() => _temperature = val),
        ),
        _buildSliderTile(
          title: 'Top-P Sampling',
          subtitle: 'Nucleus sampling threshold',
          value: _topP,
          min: 0.1,
          max: 1.0,
          divisions: 9,
          onChanged: (val) => setState(() => _topP = val),
        ),
        _buildSliderTile(
          title: 'Max Output Tokens',
          subtitle: 'Maximum token limit for reply generation',
          value: _maxTokens.toDouble(),
          min: 64,
          max: 2048,
          divisions: 31,
          onChanged: (val) => setState(() => _maxTokens = val.toInt()),
        ),
        const Divider(color: Colors.white12, height: 32),
        const Text(
          'Hardware & Acceleration',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
        ),
        const SizedBox(height: 16),
        SwitchListTile(
          title: const Text('Structured JSON Mode (GBNF Grammar)'),
          subtitle: const Text('Enforces strict valid JSON output adhering to GBNF schema'),
          value: _jsonGrammarMode,
          activeThumbColor: const Color(0xFF6366F1),
          onChanged: (val) => setState(() => _jsonGrammarMode = val),
        ),
        ListTile(
          title: const Text('Context Window Size'),
          subtitle: Text('$_contextSize tokens'),
          trailing: DropdownButton<int>(
            value: _contextSize,
            dropdownColor: const Color(0xFF1E293B),
            items: const [
              DropdownMenuItem(value: 2048, child: Text('2048 tokens')),
              DropdownMenuItem(value: 4096, child: Text('4096 tokens')),
              DropdownMenuItem(value: 8192, child: Text('8192 tokens')),
            ],
            onChanged: (val) {
              if (val != null) setState(() => _contextSize = val);
            },
          ),
        ),
        ListTile(
          title: const Text('GPU Layer Offloading'),
          subtitle: Text('$_gpuLayers layers to Metal/Vulkan (99 = All Layers)'),
          trailing: DropdownButton<int>(
            value: _gpuLayers,
            dropdownColor: const Color(0xFF1E293B),
            items: const [
              DropdownMenuItem(value: 99, child: Text('Full GPU (Metal/Vulkan)')),
              DropdownMenuItem(value: 16, child: Text('Hybrid (16 Layers)')),
              DropdownMenuItem(value: 0, child: Text('CPU Only (OpenMP)')),
            ],
            onChanged: (val) {
              if (val != null) setState(() => _gpuLayers = val);
            },
          ),
        ),
      ],
    );
  }

  Widget _buildSliderTile({
    required String title,
    required String subtitle,
    required double value,
    required double min,
    required double max,
    required int divisions,
    required ValueChanged<double> onChanged,
  }) {
    return Container(
      margin: const EdgeInsets.only(bottom: 16),
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: const Color(0xFF131B2E),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)),
              Text(
                value.toStringAsFixed(value is int ? 0 : 2),
                style: const TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF818CF8)),
              ),
            ],
          ),
          const SizedBox(height: 2),
          Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)),
          Slider(
            value: value,
            min: min,
            max: max,
            divisions: divisions,
            activeColor: const Color(0xFF6366F1),
            onChanged: onChanged,
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
148
downloads

Documentation

API reference

Publisher

verified publishertherohitsoni.in

Weekly Downloads

High-performance, production-grade Flutter plugin for on-device local LLM inference using llama.cpp via Dart FFI and Native Assets.

Repository (GitHub)
View/report issues

Topics

#llm #llama-cpp #on-device #ai #ffi

License

MIT (license)

Dependencies

crypto, ffi, flutter, http, meta, path, path_provider

More

Packages that depend on flutter_local_llm

Packages that implement flutter_local_llm