Add LLM digest setup and share timestamp context

This commit is contained in:
Rijad Zuzo
2026-05-19 19:17:35 +02:00
parent 9d2da0c600
commit 5d80af375e
42 changed files with 2878 additions and 39 deletions
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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/core/llm/llm_diagnostics_log.dart';
import 'package:relationship_saver/core/llm/llm_service.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';
@@ -114,7 +116,15 @@ class SettingsView extends ConsumerWidget {
const SizedBox(height: 12),
_SettingRow(
compact: compact,
label: 'AI network',
label: 'AI grounding',
value: digestConfig.enableWebSearch
? 'Google Search enabled'
: 'Model only',
),
const SizedBox(height: 12),
_SettingRow(
compact: compact,
label: 'Scheduled AI network',
value:
'${digestConfig.requireWifi ? 'Wi-Fi' : 'Any network'} + ${digestConfig.requireCharging ? 'charging' : 'battery allowed'}',
),
@@ -150,6 +160,19 @@ class SettingsView extends ConsumerWidget {
: 'Enable Weekly AI Digest',
),
),
OutlinedButton.icon(
onPressed: () => _toggleWebSearch(context, ref),
icon: Icon(
digestConfig.enableWebSearch
? Icons.public_off_rounded
: Icons.public_rounded,
),
label: Text(
digestConfig.enableWebSearch
? 'Disable Web Grounding'
: 'Enable Web Grounding',
),
),
OutlinedButton.icon(
onPressed: ref.watch(llmConfigProvider).isConfigured
? () => _runPrivateDigest(context, ref)
@@ -319,6 +342,26 @@ class SettingsView extends ConsumerWidget {
);
}
Future<void> _toggleWebSearch(BuildContext context, WidgetRef ref) async {
final LlmDigestConfigState current = ref.read(llmDigestConfigProvider);
final bool nextEnabled = !current.enableWebSearch;
await ref
.read(llmDigestConfigProvider.notifier)
.setWebSearchEnabled(nextEnabled);
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
nextEnabled
? 'Web grounding enabled for private digest.'
: 'Web grounding disabled for private digest.',
),
),
);
}
Future<void> _showLlmDebug(BuildContext context) async {
await showDialog<void>(
context: context,
@@ -891,8 +934,10 @@ class _LlmDebugDialog extends ConsumerStatefulWidget {
class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
late final TextEditingController _promptController;
bool _running = false;
bool _loadingEvents = true;
String? _response;
String? _error;
List<LlmDiagnosticsEvent> _events = const <LlmDiagnosticsEvent>[];
@override
void initState() {
@@ -900,6 +945,7 @@ class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
_promptController = TextEditingController(
text: 'Reply with one short sentence confirming the connection works.',
);
_loadDiagnostics();
}
@override
@@ -958,6 +1004,44 @@ class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
),
),
],
const SizedBox(height: 18),
Row(
children: <Widget>[
Expanded(
child: Text(
'Recent diagnostics',
style: Theme.of(context).textTheme.titleMedium,
),
),
TextButton.icon(
onPressed: _events.isEmpty ? null : _copyDiagnostics,
icon: const Icon(Icons.copy_rounded),
label: const Text('Copy'),
),
TextButton.icon(
onPressed: _events.isEmpty ? null : _clearDiagnostics,
icon: const Icon(Icons.delete_outline_rounded),
label: const Text('Clear'),
),
],
),
const SizedBox(height: 8),
if (_loadingEvents)
const LinearProgressIndicator()
else if (_events.isEmpty)
Text(
'No LLM diagnostics recorded yet.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppTheme.textSecondary,
),
)
else
..._events
.take(8)
.map(
(LlmDiagnosticsEvent event) =>
_LlmDiagnosticsEventTile(event: event),
),
],
),
),
@@ -976,6 +1060,40 @@ class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
);
}
Future<void> _loadDiagnostics() async {
final List<LlmDiagnosticsEvent> events = await ref
.read(llmDiagnosticsLogProvider)
.read();
if (!mounted) {
return;
}
setState(() {
_events = events;
_loadingEvents = false;
});
}
Future<void> _copyDiagnostics() async {
final LlmDiagnosticsLog log = ref.read(llmDiagnosticsLogProvider);
await Clipboard.setData(ClipboardData(text: log.exportText(_events)));
if (!mounted) {
return;
}
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('LLM diagnostics copied.')));
}
Future<void> _clearDiagnostics() async {
await ref.read(llmDiagnosticsLogProvider).clear();
if (!mounted) {
return;
}
setState(() {
_events = const <LlmDiagnosticsEvent>[];
});
}
Future<void> _runDebugPrompt() async {
setState(() {
_running = true;
@@ -996,6 +1114,7 @@ class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
setState(() {
_response = response;
});
await _loadDiagnostics();
} catch (error) {
if (!mounted) {
return;
@@ -1003,6 +1122,7 @@ class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
setState(() {
_error = error.toString();
});
await _loadDiagnostics();
} finally {
if (mounted) {
setState(() {
@@ -1013,6 +1133,46 @@ class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> {
}
}
class _LlmDiagnosticsEventTile extends StatelessWidget {
const _LlmDiagnosticsEventTile({required this.event});
final LlmDiagnosticsEvent event;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'${event.stage} · ${event.at.toLocal().toIso8601String()}',
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(height: 4),
SelectableText(event.message),
if (event.details != null &&
event.details!.trim().isNotEmpty) ...<Widget>[
const SizedBox(height: 6),
SelectableText(
event.details!,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
),
],
],
),
);
}
}
class _LlmDebugConnectionSummary extends StatelessWidget {
const _LlmDebugConnectionSummary({required this.config});