app_settings_ai 0.1.0
app_settings_ai: ^0.1.0 copied to clipboard
On-device AI settings proposals for the app_settings plugin. Apple Foundation Models on iOS, Gemini Nano on Android. Free and MIT licensed.
import 'package:app_settings_ai/app_settings_ai.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final TextEditingController _request =
TextEditingController(text: "I'm not getting message alerts");
SettingsAiAvailability? _availability;
SettingsProposalResult? _result;
bool _busy = false;
String? _error;
@override
void initState() {
super.initState();
_checkAvailability();
}
@override
void dispose() {
_request.dispose();
super.dispose();
}
Future<void> _checkAvailability() async {
final availability = await AppSettingsAI.availability();
if (mounted) setState(() => _availability = availability);
}
Future<void> _prepareModel() async {
setState(() => _busy = true);
final availability = await AppSettingsAI.prepareModel();
if (mounted) {
setState(() {
_availability = availability;
_busy = false;
});
}
}
Future<void> _propose() async {
setState(() {
_busy = true;
_error = null;
_result = null;
});
final result = await AppSettingsAI.proposeSettings(
request: _request.text,
context: const {'notificationsEnabled': false},
);
if (mounted) {
setState(() {
_result = result;
_busy = false;
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('app_settings_ai')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_availabilityCard(),
const Divider(height: 32),
TextField(
controller: _request,
decoration: const InputDecoration(
labelText: 'What is wrong?',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _busy ? null : _propose,
child: const Text('Propose settings'),
),
if (_busy) ...[
const SizedBox(height: 16),
const Center(child: CircularProgressIndicator()),
],
if (_error != null) ...[
const SizedBox(height: 16),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
..._proposals(),
],
),
),
);
}
Widget _availabilityCard() {
final availability = _availability;
if (availability == null) return const Card(child: ListTile(title: Text('Checking…')));
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
availability.isAvailable ? Icons.check_circle : Icons.cancel,
color: availability.isAvailable ? Colors.green : Colors.orange,
),
title: Text(availability.isAvailable ? 'On-device AI ready' : 'Unavailable'),
subtitle: Text(
'provider: ${availability.provider.name}'
'${availability.reason != null ? '\nreason: ${availability.reason!.name}' : ''}'
'${availability.message != null ? '\n${availability.message}' : ''}',
),
),
// Android only: Gemini Nano can be fetched on demand.
if (availability.canDownload)
TextButton(
onPressed: _busy ? null : _prepareModel,
child: const Text('Download model'),
),
],
),
);
}
List<Widget> _proposals() {
final result = _result;
if (result == null) return const [];
if (!result.isOk) {
return [
const SizedBox(height: 16),
Text(
'status: ${result.status.name}'
'${result.unavailableReason != null ? '\nreason: ${result.unavailableReason!.name}' : ''}'
'${result.errorMessage != null ? '\n${result.errorMessage}' : ''}',
),
];
}
if (result.proposals.isEmpty) {
return const [SizedBox(height: 16), Text('No relevant settings found.')];
}
return [
const SizedBox(height: 16),
for (final proposal in result.proposals)
Card(
child: ListTile(
title: Text(proposal.title),
subtitle: Text(
'${proposal.rationale}\n'
'${proposal.setting.name} · ${(proposal.confidence * 100).round()}%',
),
isThreeLine: true,
trailing: const Icon(Icons.open_in_new),
// Equivalent to AppSettings.openAppSettings(type: proposal.setting).
onTap: () => proposal.open(),
),
),
];
}
}