import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../data/ai/llm_service.dart'; import '../data/ai/model_lifecycle.dart'; import '../data/db/app_database.dart' as drift; import '../domain/ai/frame_candidate.dart'; import '../domain/ai/suggest_frame.dart'; import '../domain/models/frame_pattern.dart'; import 'providers.dart'; /// Default config for the on-device Gemma model (#215). /// OQ-1: URL + SHA-256 pinned in Developer phase. Until then, downloads are /// disabled (AI toggle is gated behind these constants being real). const _kModelUrlPlaceholder = 'https://example.invalid/gemma4-e2b-q4.bin'; // OQ-1 const _kModelShaPlaceholder = 'PENDING_OQ_1'; final modelLifecycleProvider = Provider((ref) { return ModelLifecycle( meta: ref.watch(metaDaoProvider), config: ModelConfig( url: Uri.parse(_kModelUrlPlaceholder), expectedSha256: _kModelShaPlaceholder, ), ); }); /// Read-only opt-in state. Default OFF; persisted in `meta_kv`. final aiSettingsProvider = FutureProvider((ref) async { final meta = ref.watch(metaDaoProvider); final v = await meta.find(AiMetaKeys.optIn); return v == 'true'; }); /// Toggles opt-in. On opt-out, purges model file via [ModelLifecycle.purge]. class AiSettingsController { AiSettingsController(this.ref); final Ref ref; Future setOptIn(bool value) async { final meta = ref.read(metaDaoProvider); if (value) { await meta.put(AiMetaKeys.optIn, 'true'); ref.invalidate(aiSettingsProvider); ref.invalidate(modelAvailabilityProvider); return 0; } final freed = await ref.read(modelLifecycleProvider).purge(); await meta.put(AiMetaKeys.optIn, 'false'); ref.invalidate(aiSettingsProvider); ref.invalidate(modelAvailabilityProvider); return freed; } } final aiSettingsControllerProvider = Provider((ref) { return AiSettingsController(ref); }); final modelAvailabilityProvider = FutureProvider((ref) async { final lc = ref.watch(modelLifecycleProvider); return lc.checkAvailability(); }); /// Loads FramePatterns from DB and converts to domain models. final framePatternsProvider = FutureProvider>( (ref) async { final db = ref.watch(appDatabaseProvider); final rows = await db.select(db.framePatterns).get(); return rows.map(_toDomain).toList(growable: false); }, ); FramePatternModel _toDomain(drift.FramePattern r) => FramePatternModel( id: r.id, domain: r.domain, avoidanceKeyword: r.avoidanceKeyword, l0Example: r.l0Example, l1SimpleReplace: r.l1SimpleReplace, l2Suggestion: r.l2Suggestion, l3Identity: r.l3Identity, ); /// Singleton LLM service for the app. v1 starts unloaded; first /// [suggestFrame] triggers `.load()` via the dialog. Override in tests with /// `MockLlmService`. final llmServiceProvider = Provider((ref) { throw UnimplementedError( 'llmServiceProvider must be overridden (Mock in tests, ' 'GemmaLlmService after OQ-1 in production).', ); }); /// `family` param wraps a SuggestFrameInput. Loads model lazily before /// calling suggestFrame. final frameSuggestionsProvider = FutureProvider.autoDispose .family, SuggestFrameInput>((ref, input) async { final llm = ref.watch(llmServiceProvider); final patterns = await ref.watch(framePatternsProvider.future); if (!llm.isLoaded) { try { await llm.load(); } catch (_) { return const []; } } return suggestFrame( input, llm: llm, framePatterns: patterns, ); });