import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_config.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/core/llm/llm_config.dart'; import 'package:relationship_saver/features/ai_digest/application/llm_digest_background_scheduler.dart'; import 'package:relationship_saver/features/ai_digest/application/llm_digest_orchestrator.dart'; import 'package:relationship_saver/features/ai_digest/data/llm_digest_config.dart'; import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_review_view.dart'; import 'package:relationship_saver/features/auth/session_controller.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart'; import 'package:relationship_saver/features/share_intake/share_capture_flow.dart'; import 'package:relationship_saver/features/share_intake/share_capture_models.dart'; import 'package:relationship_saver/features/share_intake/share_inbox_view.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; class SettingsView extends ConsumerWidget { const SettingsView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final AuthSession? session = ref .watch(sessionControllerProvider) .asData ?.value; final LlmDigestConfigState digestConfig = ref.watch( llmDigestConfigProvider, ); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 760; return Padding( padding: EdgeInsets.fromLTRB( compact ? 16 : 28, compact ? 16 : 24, compact ? 16 : 28, compact ? 20 : 28, ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Settings', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 6), Text( 'Environment and integration configuration for this build.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), const SizedBox(height: 16), FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SettingRow( compact: compact, label: 'Signed in as', value: session?.user.email ?? session?.user.id ?? 'Not signed in', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Gateway mode', value: AppConfig.useFakeBackend ? 'Fake (offline-safe)' : 'REST', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Base URL', value: AppConfig.backendBaseUrl, ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Realtime', value: 'Planned later (SSE/WebSocket)', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Local notifications', value: AppConfig.enableLocalNotifications ? 'Enabled' : 'Disabled', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'LLM AI', value: ref.watch(llmConfigProvider).isConfigured ? '${_providerLabel(ref.watch(llmConfigProvider).provider)} (configured)' : 'Not configured', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'AI digest', value: digestConfig.enabled ? 'Weekly, ${_weekdayLabel(digestConfig.preferredWeekday)} ${digestConfig.preferredHour.toString().padLeft(2, '0')}:00' : 'Off', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'AI network', value: '${digestConfig.requireWifi ? 'Wi-Fi' : 'Any network'} + ${digestConfig.requireCharging ? 'charging' : 'battery allowed'}', ), const SizedBox(height: 14), Wrap( spacing: 8, runSpacing: 8, children: [ OutlinedButton.icon( onPressed: () => _configureLlm(context, ref), icon: const Icon(Icons.smart_toy_rounded), label: Text( ref.watch(llmConfigProvider).isConfigured ? 'Configure AI' : 'Setup AI', ), ), OutlinedButton.icon( onPressed: () => _toggleDigest(context, ref), icon: Icon( digestConfig.enabled ? Icons.event_busy_rounded : Icons.event_repeat_rounded, ), label: Text( digestConfig.enabled ? 'Disable Weekly AI Digest' : 'Enable Weekly AI Digest', ), ), OutlinedButton.icon( onPressed: ref.watch(llmConfigProvider).isConfigured ? () => _runPrivateDigest(context, ref) : null, icon: const Icon(Icons.auto_awesome_rounded), label: const Text('Run Private Digest Now'), ), OutlinedButton.icon( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const AiDigestReviewView(), ), ), icon: const Icon(Icons.rate_review_rounded), label: const Text('Open AI Review'), ), OutlinedButton.icon( onPressed: session == null ? null : () => _signOut(context, ref), icon: const Icon(Icons.logout_rounded), label: const Text('Sign Out'), ), OutlinedButton.icon( onPressed: session == null ? null : () { ref .read( sessionControllerProvider.notifier, ) .refreshProfile(); }, icon: const Icon(Icons.refresh_rounded), label: const Text('Refresh Profile'), ), OutlinedButton.icon( onPressed: AppConfig.enableLocalNotifications ? () => _requestNotificationPermissions( context, ref, ) : null, icon: const Icon( Icons.notifications_active_rounded, ), label: const Text('Request Notification Access'), ), OutlinedButton.icon( onPressed: () => _simulateShareCapture(context, ref), icon: const Icon(Icons.message_rounded), label: const Text('Simulate Share Capture'), ), OutlinedButton.icon( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const ShareInboxView(), ), ), icon: const Icon(Icons.inbox_rounded), label: const Text('Open Share Inbox'), ), ], ), ], ), ), const SizedBox(height: 16), FrostedCard( child: Text( 'Local-first privacy\n• Relationship details stay on this device in the current build.\n• Scheduled AI uses pseudonymous person tokens and a review inbox.\n• Names, aliases, sender names, URLs, and raw shared text are not sent in the digest payload.\n• iOS background digest runs are best-effort; manual runs are available here.\n\nRun options\n• Fake mode: flutter run --dart-define=USE_FAKE_BACKEND=true\n• REST mode: flutter run --dart-define=USE_FAKE_BACKEND=false --dart-define=BACKEND_BASE_URL=https://your-api', style: Theme.of(context).textTheme.bodyLarge, ), ), ], ), ), ); }, ); } String _providerLabel(LlmProvider provider) { switch (provider) { case LlmProvider.openai: return 'OpenAI'; case LlmProvider.anthropic: return 'Anthropic'; case LlmProvider.google: return 'Google AI'; } } String _weekdayLabel(int weekday) { return switch (weekday) { DateTime.monday => 'Mon', DateTime.tuesday => 'Tue', DateTime.wednesday => 'Wed', DateTime.thursday => 'Thu', DateTime.friday => 'Fri', DateTime.saturday => 'Sat', DateTime.sunday => 'Sun', _ => 'Sun', }; } Future _configureLlm(BuildContext context, WidgetRef ref) async { final LlmConfigState current = ref.read(llmConfigProvider); final _LlmConfigDraft? draft = await showDialog<_LlmConfigDraft>( context: context, builder: (BuildContext context) => _LlmConfigDialog( currentProvider: current.provider, hasApiKey: current.apiKeyConfigured, ), ); if (draft == null || !context.mounted) { return; } await ref.read(llmConfigProvider.notifier).setProvider(draft.provider); if (draft.clearApiKey) { await ref.read(llmConfigProvider.notifier).clearApiKey(); } else if (draft.apiKey.isNotEmpty) { await ref.read(llmConfigProvider.notifier).setApiKey(draft.apiKey); } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( draft.apiKey.isNotEmpty ? 'AI configuration saved.' : draft.clearApiKey ? 'AI API key cleared.' : 'AI configuration saved.', ), ), ); } } Future _toggleDigest(BuildContext context, WidgetRef ref) async { final LlmDigestConfigState current = ref.read(llmDigestConfigProvider); final bool nextEnabled = !current.enabled; await ref.read(llmDigestConfigProvider.notifier).setEnabled(nextEnabled); await ref .read(llmDigestBackgroundSchedulerProvider) .configure(ref.read(llmDigestConfigProvider)); if (!context.mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( nextEnabled ? 'Weekly private AI digest enabled.' : 'Weekly private AI digest disabled.', ), ), ); } Future _runPrivateDigest(BuildContext context, WidgetRef ref) async { final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context); messenger.showSnackBar( const SnackBar(content: Text('Running private AI digest...')), ); final LlmDigestRunResult result = await ref .read(llmDigestOrchestratorProvider) .runManualDigest(); if (!context.mounted) { return; } messenger.showSnackBar( SnackBar( content: Text( result.completed ? 'Private digest created ${result.createdDraftCount} suggestions.' : result.reason ?? 'Private digest did not complete.', ), ), ); } Future _signOut(BuildContext context, WidgetRef ref) async { await ref.read(sessionControllerProvider.notifier).signOut(); if (context.mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Signed out.'))); } } Future _requestNotificationPermissions( BuildContext context, WidgetRef ref, ) async { final bool granted = await ref .read(reminderSchedulerProvider) .requestPermissions(); if (!context.mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( granted ? 'Notification access granted.' : 'Notification access not granted.', ), ), ); } Future _simulateShareCapture( BuildContext context, WidgetRef ref, ) async { final String platformName = Theme.of(context).platform.name; final _ShareSimulationDraft? draft = await showDialog<_ShareSimulationDraft>( context: context, builder: (BuildContext context) => const _ShareSimulationDialog(), ); if (draft == null) { return; } if (!context.mounted) { return; } final SharedPayload? payload = buildSharedPayloadFromRawText( rawText: draft.rawText, platform: platformName, sourceAppHint: draft.sourceAppHint, ); if (payload == null) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Shared payload is empty.'))); return; } final SharedMessageIngestResult? result = await startShareCaptureReview( context, ref: ref, payload: payload, ); if (!context.mounted || result == null) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(shareCaptureResultMessage(result)), action: SnackBarAction( label: result.isQueuedForResolution ? 'Open Inbox' : 'Open Profile', onPressed: () => openShareCaptureDestination(context, ref: ref, result: result), ), ), ); } } class _SettingRow extends StatelessWidget { const _SettingRow({ required this.label, required this.value, required this.compact, }); final String label; final String value; final bool compact; @override Widget build(BuildContext context) { if (compact) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 4), Text(value, style: Theme.of(context).textTheme.bodyLarge), ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 140, child: Text( label, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: AppTheme.textSecondary), ), ), Expanded( child: Text(value, style: Theme.of(context).textTheme.bodyLarge), ), ], ); } } class _LlmConfigDraft { const _LlmConfigDraft({ required this.provider, required this.apiKey, this.clearApiKey = false, }); final LlmProvider provider; final String apiKey; final bool clearApiKey; } class _LlmConfigDialog extends StatefulWidget { const _LlmConfigDialog({ required this.currentProvider, required this.hasApiKey, }); final LlmProvider currentProvider; final bool hasApiKey; @override State<_LlmConfigDialog> createState() => _LlmConfigDialogState(); } class _LlmConfigDialogState extends State<_LlmConfigDialog> { late LlmProvider _selectedProvider; late final TextEditingController _apiKeyController; @override void initState() { super.initState(); _selectedProvider = widget.currentProvider; _apiKeyController = TextEditingController(); } @override void dispose() { _apiKeyController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Configure AI Provider'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Provider'), const SizedBox(height: 8), DropdownButtonFormField( initialValue: _selectedProvider, decoration: const InputDecoration(border: OutlineInputBorder()), items: const >[ DropdownMenuItem( value: LlmProvider.openai, child: Text('OpenAI'), ), DropdownMenuItem( value: LlmProvider.anthropic, child: Text('Anthropic (Claude)'), ), DropdownMenuItem( value: LlmProvider.google, child: Text('Google AI'), ), ], onChanged: (LlmProvider? value) { if (value != null) { setState(() { _selectedProvider = value; }); } }, ), const SizedBox(height: 16), const Text('API Key'), const SizedBox(height: 8), TextField( controller: _apiKeyController, obscureText: true, decoration: InputDecoration( hintText: widget.hasApiKey ? 'Leave empty to keep current' : 'Enter your API key', border: const OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), if (widget.hasApiKey) TextButton( onPressed: () { Navigator.of(context).pop( _LlmConfigDraft( provider: _selectedProvider, apiKey: '', clearApiKey: true, ), ); }, child: const Text('Clear Key'), ), FilledButton( onPressed: () { Navigator.of(context).pop( _LlmConfigDraft( provider: _selectedProvider, apiKey: _apiKeyController.text.trim(), ), ); }, child: const Text('Save'), ), ], ); } } class _ShareSimulationDraft { const _ShareSimulationDraft({ required this.rawText, required this.sourceAppHint, }); final String rawText; final String sourceAppHint; } class _ShareSimulationDialog extends StatefulWidget { const _ShareSimulationDialog(); @override State<_ShareSimulationDialog> createState() => _ShareSimulationDialogState(); } class _ShareSimulationDialogState extends State<_ShareSimulationDialog> { late final TextEditingController _controller; String _selectedSourceApp = 'whatsapp'; @override void initState() { super.initState(); _controller = TextEditingController( text: '[2/18/26, 20:11] Ava Hart: Can we do coffee this Saturday?', ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Simulate Share Capture'), content: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( initialValue: _selectedSourceApp, decoration: const InputDecoration(labelText: 'Source app'), items: const >[ DropdownMenuItem( value: 'whatsapp', child: Text('WhatsApp'), ), DropdownMenuItem(value: 'signal', child: Text('Signal')), DropdownMenuItem(value: 'notes', child: Text('Notes')), DropdownMenuItem( value: 'share_sheet', child: Text('Generic text/link'), ), ], onChanged: (String? value) { if (value == null) { return; } setState(() { _selectedSourceApp = value; }); }, ), const SizedBox(height: 12), TextField( controller: _controller, minLines: 3, maxLines: 6, decoration: const InputDecoration( hintText: 'Paste shared text or URL payload', ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { final String rawText = _controller.text.trim(); if (rawText.isEmpty) { return; } Navigator.of(context).pop( _ShareSimulationDraft( rawText: rawText, sourceAppHint: _selectedSourceApp, ), ); }, child: const Text('Import'), ), ], ); } }