offline_slm 0.0.2
offline_slm: ^0.0.2 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',
],
),
);
}
}