diff --git a/backend/src/ai/ai-trace.service.ts b/backend/src/ai/ai-trace.service.ts index ad9cd9ee..634695f3 100644 --- a/backend/src/ai/ai-trace.service.ts +++ b/backend/src/ai/ai-trace.service.ts @@ -457,7 +457,7 @@ export class AiTraceService { 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[]; legacyWarnings: string[]; } { @@ -467,6 +467,7 @@ export class AiTraceService { for (const item of items) { const itemIndex = item.itemIndex != null ? item.itemIndex + 1 : undefined; + const productName = item.rawName?.trim() || 'okänt'; if (Array.isArray(item.parseReasons)) { for (const reason of item.parseReasons) { @@ -475,7 +476,8 @@ export class AiTraceService { const warning: AdminAiWarning = { ...describeParseReason(text), itemIndex, - }; + productName, + } as AdminAiWarning; const key = `${warning.kind}:${text}:${warning.itemIndex ?? 0}`; if (dedupe.has(key)) continue; dedupe.add(key); @@ -491,7 +493,8 @@ export class AiTraceService { const warning: AdminAiWarning = { ...describeMatchReason(text), itemIndex, - }; + productName, + } as AdminAiWarning; const key = `${warning.kind}:${text}:${warning.itemIndex ?? 0}`; if (dedupe.has(key)) continue; dedupe.add(key); diff --git a/backend/src/ai/dto/admin-ai-trace.response.ts b/backend/src/ai/dto/admin-ai-trace.response.ts new file mode 100644 index 00000000..21d579f4 --- /dev/null +++ b/backend/src/ai/dto/admin-ai-trace.response.ts @@ -0,0 +1,6 @@ +import { FlyerReasonDescriptor } from '../../flyer-import/services/reason-codes'; + +export type AdminAiWarning = FlyerReasonDescriptor & { + itemIndex?: number; + productName?: string; +}; \ No newline at end of file diff --git a/backend/src/flyer-import/flyer-import.service.ts b/backend/src/flyer-import/flyer-import.service.ts index f584d241..f703bc1d 100644 --- a/backend/src/flyer-import/flyer-import.service.ts +++ b/backend/src/flyer-import/flyer-import.service.ts @@ -24,22 +24,23 @@ import { buildDisplayNameDetailed } from '../import-common/import-display-name.u import { extractImportSignals } from '../import-common/import-signals.util'; import { ImportedItemSignals } from '../import-common/import-item.types'; -type FlyerParseItem = { - rawName: string; - normalizedName: string; - brand: string | null; - category: string | null; - price: number | null; - priceUnit: string | null; - comparisonPrice: number | null; - comparisonUnit: string | null; - weight: string | null; - bundleWeight: string | null; - isBundle: boolean; - bundleItems: string[]; - offerText: string | null; - confidence: number; - reasonCodes: string[]; +type FlyerParseItem = { + rawName: string; + normalizedName: string; + brand: string | null; + category: string | null; + price: number | null; + priceUnit: string | null; + comparisonPrice: number | null; + comparisonUnit: string | null; + weight: string | null; + bundleWeight: string | null; + isBundle: boolean; + bundleItems: string[]; + offerText: string | null; + confidence: number; + reasonCodes: string[]; + signals?: ImportedItemSignals | null; }; type FlyerParseResponse = { @@ -129,56 +130,59 @@ export class FlyerImportService { offerText: item.offerText, }); - const match = this.matchItem(item, signalData.normalizedMatchName, signalData.signals, products, aliasToProduct, productById); - const signals = this.extractOfferSignals(item.offerText); - const price = item.price ?? signals.price; - const priceUnit = this.normalizeUnit(item.priceUnit) ?? signals.priceUnit; - const comparisonPrice = item.comparisonPrice ?? signals.comparisonPrice; - const comparisonUnit = this.normalizeUnit(item.comparisonUnit) ?? signals.comparisonUnit; - const offerLimitText = this.extractOfferLimitText(item.offerText); - const displayNameDetailed = buildDisplayNameDetailed({ - rawName: item.rawName, - isBundle: item.isBundle, - bundleItems: this.sanitizeBundleItems(item.bundleItems), - }); - const categoryId = this.categoryResolver.resolveForFlyer({ - categories, - signalText: [item.rawName, item.brand ?? '', item.offerText ?? ''].join(' ').trim(), - categoryHint: item.category, - matchedProductCategoryId: match.product?.categoryId ?? null, - matchConfidence: match.confidence, - }); - - return { - flyerItemId: null, - rawName: item.rawName, - normalizedName: signalData.normalizedMatchName || item.normalizedName, - brand: item.brand, - category: item.category, - categoryId, - price, - priceUnit, - comparisonPrice, - comparisonUnit, - weight: item.weight, - bundleWeight: item.bundleWeight, - isBundle: item.isBundle, - bundleItems: this.sanitizeBundleItems(item.bundleItems), - displayNameDetailed, - signals: signalData.signals, - offerText: item.offerText, - isOffer: this.isOfferItem(item, signals.hasCampaignPattern), - offerLimitText, - parseConfidence: item.confidence, - parseReasons: item.reasonCodes, - parseReasonsDetailed: this.describeParseReasons(item.reasonCodes), - matchedProductId: match.product?.id ?? null, - matchedProductName: match.product?.name ?? null, - matchedVia: match.via, - matchConfidence: match.confidence, - matchReasons: match.reasons, - matchReasonsDetailed: this.describeMatchReasons(match.reasons), - }; + const match = this.matchItem(item, signalData.normalizedMatchName, signalData.signals, products, aliasToProduct, productById); + const signals = this.extractOfferSignals(item.offerText); + const price = item.price ?? signals.price; + const priceUnit = this.normalizeUnit(item.priceUnit) ?? signals.priceUnit; + const comparisonPrice = item.comparisonPrice ?? signals.comparisonPrice; + const comparisonUnit = this.normalizeUnit(item.comparisonUnit) ?? signals.comparisonUnit; + const offerLimitText = this.extractOfferLimitText(item.offerText); + const displayNameDetailed = buildDisplayNameDetailed({ + rawName: item.rawName, + isBundle: item.isBundle, + bundleItems: this.sanitizeBundleItems(item.bundleItems), + }); + const categoryId = this.categoryResolver.resolveForFlyer({ + categories, + signalText: [item.rawName, item.brand ?? '', item.offerText ?? ''].join(' ').trim(), + categoryHint: item.category, + matchedProductCategoryId: match.product?.categoryId ?? null, + matchConfidence: match.confidence, + }); + + const origin = item.signals?.originCountries?.[0] || null; + const brand = item.brand && item.brand.trim() !== origin ? item.brand : null; + + return { + flyerItemId: null, + rawName: item.rawName, + normalizedName: signalData.normalizedMatchName || item.normalizedName, + brand, + category: item.category, + categoryId, + price, + priceUnit, + comparisonPrice, + comparisonUnit, + weight: item.weight, + bundleWeight: item.bundleWeight, + isBundle: item.isBundle, + bundleItems: this.sanitizeBundleItems(item.bundleItems), + displayNameDetailed, + signals: signalData.signals, + offerText: item.offerText, + isOffer: this.isOfferItem(item, signals.hasCampaignPattern), + offerLimitText, + parseConfidence: item.confidence, + parseReasons: item.reasonCodes, + parseReasonsDetailed: this.describeParseReasons(item.reasonCodes), + matchedProductId: match.product?.id ?? null, + matchedProductName: match.product?.name ?? null, + matchedVia: match.via, + matchConfidence: match.confidence, + matchReasons: match.reasons, + matchReasonsDetailed: this.describeMatchReasons(match.reasons), + }; }); this.logImportMetrics(items); @@ -716,24 +720,25 @@ export class FlyerImportService { // 3. Normalisera resultatet const normalizedItems = this.normalizer.normalize(aiParseResult.items); - // 4. Konvertera till intern FlyerParseItem-format - const items: FlyerParseItem[] = normalizedItems.map((item) => ({ - rawName: item.rawName, - normalizedName: item.normalizedName, - brand: item.brand, - category: item.categoryHint, - price: item.price, - priceUnit: item.priceUnit, - comparisonPrice: item.comparisonPrice, - comparisonUnit: item.comparisonUnit, - weight: item.weight, - bundleWeight: item.bundleWeight, - isBundle: item.isBundle, - bundleItems: item.bundleItems, - offerText: item.offerText, - confidence: item.parseConfidence, - reasonCodes: item.parseReasons, - })); + // 4. Konvertera till intern FlyerParseItem-format + const items: FlyerParseItem[] = normalizedItems.map((item) => ({ + rawName: item.rawName, + normalizedName: item.normalizedName, + brand: item.brand, + category: item.categoryHint, + price: item.price, + priceUnit: item.priceUnit, + comparisonPrice: item.comparisonPrice, + comparisonUnit: item.comparisonUnit, + weight: item.weight, + bundleWeight: item.bundleWeight, + isBundle: item.isBundle, + bundleItems: item.bundleItems, + offerText: item.offerText, + confidence: item.parseConfidence, + reasonCodes: item.parseReasons, + signals: null, + })); const warnings: string[] = []; if (items.length === 0) { diff --git a/flutter/lib/features/admin/domain/admin_ai_trace_detail.dart b/flutter/lib/features/admin/domain/admin_ai_trace_detail.dart index 8b136599..43250e83 100644 --- a/flutter/lib/features/admin/domain/admin_ai_trace_detail.dart +++ b/flutter/lib/features/admin/domain/admin_ai_trace_detail.dart @@ -8,6 +8,7 @@ class AdminAiWarning { final String severity; final String? location; final int? itemIndex; + final String? productName; const AdminAiWarning({ required this.code, @@ -17,6 +18,7 @@ class AdminAiWarning { required this.severity, required this.location, required this.itemIndex, + required this.productName, }); factory AdminAiWarning.fromJson(Map json) { @@ -28,6 +30,7 @@ class AdminAiWarning { severity: (json['severity'] ?? '').toString(), location: json['location']?.toString(), itemIndex: (json['itemIndex'] as num?)?.toInt(), + productName: json['productName']?.toString(), ); } @@ -44,6 +47,7 @@ class AdminAiWarning { severity: 'warning', location: null, itemIndex: null, + productName: null, ); } } diff --git a/flutter/lib/features/admin/presentation/admin_ai_panel.dart b/flutter/lib/features/admin/presentation/admin_ai_panel.dart index d9ea6382..e44c917f 100644 --- a/flutter/lib/features/admin/presentation/admin_ai_panel.dart +++ b/flutter/lib/features/admin/presentation/admin_ai_panel.dart @@ -139,7 +139,8 @@ class _AdminAiPanelState extends ConsumerState { String _formatWarningLine(AdminAiWarning warning) { 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({ @@ -685,11 +686,16 @@ class _WarningsCard extends StatelessWidget { warning.location!, style: theme.textTheme.bodySmall, ), - if (warning.itemIndex != null) - Text( - 'Rad: ${warning.itemIndex}', - style: theme.textTheme.bodySmall, - ), + if (warning.itemIndex != null) + Text( + 'Rad: ${warning.itemIndex}', + style: theme.textTheme.bodySmall, + ), + if (warning.productName != null) + Text( + 'Produkt: ${warning.productName}', + style: theme.textTheme.bodySmall, + ), ], ), trailing: IconButton(