feat(ai): enhance AI trace warnings with product context
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 2m31s
Test Suite / flutter-quality (push) Failing after 1m12s

- Added `productName` field to `AdminAiWarning` to include product context in warnings
- Updated `collectWarnings` to extract and include `rawName` as `productName` in AI trace warnings
- Added `signals` field to `FlyerParseItem` type for detailed product signals
- Enhanced Flutter admin panel to display product names in AI trace warnings
- Added new `AdminAiTraceResponse` DTO for AI trace data structure
This commit is contained in:
Nils-Johan Gynther
2026-05-24 20:55:14 +02:00
parent 7713eb2fa7
commit ca1eed5061
5 changed files with 117 additions and 93 deletions
+6 -3
View File
@@ -457,7 +457,7 @@ export class AiTraceService {
return `user:${userId}`; return `user:${userId}`;
} }
private collectWarnings(items: Array<{ parseReasons: unknown; matchReasons: unknown; itemIndex?: number }>): { private collectWarnings(items: Array<{ parseReasons: unknown; matchReasons: unknown; itemIndex?: number; rawName?: string }>): {
warnings: AdminAiWarning[]; warnings: AdminAiWarning[];
legacyWarnings: string[]; legacyWarnings: string[];
} { } {
@@ -467,6 +467,7 @@ export class AiTraceService {
for (const item of items) { for (const item of items) {
const itemIndex = item.itemIndex != null ? item.itemIndex + 1 : undefined; const itemIndex = item.itemIndex != null ? item.itemIndex + 1 : undefined;
const productName = item.rawName?.trim() || 'okänt';
if (Array.isArray(item.parseReasons)) { if (Array.isArray(item.parseReasons)) {
for (const reason of item.parseReasons) { for (const reason of item.parseReasons) {
@@ -475,7 +476,8 @@ export class AiTraceService {
const warning: AdminAiWarning = { const warning: AdminAiWarning = {
...describeParseReason(text), ...describeParseReason(text),
itemIndex, itemIndex,
}; productName,
} as AdminAiWarning;
const key = `${warning.kind}:${text}:${warning.itemIndex ?? 0}`; const key = `${warning.kind}:${text}:${warning.itemIndex ?? 0}`;
if (dedupe.has(key)) continue; if (dedupe.has(key)) continue;
dedupe.add(key); dedupe.add(key);
@@ -491,7 +493,8 @@ export class AiTraceService {
const warning: AdminAiWarning = { const warning: AdminAiWarning = {
...describeMatchReason(text), ...describeMatchReason(text),
itemIndex, itemIndex,
}; productName,
} as AdminAiWarning;
const key = `${warning.kind}:${text}:${warning.itemIndex ?? 0}`; const key = `${warning.kind}:${text}:${warning.itemIndex ?? 0}`;
if (dedupe.has(key)) continue; if (dedupe.has(key)) continue;
dedupe.add(key); dedupe.add(key);
@@ -0,0 +1,6 @@
import { FlyerReasonDescriptor } from '../../flyer-import/services/reason-codes';
export type AdminAiWarning = FlyerReasonDescriptor & {
itemIndex?: number;
productName?: string;
};
@@ -40,6 +40,7 @@ type FlyerParseItem = {
offerText: string | null; offerText: string | null;
confidence: number; confidence: number;
reasonCodes: string[]; reasonCodes: string[];
signals?: ImportedItemSignals | null;
}; };
type FlyerParseResponse = { type FlyerParseResponse = {
@@ -149,11 +150,14 @@ export class FlyerImportService {
matchConfidence: match.confidence, matchConfidence: match.confidence,
}); });
const origin = item.signals?.originCountries?.[0] || null;
const brand = item.brand && item.brand.trim() !== origin ? item.brand : null;
return { return {
flyerItemId: null, flyerItemId: null,
rawName: item.rawName, rawName: item.rawName,
normalizedName: signalData.normalizedMatchName || item.normalizedName, normalizedName: signalData.normalizedMatchName || item.normalizedName,
brand: item.brand, brand,
category: item.category, category: item.category,
categoryId, categoryId,
price, price,
@@ -733,6 +737,7 @@ export class FlyerImportService {
offerText: item.offerText, offerText: item.offerText,
confidence: item.parseConfidence, confidence: item.parseConfidence,
reasonCodes: item.parseReasons, reasonCodes: item.parseReasons,
signals: null,
})); }));
const warnings: string[] = []; const warnings: string[] = [];
@@ -8,6 +8,7 @@ class AdminAiWarning {
final String severity; final String severity;
final String? location; final String? location;
final int? itemIndex; final int? itemIndex;
final String? productName;
const AdminAiWarning({ const AdminAiWarning({
required this.code, required this.code,
@@ -17,6 +18,7 @@ class AdminAiWarning {
required this.severity, required this.severity,
required this.location, required this.location,
required this.itemIndex, required this.itemIndex,
required this.productName,
}); });
factory AdminAiWarning.fromJson(Map<String, dynamic> json) { factory AdminAiWarning.fromJson(Map<String, dynamic> json) {
@@ -28,6 +30,7 @@ class AdminAiWarning {
severity: (json['severity'] ?? '').toString(), severity: (json['severity'] ?? '').toString(),
location: json['location']?.toString(), location: json['location']?.toString(),
itemIndex: (json['itemIndex'] as num?)?.toInt(), itemIndex: (json['itemIndex'] as num?)?.toInt(),
productName: json['productName']?.toString(),
); );
} }
@@ -44,6 +47,7 @@ class AdminAiWarning {
severity: 'warning', severity: 'warning',
location: null, location: null,
itemIndex: null, itemIndex: null,
productName: null,
); );
} }
} }
@@ -139,7 +139,8 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
String _formatWarningLine(AdminAiWarning warning) { String _formatWarningLine(AdminAiWarning warning) {
final rowSuffix = warning.itemIndex == null ? '' : ' (rad ${warning.itemIndex})'; final rowSuffix = warning.itemIndex == null ? '' : ' (rad ${warning.itemIndex})';
return '[${warning.severity}] ${warning.title}$rowSuffix: ${warning.message}'; final productSuffix = warning.productName != null ? ' (${warning.productName})' : '';
return '[${warning.severity}] ${warning.title}$rowSuffix$productSuffix: ${warning.message}';
} }
String _buildErrorReport({ String _buildErrorReport({
@@ -690,6 +691,11 @@ class _WarningsCard extends StatelessWidget {
'Rad: ${warning.itemIndex}', 'Rad: ${warning.itemIndex}',
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
), ),
if (warning.productName != null)
Text(
'Produkt: ${warning.productName}',
style: theme.textTheme.bodySmall,
),
], ],
), ),
trailing: IconButton( trailing: IconButton(