feat(ai): enhance AI trace warnings with product context
- 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:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user