Fix ai add
This commit is contained in:
@@ -84,10 +84,10 @@ class HiveAiRuleRepository implements IAiRuleRepository {
|
||||
Future<List<AiRule>> getByPriority() async {
|
||||
try {
|
||||
final rules = _box.values.toList();
|
||||
rules.sort((a, b) => b.priority.compareTo(a.priority));
|
||||
rules.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
|
||||
return rules;
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения правил ИИ по приоритету: $e');
|
||||
throw Exception('Ошибка получения правил ИИ: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,10 +224,6 @@ class AiRulesBloc extends Bloc<AiRulesEvent, AiRulesState> {
|
||||
errors.add('Название правила не может быть пустым');
|
||||
}
|
||||
|
||||
// Валидация описания
|
||||
if (event.rule.description.trim().isEmpty) {
|
||||
errors.add('Описание правила не может быть пустым');
|
||||
}
|
||||
|
||||
// Валидация процента уверенности
|
||||
if (event.rule.confidencePercentage < 0 || event.rule.confidencePercentage > 100) {
|
||||
|
||||
+9
-36
@@ -47,66 +47,51 @@ class AiRule extends Equatable {
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
/// Описание правила
|
||||
@HiveField(2)
|
||||
final String description;
|
||||
|
||||
/// Тип правила
|
||||
@HiveField(3)
|
||||
@HiveField(2)
|
||||
final AiRuleType type;
|
||||
|
||||
/// Активность правила
|
||||
@HiveField(4)
|
||||
@HiveField(3)
|
||||
final bool isActive;
|
||||
|
||||
/// Приоритет выполнения (чем больше, тем выше приоритет)
|
||||
@HiveField(5)
|
||||
final int priority;
|
||||
|
||||
/// Процент уверенности (0-100)
|
||||
@HiveField(6)
|
||||
@HiveField(4)
|
||||
final int confidencePercentage;
|
||||
|
||||
/// Ссылка на сообщение
|
||||
@HiveField(7)
|
||||
final String? messageReference;
|
||||
|
||||
/// Статус обработки
|
||||
@HiveField(8)
|
||||
@HiveField(5)
|
||||
final ProcessingStatus processingStatus;
|
||||
|
||||
// Поля для точки продаж
|
||||
/// Паттерн для определения торговой точки (регулярное выражение)
|
||||
@HiveField(9)
|
||||
@HiveField(6)
|
||||
final String? merchantPattern;
|
||||
|
||||
/// ID категории для автоматического назначения
|
||||
@HiveField(10)
|
||||
@HiveField(7)
|
||||
final String? categoryId;
|
||||
|
||||
// Поля для шаблона пропуска
|
||||
/// Регулярное выражение для пропуска сообщений
|
||||
@HiveField(11)
|
||||
@HiveField(8)
|
||||
final String? skipRegex;
|
||||
|
||||
/// Дата создания
|
||||
@HiveField(12)
|
||||
@HiveField(9)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Дата последнего обновления
|
||||
@HiveField(13)
|
||||
@HiveField(10)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор
|
||||
AiRule({
|
||||
String? id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.type,
|
||||
this.isActive = true,
|
||||
this.priority = 0,
|
||||
this.confidencePercentage = 80,
|
||||
this.messageReference,
|
||||
this.processingStatus = ProcessingStatus.created,
|
||||
this.merchantPattern,
|
||||
this.categoryId,
|
||||
@@ -123,12 +108,9 @@ class AiRule extends Equatable {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'type': type.name,
|
||||
'isActive': isActive,
|
||||
'priority': priority,
|
||||
'confidencePercentage': confidencePercentage,
|
||||
'messageReference': messageReference,
|
||||
'processingStatus': processingStatus.name,
|
||||
'merchantPattern': merchantPattern,
|
||||
'categoryId': categoryId,
|
||||
@@ -143,12 +125,9 @@ class AiRule extends Equatable {
|
||||
return AiRule(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
description: map['description'],
|
||||
type: AiRuleType.values.firstWhere((e) => e.name == map['type']),
|
||||
isActive: map['isActive'],
|
||||
priority: map['priority'],
|
||||
confidencePercentage: map['confidencePercentage'],
|
||||
messageReference: map['messageReference'],
|
||||
processingStatus: ProcessingStatus.values.firstWhere((e) => e.name == map['processingStatus']),
|
||||
merchantPattern: map['merchantPattern'],
|
||||
categoryId: map['categoryId'],
|
||||
@@ -161,12 +140,9 @@ class AiRule extends Equatable {
|
||||
/// Метод для создания копии объекта с возможностью изменения полей
|
||||
AiRule copyWith({
|
||||
String? name,
|
||||
String? description,
|
||||
AiRuleType? type,
|
||||
bool? isActive,
|
||||
int? priority,
|
||||
int? confidencePercentage,
|
||||
String? messageReference,
|
||||
ProcessingStatus? processingStatus,
|
||||
String? merchantPattern,
|
||||
String? categoryId,
|
||||
@@ -175,12 +151,9 @@ class AiRule extends Equatable {
|
||||
return AiRule(
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
type: type ?? this.type,
|
||||
isActive: isActive ?? this.isActive,
|
||||
priority: priority ?? this.priority,
|
||||
confidencePercentage: confidencePercentage ?? this.confidencePercentage,
|
||||
messageReference: messageReference ?? this.messageReference,
|
||||
processingStatus: processingStatus ?? this.processingStatus,
|
||||
merchantPattern: merchantPattern ?? this.merchantPattern,
|
||||
categoryId: categoryId ?? this.categoryId,
|
||||
|
||||
+19
-28
@@ -19,54 +19,45 @@ class AiRuleAdapter extends TypeAdapter<AiRule> {
|
||||
return AiRule(
|
||||
id: fields[0] as String?,
|
||||
name: fields[1] as String,
|
||||
description: fields[2] as String,
|
||||
type: fields[3] as AiRuleType,
|
||||
isActive: fields[4] == null ? true : fields[4] as bool,
|
||||
priority: fields[5] == null ? 0 : (fields[5] as num).toInt(),
|
||||
confidencePercentage: fields[6] == null ? 80 : (fields[6] as num).toInt(),
|
||||
messageReference: fields[7] as String?,
|
||||
processingStatus: fields[8] == null
|
||||
type: fields[2] as AiRuleType,
|
||||
isActive: fields[3] == null ? true : fields[3] as bool,
|
||||
confidencePercentage: fields[4] == null ? 80 : (fields[4] as num).toInt(),
|
||||
processingStatus: fields[5] == null
|
||||
? ProcessingStatus.created
|
||||
: fields[8] as ProcessingStatus,
|
||||
merchantPattern: fields[9] as String?,
|
||||
categoryId: fields[10] as String?,
|
||||
skipRegex: fields[11] as String?,
|
||||
createdAt: fields[12] as DateTime?,
|
||||
updatedAt: fields[13] as DateTime?,
|
||||
: fields[5] as ProcessingStatus,
|
||||
merchantPattern: fields[6] as String?,
|
||||
categoryId: fields[7] as String?,
|
||||
skipRegex: fields[8] as String?,
|
||||
createdAt: fields[9] as DateTime?,
|
||||
updatedAt: fields[10] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AiRule obj) {
|
||||
writer
|
||||
..writeByte(14)
|
||||
..writeByte(11)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.description)
|
||||
..writeByte(3)
|
||||
..write(obj.type)
|
||||
..writeByte(4)
|
||||
..writeByte(3)
|
||||
..write(obj.isActive)
|
||||
..writeByte(5)
|
||||
..write(obj.priority)
|
||||
..writeByte(6)
|
||||
..writeByte(4)
|
||||
..write(obj.confidencePercentage)
|
||||
..writeByte(7)
|
||||
..write(obj.messageReference)
|
||||
..writeByte(8)
|
||||
..writeByte(5)
|
||||
..write(obj.processingStatus)
|
||||
..writeByte(9)
|
||||
..writeByte(6)
|
||||
..write(obj.merchantPattern)
|
||||
..writeByte(10)
|
||||
..writeByte(7)
|
||||
..write(obj.categoryId)
|
||||
..writeByte(11)
|
||||
..writeByte(8)
|
||||
..write(obj.skipRegex)
|
||||
..writeByte(12)
|
||||
..writeByte(9)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(13)
|
||||
..writeByte(10)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,12 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _descriptionController;
|
||||
late final TextEditingController _merchantPatternController;
|
||||
late final TextEditingController _categoryIdController;
|
||||
late final TextEditingController _skipRegexController;
|
||||
late final TextEditingController _messageReferenceController;
|
||||
|
||||
late AiRuleType _selectedType;
|
||||
late bool _isActive;
|
||||
late int _priority;
|
||||
late int _confidencePercentage;
|
||||
late ProcessingStatus _processingStatus;
|
||||
|
||||
List<Category> _categories = [];
|
||||
@@ -44,16 +40,12 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
final rule = widget.rule;
|
||||
|
||||
_nameController = TextEditingController(text: rule?.name ?? '');
|
||||
_descriptionController = TextEditingController(text: rule?.description ?? '');
|
||||
_merchantPatternController = TextEditingController(text: rule?.merchantPattern ?? '');
|
||||
_categoryIdController = TextEditingController(text: rule?.categoryId ?? '');
|
||||
_skipRegexController = TextEditingController(text: rule?.skipRegex ?? '');
|
||||
_messageReferenceController = TextEditingController(text: rule?.messageReference ?? '');
|
||||
|
||||
_selectedType = rule?.type ?? AiRuleType.pointOfSale;
|
||||
_isActive = rule?.isActive ?? true;
|
||||
_priority = rule?.priority ?? 0;
|
||||
_confidencePercentage = rule?.confidencePercentage ?? 80;
|
||||
_processingStatus = rule?.processingStatus ?? ProcessingStatus.created;
|
||||
|
||||
_loadCategories();
|
||||
@@ -62,11 +54,9 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_merchantPatternController.dispose();
|
||||
_categoryIdController.dispose();
|
||||
_skipRegexController.dispose();
|
||||
_messageReferenceController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -247,42 +237,19 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
}
|
||||
|
||||
Widget _buildBasicFields() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Название правила *',
|
||||
hintText: 'Например: Сбербанк - продуктовые',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Название не может быть пустым';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Описание',
|
||||
hintText: 'Подробное описание правила...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Описание не может быть пустым';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
return TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Название правила *',
|
||||
hintText: 'Например: Сбербанк - продуктовые',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Название не может быть пустым';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -299,54 +266,28 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile<AiRuleType>(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: SegmentedButton<AiRuleType>(
|
||||
segments: const [
|
||||
ButtonSegment<AiRuleType>(
|
||||
value: AiRuleType.pointOfSale,
|
||||
groupValue: _selectedType,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
});
|
||||
},
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.store, size: 20, color: Colors.blue),
|
||||
SizedBox(width: 8),
|
||||
Text('Точка продаж'),
|
||||
],
|
||||
),
|
||||
subtitle: const Text('Определение категории по месту покупки'),
|
||||
dense: true,
|
||||
icon: Icon(Icons.store, size: 18),
|
||||
label: Text('Точка продаж'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile<AiRuleType>(
|
||||
ButtonSegment<AiRuleType>(
|
||||
value: AiRuleType.skipTemplate,
|
||||
groupValue: _selectedType,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
});
|
||||
},
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.block, size: 20, color: Colors.orange),
|
||||
SizedBox(width: 8),
|
||||
Text('Пропуск сообщений'),
|
||||
],
|
||||
),
|
||||
subtitle: const Text('Игнорирование определенных SMS'),
|
||||
dense: true,
|
||||
icon: Icon(Icons.block, size: 18),
|
||||
label: Text('Пропуск SMS'),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
selected: {_selectedType},
|
||||
onSelectionChanged: (Set<AiRuleType> newSelection) {
|
||||
setState(() {
|
||||
_selectedType = newSelection.first;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -412,9 +353,38 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
items: _categories.map<DropdownMenuItem<Category>>((Category category) {
|
||||
return DropdownMenuItem<Category>(
|
||||
value: category,
|
||||
child: Text(
|
||||
'${category.isIncome ? '💰' : '💳'} ${category.name}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: category.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
category.icon,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
category.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
category.isIncome ? '↑' : '↓',
|
||||
style: TextStyle(
|
||||
color: category.isIncome ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -496,64 +466,6 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _priority.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Приоритет',
|
||||
border: OutlineInputBorder(),
|
||||
helperText: 'Чем выше, тем раньше применяется',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
_priority = int.tryParse(value) ?? 0;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Приоритет обязателен';
|
||||
}
|
||||
final priority = int.tryParse(value);
|
||||
if (priority == null) {
|
||||
return 'Должно быть числом';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _confidencePercentage.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Уверенность (%)',
|
||||
border: OutlineInputBorder(),
|
||||
helperText: 'От 0 до 100',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
_confidencePercentage = int.tryParse(value) ?? 80;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Уверенность обязательна';
|
||||
}
|
||||
final confidence = int.tryParse(value);
|
||||
if (confidence == null || confidence < 0 || confidence > 100) {
|
||||
return 'От 0 до 100';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownButtonFormField<ProcessingStatus>(
|
||||
value: _processingStatus,
|
||||
decoration: const InputDecoration(
|
||||
@@ -589,17 +501,6 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _messageReferenceController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ссылка на сообщение',
|
||||
hintText: 'Необязательно',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -612,14 +513,8 @@ class _AiRuleEditDialogState extends State<AiRuleEditDialog> {
|
||||
final rule = AiRule(
|
||||
id: widget.rule?.id,
|
||||
name: _nameController.text.trim(),
|
||||
description: _descriptionController.text.trim(),
|
||||
type: _selectedType,
|
||||
isActive: _isActive,
|
||||
priority: _priority,
|
||||
confidencePercentage: _confidencePercentage,
|
||||
messageReference: _messageReferenceController.text.trim().isEmpty
|
||||
? null
|
||||
: _messageReferenceController.text.trim(),
|
||||
processingStatus: _processingStatus,
|
||||
merchantPattern: _selectedType == AiRuleType.pointOfSale
|
||||
? _merchantPatternController.text.trim()
|
||||
|
||||
@@ -53,18 +53,6 @@ class AiRuleListItem extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Описание
|
||||
if (rule.description.isNotEmpty) ...[
|
||||
Text(
|
||||
rule.description,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Детали правила
|
||||
_buildRuleDetails(context),
|
||||
@@ -74,62 +62,37 @@ class AiRuleListItem extends StatelessWidget {
|
||||
// Нижняя панель с действиями
|
||||
Row(
|
||||
children: [
|
||||
// Приоритет и уверенность
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.priority_high,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
// Уверенность (только для статусов "создано" и "требует внимания")
|
||||
if (rule.processingStatus == ProcessingStatus.created ||
|
||||
rule.processingStatus == ProcessingStatus.needsAttention) ...
|
||||
[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'П: ${rule.priority}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.percent,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${rule.confidencePercentage}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.percent,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${rule.confidencePercentage}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const Spacer(),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user