Skip to main content

offline_slm 0.0.1 copy "offline_slm: ^0.0.1" to clipboard
offline_slm: ^0.0.1 copied to clipboard

A fast, privacy-focused Flutter package for running offline Small Language Models (SLMs) and drop-in AI chatbots locally on device.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:offline_slm/offline_slm.dart';

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

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

  @override
  State<OfflineChatApp> createState() => _OfflineChatAppState();
}

class _OfflineChatAppState extends State<OfflineChatApp> {
  ThemeMode _themeMode = ThemeMode.dark;

  void _toggleTheme() {
    setState(() {
      _themeMode =
          _themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Offline SLM Chatbot',
      debugShowCheckedModeBanner: false,
      themeMode: _themeMode,
      theme: ThemeData(
        brightness: Brightness.light,
        colorSchemeSeed: const Color(0xFF2563EB),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        colorSchemeSeed: const Color(0xFF3B82F6),
        scaffoldBackgroundColor: const Color(0xFF121417),
        useMaterial3: true,
      ),
      home: ChatScreen(
        onToggleTheme: _toggleTheme,
        isDark: _themeMode == ThemeMode.dark,
      ),
    );
  }
}

class ChatScreen extends StatefulWidget {
  final VoidCallback onToggleTheme;
  final bool isDark;

  const ChatScreen({
    super.key,
    required this.onToggleTheme,
    required this.isDark,
  });

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

class _ChatScreenState extends State<ChatScreen> {
  late OfflineChatController _chatController;
  DownloadProgress? _downloadProgress;
  ModelPreset _selectedPreset = ModelPresets.smolLM2_360M;
  bool _useSimulation = true;

  @override
  void initState() {
    super.initState();
    _chatController = OfflineChatController(
      engine: MockSLMEngine(),
      systemPrompt:
          'You are an offline AI assistant running locally on the user device. Be helpful, concise, and friendly.',
    );

    // Initialize with default simulation/preset
    _initDefaultEngine();
  }

  Future<void> _initDefaultEngine() async {
    await _chatController.initializeModel(
      const SLMModelConfig(
        modelPath: 'simulated_smollm2_360m.gguf',
        contextSize: 2048,
        threads: 4,
      ),
      modelName: 'SmolLM2 360M (Simulation)',
    );
  }

  @override
  void dispose() {
    _chatController.dispose();
    super.dispose();
  }

  void _showModelPickerSheet() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
      ),
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setSheetState) {
            return SafeArea(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Select Offline Model',
                          style: TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        IconButton(
                          onPressed: () => Navigator.pop(context),
                          icon: const Icon(Icons.close),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'All models run 100% offline on your device CPU/GPU.',
                      style: TextStyle(fontSize: 13, color: Colors.grey),
                    ),
                    const SizedBox(height: 16),
                    // Simulation Mode Switch
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Simulator Mode (Instant Test)'),
                      subtitle: const Text(
                        'Test full streaming UI without downloading GGUF files',
                      ),
                      value: _useSimulation,
                      onChanged: (val) {
                        setSheetState(() => _useSimulation = val);
                        setState(() => _useSimulation = val);
                      },
                    ),
                    const Divider(),
                    const SizedBox(height: 8),
                    ...ModelPresets.all.map((preset) {
                      final isSelected = _selectedPreset.id == preset.id;
                      return ListTile(
                        contentPadding: EdgeInsets.zero,
                        leading: CircleAvatar(
                          backgroundColor: isSelected
                              ? Theme.of(context).colorScheme.primary
                              : Colors.grey.withAlpha(50),
                          child: Icon(
                            Icons.memory,
                            color: isSelected ? Colors.white : Colors.grey,
                            size: 20,
                          ),
                        ),
                        title: Row(
                          children: [
                            Text(
                              preset.name,
                              style: TextStyle(
                                fontWeight: isSelected
                                    ? FontWeight.bold
                                    : FontWeight.normal,
                              ),
                            ),
                            const SizedBox(width: 8),
                            Container(
                              padding: const EdgeInsets.symmetric(
                                horizontal: 6,
                                vertical: 2,
                              ),
                              decoration: BoxDecoration(
                                color: Colors.blue.withAlpha(30),
                                borderRadius: BorderRadius.circular(4),
                              ),
                              child: Text(
                                preset.formattedSize,
                                style: const TextStyle(
                                  fontSize: 11,
                                  fontWeight: FontWeight.w600,
                                ),
                              ),
                            ),
                          ],
                        ),
                        subtitle: Text(
                          preset.description,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                          style: const TextStyle(fontSize: 12),
                        ),
                        trailing: isSelected
                            ? const Icon(Icons.check_circle, color: Colors.blue)
                            : null,
                        onTap: () async {
                          Navigator.pop(context);
                          _selectAndLoadModel(preset);
                        },
                      );
                    }),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }

