Files
recipe-app/flutter/lib/features/admin/domain/admin_ai_trace_detail.dart
T
Nils-Johan Gynther d9f992ca9a
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 4m21s
Test Suite / flutter-quality (push) Failing after 1m38s
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
2026-05-23 21:11:46 +02:00

135 lines
4.2 KiB
Dart

import 'admin_ai_trace.dart';
class AdminAiWarning {
final String code;
final String kind;
final String title;
final String message;
final String severity;
final String? location;
final int? itemIndex;
const AdminAiWarning({
required this.code,
required this.kind,
required this.title,
required this.message,
required this.severity,
required this.location,
required this.itemIndex,
});
factory AdminAiWarning.fromJson(Map<String, dynamic> json) {
return AdminAiWarning(
code: (json['code'] ?? '').toString(),
kind: (json['kind'] ?? '').toString(),
title: (json['title'] ?? '').toString(),
message: (json['message'] ?? '').toString(),
severity: (json['severity'] ?? '').toString(),
location: json['location']?.toString(),
itemIndex: (json['itemIndex'] as num?)?.toInt(),
);
}
factory AdminAiWarning.fromLegacy(String value) {
final trimmed = value.trim();
final parts = trimmed.split(':');
final kind = parts.isNotEmpty ? parts.first : 'parse';
final code = parts.length > 1 ? parts.sublist(1).join(':') : trimmed;
return AdminAiWarning(
code: code,
kind: kind,
title: trimmed,
message: trimmed,
severity: 'warning',
location: null,
itemIndex: null,
);
}
}
class AdminAiTraceDetail {
final String id;
final AdminAiTraceSource source;
final AdminAiTraceStatus status;
final DateTime createdAt;
final int userId;
final String userLabel;
final int? sessionId;
final String? fileName;
final String? model;
final int? durationMs;
final int? retryCount;
final int? chunkCount;
final List<AdminAiWarning> warnings;
final List<String> legacyWarnings;
final String? error;
final String? prompt;
final String? rawOutput;
final Map<String, dynamic>? normalizedOutput;
final Map<String, dynamic> summary;
const AdminAiTraceDetail({
required this.id,
required this.source,
required this.status,
required this.createdAt,
required this.userId,
required this.userLabel,
required this.sessionId,
required this.fileName,
required this.model,
required this.durationMs,
required this.retryCount,
required this.chunkCount,
required this.warnings,
required this.legacyWarnings,
required this.error,
required this.prompt,
required this.rawOutput,
required this.normalizedOutput,
required this.summary,
});
factory AdminAiTraceDetail.fromJson(Map<String, dynamic> json) {
final warningsRaw = (json['warnings'] as List<dynamic>?) ?? const [];
final legacyWarningsRaw = (json['legacyWarnings'] as List<dynamic>?) ?? const [];
final normalizedOutputMap = json['normalizedOutput'] is Map
? Map<String, dynamic>.from(json['normalizedOutput'] as Map)
: null;
final summaryMap = json['summary'] is Map
? Map<String, dynamic>.from(json['summary'] as Map)
: const <String, dynamic>{};
return AdminAiTraceDetail(
id: (json['id'] ?? '').toString(),
source: AdminAiTraceSourceX.fromApi(json['source']?.toString()),
status: AdminAiTraceStatusX.fromApi(json['status']?.toString()),
createdAt: DateTime.tryParse((json['createdAt'] ?? '').toString()) ??
DateTime.fromMillisecondsSinceEpoch(0),
userId: (json['userId'] as num?)?.toInt() ?? 0,
userLabel: (json['userLabel'] ?? '').toString(),
sessionId: (json['sessionId'] as num?)?.toInt(),
fileName: json['fileName']?.toString(),
model: json['model']?.toString(),
durationMs: (json['durationMs'] as num?)?.toInt(),
retryCount: (json['retryCount'] as num?)?.toInt(),
chunkCount: (json['chunkCount'] as num?)?.toInt(),
warnings: warningsRaw
.map((entry) {
if (entry is Map) {
return AdminAiWarning.fromJson(Map<String, dynamic>.from(entry));
}
return AdminAiWarning.fromLegacy(entry.toString());
})
.toList(),
legacyWarnings: legacyWarningsRaw.map((entry) => entry.toString()).toList(),
error: json['error']?.toString(),
prompt: json['prompt']?.toString(),
rawOutput: json['rawOutput']?.toString(),
normalizedOutput: normalizedOutputMap,
summary: summaryMap,
);
}
}