d4t_formulas/lib/ai/formula_screen.dart

315 lines
8.8 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../formula_models.dart';
import '../formula_evaluator.dart';
import '../corpus.dart';
import 'unit_dropdown.dart';
class FormulaScreen extends StatefulWidget {
final Formula formula;
2025-09-20 14:46:21 +00:00
final Corpus corpus;
const FormulaScreen({super.key, required this.formula, required this.corpus});
@override
State<FormulaScreen> createState() => _FormulaScreenState();
}
2026-01-25 17:39:43 +00:00
// TODO: Create VariableWidget. Depending on VariableSpec.values, it can be a ValueDropdown or a D4rtEditingController
// The d4rtValue will be FormulaResult?
//// Start of D4rtEditingController class ////
class D4rtEditingController extends TextEditingController {
String? _lastError;
String? get lastError => _lastError;
FormulaResult? _lastValue;
2026-01-25 17:39:43 +00:00
D4rtEditingController({super.text});
bool validate() {
try {
_lastValue = null;
_lastValue = FormulaEvaluator.evaluateExpression(text);
_lastError = null;
return true;
} catch (e, s) {
_lastError = e.toString();
print( "validate: $text: $e" );
print( "stack: $s" );
return false;
}
}
2026-01-25 17:39:43 +00:00
FormulaResult? get d4rtValue => _lastValue;
set text(String newText) {
super.text = newText;
validate();
}
@override
void notifyListeners() {
validate();
super.notifyListeners();
}
}
//// End of D4rtEditingController class ////
class _FormulaScreenState extends State<FormulaScreen> {
final _formKey = GlobalKey<FormState>();
final Map<String, D4rtEditingController> _inputControllers = {};
final Map<String, String?> _selectedUnits = {};
String? _result;
String? _selectedOutputUnit;
2025-10-14 17:21:35 +00:00
@override
void initState() {
super.initState();
// Initialize controllers and units with listeners
for (final input in widget.formula.input) {
_inputControllers[input.name] = D4rtEditingController();
_selectedUnits[input.name] = input.unit;
_inputControllers[input.name]!.addListener(_evaluateFormula);
}
_selectedOutputUnit = widget.formula.output.unit;
}
@override
void dispose() {
// Clean up controllers and listeners
for (final controller in _inputControllers.values) {
controller.removeListener(_evaluateFormula);
controller.dispose();
}
super.dispose();
}
void _evaluateFormula() {
if (!_formKey.currentState!.validate()) return;
try {
final inputValues = <String, dynamic>{};
for (final input in widget.formula.input) {
final controller = _inputControllers[input.name]!;
if (controller.d4rtValue == null) {
//throw FormulaEvaluationException("Field ${input.name} is invalid");
_result = "";
return;
}
late final dynamic convertedValue;
switch (controller.d4rtValue) {
case NumberResult nr:
// Convert input to base unit if needed
// Always convert from dropdown unit to variable's base unit
if (input.unit != null) {
convertedValue = widget.corpus.convert(
nr.value,
_selectedUnits[input.name]!,
input.unit as String,
);
} else {
convertedValue = nr.value;
}
case StringResult sr:
convertedValue = sr.value;
default:
throw FormulaEvaluationException(
"Field ${input.name} has unsupported type ${controller.d4rtValue!.runtimeType}",
);
}
inputValues[input.name] = convertedValue;
}
final evaluator = FormulaEvaluator();
final result = evaluator.evaluate(widget.formula, inputValues);
// Convert output to selected unit if needed
2025-11-09 19:29:58 +00:00
String? unit = widget.formula.output.unit;
if (unit != null) {
_result = widget.corpus
.convert(result, unit, _selectedOutputUnit!)
.toStringAsFixed(2);
} else {
2025-11-09 19:29:58 +00:00
_result = result;
}
2025-10-14 17:21:35 +00:00
//print( "_evaluateFormula: result:${result} _result:${_result}");
setState(() {});
} catch (e, stack) {
debugPrint('Formula evaluation error: $e');
debugPrint('Stack trace: $stack');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${e.toString()}\n${stack.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.formula.name)),
body: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
_buildDescriptionSection(),
_buildInputSection(),
const SizedBox(height: 24),
_buildOutputSection(),
],
),
),
),
);
}
Widget _buildDescriptionSection() {
if (widget.formula.description == null ||
widget.formula.description!.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Description',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: MarkdownBody(
data: widget.formula.description!,
shrinkWrap: true,
),
),
const SizedBox(height: 24),
],
);
}
Widget _buildInputSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Input Variables',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...widget.formula.input.map((variable) => _buildVariableRow(variable)),
],
);
}
Widget _buildOutputSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Result',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
Text(widget.formula.output.name),
const Spacer(),
SizedBox(
2025-10-05 14:53:46 +00:00
width: 150,
child: TextFormField(
readOnly: true,
enabled: false,
controller: TextEditingController(text: _result),
decoration: const InputDecoration(
border: UnderlineInputBorder(),
filled: true,
),
),
),
const SizedBox(width: 8),
UnitDropdown(
corpus: widget.corpus,
variable: widget.formula.output,
selectedUnit: _selectedOutputUnit,
onUnitChanged: (unit) {
setState(() {
_selectedOutputUnit = unit;
});
_evaluateFormula();
},
),
],
),
],
);
}
Widget _buildVariableRow(VariableSpec variable) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Text(variable.name),
const Spacer(),
SizedBox(
width: 100,
child: TextFormField(
controller: _inputControllers[variable.name],
keyboardType: TextInputType.number,
inputFormatters: [
2025-10-13 07:29:58 +00:00
//FilteringTextInputFormatter.allow(RegExp(r'[0-9\.\-]')),
],
decoration: const InputDecoration(
border: UnderlineInputBorder(),
),
autovalidateMode: AutovalidateMode.always,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return _inputControllers[variable.name]!.lastError;
},
),
),
const SizedBox(width: 8),
UnitDropdown(
corpus: widget.corpus,
variable: variable,
selectedUnit: _selectedUnits[variable.name],
onUnitChanged: (unit) {
setState(() {
_selectedUnits[variable.name] = unit;
});
_evaluateFormula();
},
),
],
),
);
}
}