Add LLM digest setup and share timestamp context
This commit is contained in:
@@ -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});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user