import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; const String _llmDiagnosticsLogKey = 'llm_diagnostics_log_v1'; const int _maxLlmDiagnosticsEvents = 40; const int _maxLlmDiagnosticsDetailChars = 8000; class LlmDiagnosticsEvent { const LlmDiagnosticsEvent({ required this.at, required this.stage, required this.message, this.details, }); factory LlmDiagnosticsEvent.fromJson(Map json) { return LlmDiagnosticsEvent( at: DateTime.parse( json['at'] as String? ?? DateTime.now().toUtc().toIso8601String(), ).toLocal(), stage: json['stage'] as String? ?? 'unknown', message: json['message'] as String? ?? '', details: json['details'] as String?, ); } final DateTime at; final String stage; final String message; final String? details; Map toJson() { return { 'at': at.toUtc().toIso8601String(), 'stage': stage, 'message': message, if (details != null) 'details': details, }; } } class LlmDiagnosticsLog { const LlmDiagnosticsLog(); Future> read() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final String? raw = prefs.getString(_llmDiagnosticsLogKey); if (raw == null || raw.isEmpty) { return const []; } try { final List items = jsonDecode(raw) as List; return items .whereType>() .map(LlmDiagnosticsEvent.fromJson) .toList(growable: false); } on FormatException { return const []; } } Future record(String stage, String message, {String? details}) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final List current = await read(); final List next = [ LlmDiagnosticsEvent( at: DateTime.now(), stage: stage, message: message, details: _compactDetails(details), ), ...current, ].take(_maxLlmDiagnosticsEvents).toList(growable: false); await prefs.setString( _llmDiagnosticsLogKey, jsonEncode( next .map((LlmDiagnosticsEvent event) => event.toJson()) .toList(growable: false), ), ); } Future clear() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.remove(_llmDiagnosticsLogKey); } String exportText(List events) { if (events.isEmpty) { return 'No LLM diagnostics recorded.'; } return events .map((LlmDiagnosticsEvent event) { final String details = event.details == null ? '' : '\n${event.details}'; return '[${event.at.toIso8601String()}] ${event.stage}: ${event.message}$details'; }) .join('\n\n'); } } final Provider llmDiagnosticsLogProvider = Provider((Ref ref) => const LlmDiagnosticsLog()); String? _compactDetails(String? value) { final String? trimmed = value?.trim(); if (trimmed == null || trimmed.isEmpty) { return null; } if (trimmed.length <= _maxLlmDiagnosticsDetailChars) { return trimmed; } return '${trimmed.substring(0, _maxLlmDiagnosticsDetailChars)}\n[truncated]'; }