feat(ai): enhance AI trace warnings and reason codes system
- Added structured warning system with `AdminAiWarning` type in backend and Flutter - Implemented detailed reason descriptors with `FlyerReasonDescriptor` for parse and match operations - Added `legacyWarnings` field to maintain backward compatibility - Enhanced AI trace service to collect and format warnings with item-level context - Updated flyer import services to include detailed reason descriptions in responses - Added Swedish diacritic preservation for cheese variants (Prästost, Herrgårdsost, Grevéost) - Implemented UTF-8 content validation for AI responses - Added new reason code definitions in `reason-codes.ts` - Updated Flutter UI to display structured warnings with severity indicators - Added error report generation and copy functionality in admin panel - Added comprehensive test coverage for new warning system and cheese normalization BREAKING CHANGE: AI trace warnings are now structured objects instead of simple strings
This commit is contained in:
@@ -137,6 +137,41 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
|
||||
return const JsonEncoder.withIndent(' ').convert(data);
|
||||
}
|
||||
|
||||
String _formatWarningLine(AdminAiWarning warning) {
|
||||
final rowSuffix = warning.itemIndex == null ? '' : ' (rad ${warning.itemIndex})';
|
||||
return '[${warning.severity}] ${warning.title}$rowSuffix: ${warning.message}';
|
||||
}
|
||||
|
||||
String _buildErrorReport({
|
||||
required AdminAiTraceDetail detail,
|
||||
required String prettyOutput,
|
||||
}) {
|
||||
final warningCount = detail.warnings.length;
|
||||
final buffer = StringBuffer()
|
||||
..writeln('[AI-trace ${detail.id}]')
|
||||
..writeln('Modell: ${detail.model ?? 'okänd'}')
|
||||
..writeln('Status: ${detail.status.name} ($warningCount varningar)')
|
||||
..writeln('Tid: ${detail.createdAt.toIso8601String()}')
|
||||
..writeln();
|
||||
|
||||
if (detail.warnings.isNotEmpty) {
|
||||
buffer.writeln('Varningar:');
|
||||
for (final warning in detail.warnings) {
|
||||
buffer.writeln('- ${_formatWarningLine(warning)}');
|
||||
}
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
buffer
|
||||
..writeln('Prompt:')
|
||||
..writeln((detail.prompt ?? '').trim().isEmpty ? '[saknas]' : detail.prompt!.trim())
|
||||
..writeln()
|
||||
..writeln('Raw output:')
|
||||
..writeln((detail.rawOutput ?? '').trim().isEmpty ? prettyOutput : detail.rawOutput!.trim());
|
||||
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
|
||||
Color _statusColor(AdminAiTraceStatus status, ColorScheme scheme) {
|
||||
return switch (status) {
|
||||
AdminAiTraceStatus.success => Colors.green.shade700,
|
||||
@@ -348,16 +383,30 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
|
||||
? const <String, dynamic>{}
|
||||
: {'rawOutput': detail.rawOutput});
|
||||
final prettyOutput = _prettyOutputFor(detail.id, outputJson);
|
||||
final errorReport = _buildErrorReport(detail: detail, prettyOutput: prettyOutput);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_TraceMetaCard(detail: detail, formatDateTime: _formatDateTime),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _copyText(errorReport, 'Felrapport'),
|
||||
icon: const Icon(Icons.bug_report_outlined),
|
||||
label: const Text('Kopiera felrapport'),
|
||||
),
|
||||
),
|
||||
if (detail.warnings.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_WarningsCard(
|
||||
warnings: detail.warnings,
|
||||
onCopyWarning: (warning) => _copyText(warning, 'Varning'),
|
||||
onCopyAll: () => _copyText(detail.warnings.join('\n'), 'Varningar'),
|
||||
onCopyWarning: (warning) =>
|
||||
_copyText(_formatWarningLine(warning), 'Varning'),
|
||||
onCopyAll: () => _copyText(
|
||||
detail.warnings.map(_formatWarningLine).join('\n'),
|
||||
'Varningar',
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
@@ -583,8 +632,8 @@ class _OutputJsonCardState extends State<_OutputJsonCard> {
|
||||
}
|
||||
|
||||
class _WarningsCard extends StatelessWidget {
|
||||
final List<String> warnings;
|
||||
final void Function(String warning) onCopyWarning;
|
||||
final List<AdminAiWarning> warnings;
|
||||
final void Function(AdminAiWarning warning) onCopyWarning;
|
||||
final VoidCallback onCopyAll;
|
||||
|
||||
const _WarningsCard({
|
||||
@@ -622,8 +671,27 @@ class _WarningsCard extends StatelessWidget {
|
||||
(warning) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.warning_amber_rounded, size: 18),
|
||||
title: SelectableText(warning),
|
||||
leading: Icon(_severityIcon(warning), size: 18, color: _severityColor(warning, theme)),
|
||||
title: Text(
|
||||
warning.title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(warning.message),
|
||||
if ((warning.location ?? '').trim().isNotEmpty)
|
||||
Text(
|
||||
warning.location!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (warning.itemIndex != null)
|
||||
Text(
|
||||
'Rad: ${warning.itemIndex}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: 'Kopiera varning',
|
||||
onPressed: () => onCopyWarning(warning),
|
||||
@@ -636,4 +704,26 @@ class _WarningsCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _severityIcon(AdminAiWarning warning) {
|
||||
switch (warning.severity) {
|
||||
case 'error':
|
||||
return Icons.error_outline;
|
||||
case 'warning':
|
||||
return Icons.warning_amber_rounded;
|
||||
default:
|
||||
return Icons.info_outline;
|
||||
}
|
||||
}
|
||||
|
||||
Color _severityColor(AdminAiWarning warning, ThemeData theme) {
|
||||
switch (warning.severity) {
|
||||
case 'error':
|
||||
return theme.colorScheme.error;
|
||||
case 'warning':
|
||||
return Colors.orange.shade700;
|
||||
default:
|
||||
return theme.colorScheme.primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user