feat(species): auto-classify variety species from its label
Infer the catalog species a free-text variety label names ("Maiz de la
abuela" -> Zea mays) and prefill the category from the species family.
- Pure, testable matcher (domain/species_autoclassify.dart): whole-word,
accent/case-insensitive, Unicode-aware (any script), longest-name-wins,
ambiguous names left unclassified, light plural fold.
- Quick-add and draft naming auto-link the species when the field is empty
(non-destructive; an explicit category is kept).
- Edit sheet offers a one-tap suggestion from the typed name.
Tests: matcher unit, repository integration, SpeciesRepository.classifyLabel,
and an edit-sheet widget test.
This commit is contained in:
parent
0e6ef13d00
commit
b840b83c42
15 changed files with 510 additions and 3 deletions
|
|
@ -605,6 +605,11 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
|
||||
List<SpeciesMatch> _suggestions = const [];
|
||||
String? _pickedSpeciesId;
|
||||
|
||||
/// A species inferred from the typed name, offered as a one-tap suggestion.
|
||||
/// Only surfaced while the variety has no species and the grower has not
|
||||
/// picked one, so it never nags over a deliberate choice.
|
||||
SpeciesMatch? _suggested;
|
||||
late bool _isOrganic = widget.detail.isOrganic;
|
||||
late bool _needsReproduction = widget.detail.needsReproduction;
|
||||
late int? _sowMonths = widget.detail.sowMonths;
|
||||
|
|
@ -626,14 +631,26 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
Future<void> _onSpeciesQuery(String query) async {
|
||||
final lang = Localizations.localeOf(context).languageCode;
|
||||
final results = await widget.species.search(query, languageCode: lang);
|
||||
// A manual species search takes over from the name-based suggestion.
|
||||
if (mounted) setState(() => _suggestions = results);
|
||||
}
|
||||
|
||||
/// As the name is typed, offer the species it names — but only when the
|
||||
/// variety has no species yet and the grower hasn't picked one, so an
|
||||
/// existing link is never second-guessed.
|
||||
Future<void> _onNameChanged(String name) async {
|
||||
if (widget.detail.speciesId != null || _pickedSpeciesId != null) return;
|
||||
final lang = Localizations.localeOf(context).languageCode;
|
||||
final match = await widget.species.classifyLabel(name, languageCode: lang);
|
||||
if (mounted) setState(() => _suggested = match);
|
||||
}
|
||||
|
||||
void _pick(SpeciesMatch match) {
|
||||
setState(() {
|
||||
_pickedSpeciesId = match.id;
|
||||
_speciesField.text = match.displayLabel;
|
||||
_suggestions = const [];
|
||||
_suggested = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -694,6 +711,7 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
onChanged: _onNameChanged,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
|
|
@ -719,6 +737,19 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
: Text(match.family!),
|
||||
onTap: () => _pick(match),
|
||||
),
|
||||
// One-tap suggestion inferred from the name (only when the
|
||||
// species field is idle, so it never competes with a manual
|
||||
// search).
|
||||
if (_suggested case final suggested?
|
||||
when _suggestions.isEmpty && _pickedSpeciesId == null)
|
||||
ListTile(
|
||||
dense: true,
|
||||
key: const Key('editVariety.speciesSuggestion'),
|
||||
leading: const Icon(Icons.auto_awesome_outlined),
|
||||
title: Text(suggested.displayLabel),
|
||||
subtitle: Text(t.editVariety.speciesSuggested),
|
||||
onTap: () => _pick(suggested),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _category,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue