refactor(ai): enhance AI trace integration and OCR normalization
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 3m54s
Test Suite / flutter-quality (push) Failing after 1m29s

- Add FlyerTraceSupplement type for AI trace metadata
- Implement getFlyerTraceSupplements method to fetch trace supplements
- Update AiTraceService to include prompt/rawOutput and counters in flyer traces
- Add persistFlyerTrace method to FlyerImportService for trace persistence
- Enhance AiFlyerParserService to return structured trace data with prompts and retries
- Update FlyerNormalizerService with OCR typo fixes for cheese variants and spröd bakad firre
- Improve Flutter admin panel with selectable text, warnings display, and tooltips
- Add comprehensive tests for AI trace supplements and normalization rules
This commit is contained in:
Nils-Johan Gynther
2026-05-21 19:11:54 +02:00
parent 67a7590525
commit 026323b72a
9 changed files with 681 additions and 67 deletions
@@ -300,10 +300,13 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
title: Text(item.fileName ?? item.id),
subtitle: Text(
'${_formatDateTime(item.createdAt)}${item.userLabel}'),
trailing: Chip(
label: Text(item.status.label),
labelStyle: TextStyle(
color: _statusColor(item.status, theme.colorScheme)),
trailing: Tooltip(
message: _statusTooltipText(item),
child: Chip(
label: Text(item.status.label),
labelStyle: TextStyle(
color: _statusColor(item.status, theme.colorScheme)),
),
),
);
},
@@ -349,6 +352,14 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
return ListView(
children: [
_TraceMetaCard(detail: detail, formatDateTime: _formatDateTime),
if (detail.warnings.isNotEmpty) ...[
const SizedBox(height: 12),
_WarningsCard(
warnings: detail.warnings,
onCopyWarning: (warning) => _copyText(warning, 'Varning'),
onCopyAll: () => _copyText(detail.warnings.join('\n'), 'Varningar'),
),
],
const SizedBox(height: 12),
_PromptCard(
prompt: prompt,
@@ -375,6 +386,20 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
_cachedOutputPrettyJson = next;
return next;
}
String _statusTooltipText(AdminAiTraceListItem item) {
final parts = <String>[];
if (item.status == AdminAiTraceStatus.warning && item.warningsCount > 0) {
parts.add('${item.warningsCount} varning(ar). Välj raden för detaljer och kopiering.');
}
if (item.error != null && item.error!.trim().isNotEmpty) {
parts.add(item.error!.trim());
}
if (parts.isEmpty) {
return 'Inga ytterligare detaljer.';
}
return parts.join('\n');
}
}
class _TraceMetaCard extends StatelessWidget {
@@ -466,15 +491,13 @@ class _PromptCard extends StatelessWidget {
.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(8),
),
child: Text(
hasPrompt
? value
: 'Prompt är inte tillgänglig i denna fas för vald källa.',
maxLines: expanded ? null : 10,
overflow:
expanded ? TextOverflow.visible : TextOverflow.ellipsis,
style: theme.textTheme.bodySmall
?.copyWith(fontFamily: 'monospace'),
child: SelectionArea(
child: SelectableText(
hasPrompt ? value : 'Prompt saknas för detta spår.',
maxLines: expanded ? null : 10,
style: theme.textTheme.bodySmall
?.copyWith(fontFamily: 'monospace'),
),
),
),
],
@@ -484,15 +507,27 @@ class _PromptCard extends StatelessWidget {
}
}
class _OutputJsonCard extends StatelessWidget {
class _OutputJsonCard extends StatefulWidget {
final String jsonText;
final VoidCallback onCopy;
const _OutputJsonCard({required this.jsonText, required this.onCopy});
@override
State<_OutputJsonCard> createState() => _OutputJsonCardState();
}
class _OutputJsonCardState extends State<_OutputJsonCard> {
static const int _maxPreviewChars = 12000;
bool _expanded = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final shouldTruncate = widget.jsonText.length > _maxPreviewChars;
final visibleText =
!_expanded && shouldTruncate ? '${widget.jsonText.substring(0, _maxPreviewChars)}\n' : widget.jsonText;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
@@ -506,11 +541,20 @@ class _OutputJsonCard extends StatelessWidget {
style: theme.textTheme.titleMedium)),
IconButton(
tooltip: 'Kopiera JSON',
onPressed: onCopy,
onPressed: widget.onCopy,
icon: const Icon(Icons.copy_all),
),
],
),
if (shouldTruncate)
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () => setState(() => _expanded = !_expanded),
icon: Icon(_expanded ? Icons.unfold_less : Icons.unfold_more),
label: Text(_expanded ? 'Visa mindre' : 'Visa hela outputen'),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -520,12 +564,70 @@ class _OutputJsonCard extends StatelessWidget {
.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
jsonText,
style: theme.textTheme.bodySmall
?.copyWith(fontFamily: 'monospace'),
child: SelectionArea(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SelectableText(
visibleText,
style: theme.textTheme.bodySmall
?.copyWith(fontFamily: 'monospace'),
),
),
),
),
],
),
),
);
}
}
class _WarningsCard extends StatelessWidget {
final List<String> warnings;
final void Function(String warning) onCopyWarning;
final VoidCallback onCopyAll;
const _WarningsCard({
required this.warnings,
required this.onCopyWarning,
required this.onCopyAll,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Varningar (${warnings.length})',
style: theme.textTheme.titleMedium,
),
),
IconButton(
tooltip: 'Kopiera alla varningar',
onPressed: onCopyAll,
icon: const Icon(Icons.copy_all),
),
],
),
const SizedBox(height: 8),
...warnings.map(
(warning) => ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.warning_amber_rounded, size: 18),
title: SelectableText(warning),
trailing: IconButton(
tooltip: 'Kopiera varning',
onPressed: () => onCopyWarning(warning),
icon: const Icon(Icons.copy, size: 18),
),
),
),