  Future<void> _selectAndLoadModel(ModelPreset preset) async {
    setState(() {
      _selectedPreset = preset;
    });

    if (_useSimulation) {
      await _chatController.initializeModel(
        SLMModelConfig(
          modelPath: '${preset.fileName}.sim',
          contextSize: preset.recommendedContextSize,
        ),
        modelName: '${preset.name} (Simulation)',
        template: preset.template,
      );
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Switched to ${preset.name} (Simulation)')),
        );
      }
      return;
    }

    // Real download & load
    try {
      final config = await ModelManager.prepareModelConfig(
        preset,
        onProgress: (progress) {
          setState(() {
            _downloadProgress = progress;
          });
        },
      );

      await _chatController.initializeModel(
        config,
        modelName: preset.name,
        template: preset.template,
      );

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${preset.name} loaded successfully!')),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error loading model: $e')),
        );
      }
    } finally {
      setState(() {
        _downloadProgress = null;
      });
    }
  }

  void _showSettingsDialog() {
    final systemPromptCtrl =
        TextEditingController(text: _chatController.systemPrompt);
    double temp = _chatController.generationConfig.temperature;
    double topP = _chatController.generationConfig.topP;
    int maxTokens = _chatController.generationConfig.maxTokens;

    showDialog(
      context: context,
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setDialogState) {
            return AlertDialog(
              title: const Text('Chat & Model Settings'),
              content: SingleChildScrollView(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'System Prompt',
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
                    ),
                    const SizedBox(height: 6),
                    TextField(
                      controller: systemPromptCtrl,
                      maxLines: 3,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        isDense: true,
                      ),
                    ),
                    const SizedBox(height: 16),
                    Text(
                      'Temperature: ${temp.toStringAsFixed(2)}',
                      style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
                    ),
                    Slider(
                      value: temp,
                      min: 0.0,
                      max: 1.5,
                      divisions: 15,
                      label: temp.toStringAsFixed(2),
                      onChanged: (v) => setDialogState(() => temp = v),
                    ),
                    Text(
                      'Top-P: ${topP.toStringAsFixed(2)}',
                      style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
                    ),
                    Slider(
                      value: topP,
                      min: 0.1,
                      max: 1.0,
                      divisions: 9,
                      label: topP.toStringAsFixed(2),
                      onChanged: (v) => setDialogState(() => topP = v),
                    ),
                    Text(
                      'Max Tokens: $maxTokens',
                      style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
                    ),
                    Slider(
                      value: maxTokens.toDouble(),
                      min: 128,
                      max: 2048,
                      divisions: 15,
                      label: '$maxTokens',
                      onChanged: (v) =>
                          setDialogState(() => maxTokens = v.toInt()),
                    ),
                  ],
                ),
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.pop(context),
                  child: const Text('Cancel'),
                ),
                FilledButton(
                  onPressed: () {
                    _chatController.systemPrompt = systemPromptCtrl.text;
                    _chatController.generationConfig =
                        _chatController.generationConfig.copyWith(
                      temperature: temp,
                      topP: topP,
                      maxTokens: maxTokens,
                    );
                    Navigator.pop(context);
                  },
                  child: const Text('Save'),
                ),
              ],
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            const Icon(Icons.bolt, color: Colors.blueAccent),
            const SizedBox(width: 8),
            Text(
              'Offline SLM',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 18,
                color: widget.isDark ? Colors.white : Colors.black87,
              ),
            ),
          ],
        ),
        elevation: 0,
        actions: [
          IconButton(
            icon: Icon(widget.isDark ? Icons.light_mode : Icons.dark_mode),
            tooltip: 'Toggle Theme',
            onPressed: widget.onToggleTheme,
          ),
          IconButton(
            icon: const Icon(Icons.tune),
            tooltip: 'Settings',
            onPressed: _showSettingsDialog,
          ),
        ],
      ),
      body: OfflineChatWidget(
        controller: _chatController,
        theme: widget.isDark ? OfflineChatTheme.dark() : OfflineChatTheme.light(),
        downloadProgress: _downloadProgress,
        onSwitchModel: _showModelPickerSheet,
        suggestions: const [
          'What are the advantages of offline SLMs?',
          'Write a Flutter offline data cache service',
          'Explain quantization (Q4_K_M) simply',
          'Give me 3 mobile privacy tips',
        ],
      ),
    );
  }
}
1
likes
160
points
117
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A fast, privacy-focused Flutter package for running offline Small Language Models (SLMs) and drop-in AI chatbots locally on device.

Repository (GitHub)
View/report issues

Topics

#ai #llm #offline #chatbot #slm

License

MIT (license)

Dependencies

ffi, flutter, http, path, path_provider, plugin_platform_interface

More

Packages that depend on offline_slm

Packages that implement offline_slm