import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'data/ai/gemma_llm_service.dart'; import 'data/ai/llm_service.dart'; import 'data/ai/model_lifecycle.dart'; import 'data/db/daos/meta_dao.dart'; import 'state/ai_providers.dart'; import 'state/providers.dart'; import 'ui/screens/habit_list_screen.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); final db = await openProductionDatabase(); runApp(ProviderScope( overrides: [ appDatabaseProvider.overrideWithValue(db), // #218: real GemmaLlmService when model file is on disk + verified, // MockLlmService otherwise. The provider is read lazily by the frame // suggestion flow, so the resolution is dynamic per call. llmServiceProvider.overrideWith((ref) { return _LazyLlmService( lifecycle: ref.watch(modelLifecycleProvider), meta: ref.watch(metaDaoProvider), ); }), ], child: const LifeHelperApp(), )); } /// Adapter that lazily resolves between [GemmaLlmService] (when the /// model file exists + meta is intact) and [MockLlmService] (fallback, /// graceful empty candidates). Keeps the rest of the app unaware of /// the difference — `suggestFrame` only sees [LlmService]. class _LazyLlmService implements LlmService { _LazyLlmService({required this.lifecycle, required this.meta}); final ModelLifecycle lifecycle; final MetaDao meta; LlmService? _delegate; Future _resolve() async { final avail = await lifecycle.checkAvailability(); final path = await meta.find(AiMetaKeys.modelPath); final wantGemma = avail == ModelAvailability.ready && path != null; // Re-resolve every call so opt-in / opt-out state changes are reflected // without an app restart. Repeat-resolve of the same kind reuses the // cached instance (Gemma's flutter_gemma installModel is idempotent; // Mock has no setup), but the kind itself flips when availability does. final keep = _delegate != null && (wantGemma == (_delegate is GemmaLlmService)) && (!wantGemma || (_delegate as GemmaLlmService).modelPath == path); if (!keep) { _delegate = wantGemma ? GemmaLlmService(modelPath: path) : MockLlmService(); } return _delegate!; } @override bool get isLoaded => _delegate?.isLoaded ?? false; @override Future load() async => (await _resolve()).load(); @override Future unload() async { final d = _delegate; if (d != null) await d.unload(); } @override Future> generateStructured( String prompt, Map schema, ) async => (await _resolve()).generateStructured(prompt, schema); } class LifeHelperApp extends StatelessWidget { const LifeHelperApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'life-helper', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), useMaterial3: true, ), home: const HabitListScreen(), ); } }