d4t_formulas/lib/ai/formula_screen.dart

440 lines
13 KiB
Dart
Raw Normal View History

2026-01-25 18:20:28 +00:00
// dart
import 'package:flutter/material.dart';
2026-02-16 13:19:10 +00:00
import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
2026-02-07 15:16:00 +00:00
import 'package:markdown/markdown.dart' as markdown;
import '../formula_models.dart';
import '../formula_evaluator.dart';
import '../corpus.dart';
2026-02-09 15:57:53 +00:00
import '../error_handler.dart';
import 'unit_dropdown.dart';
2026-02-25 08:08:06 +00:00
import 'formula_editor.dart';
class FormulaScreen extends StatefulWidget {
Formula formula;
2025-09-20 14:46:21 +00:00
final Corpus corpus;
FormulaScreen({super.key, required this.formula, required this.corpus});
@override
State<FormulaScreen> createState() => _FormulaScreenState();
}
//// Start of D4rtEditingController class ////
class D4rtEditingController extends TextEditingController {
String? _lastError;
String? get lastError => _lastError;
FormulaResult? _lastValue;
2026-02-27 10:29:15 +00:00
final bool isString;
2026-02-27 10:29:15 +00:00
D4rtEditingController({super.text, this.isString = false});
bool validate() {
2026-02-27 10:29:15 +00:00
if( _validateAsNumberExpression(text) ){
return true;
}
if( isString && _validateAsStringExpression(text) ){
return true;
}
return false;
}
bool _validateAsNumberExpression(String text){
return _validateAsD4rtExpression(text) && _lastValue is NumberResult;
}
bool _validateAsD4rtExpression(String text){
try {
_lastValue = null;
if( text.trim().isEmpty ){
return true;
}
_lastValue = FormulaEvaluator.evaluateExpression(text);
_lastError = null;
return true;
} catch (e, s) {
2026-02-27 10:29:15 +00:00
//errorHandler.notify(e, s);
_lastError = e.toString();
return false;
}
}
2026-02-27 10:29:15 +00:00
bool _validateAsStringExpression(String text){
if( _validateAsD4rtExpression(text) && _lastValue is StringResult ){
return true;
}
if( _validateAsD4rtExpression('"' + text + '"') && _lastValue is StringResult ){
return true;
}
if( _validateAsD4rtExpression("'" + text + "'") && _lastValue is StringResult ){
return true;
}
return false;
}
2026-01-25 17:39:43 +00:00
FormulaResult? get d4rtValue => _lastValue;
2026-02-28 13:22:18 +00:00
@override
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 = {};
2026-01-25 18:20:28 +00:00
final Map<String, String?> _selectedValues = {}; // for string dropdowns
String? _result;
String? _selectedOutputUnit;
2026-02-11 07:45:56 +00:00
bool _isDescriptionExpanded = false; // Track description expansion state
@override
void initState() {
super.initState();
// Initialize controllers and units with listeners
for (final input in widget.formula.input) {
_selectedUnits[input.name] = input.unit;
2026-01-25 18:20:28 +00:00
if (input.values != null && input.values!.isNotEmpty) {
// string/categorical variable -> use dropdown
_selectedValues[input.name] = input.values!.first;
} else {
// numeric variable -> use D4rtEditingController
2026-02-27 10:29:15 +00:00
_inputControllers[input.name] = D4rtEditingController(isString: input.unit == "string");
2026-01-25 18:20:28 +00:00
_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() {
try {
final inputValues = <String, dynamic>{};
for (final input in widget.formula.input) {
2026-01-25 18:20:28 +00:00
// string/categorical variable
if (input.values != null && input.values!.isNotEmpty) {
final selected = _selectedValues[input.name];
if (selected == null) {
_result = "";
return;
}
inputValues[input.name] = selected;
continue;
}
// numeric variable - must have controller
final controller = _inputControllers[input.name]!;
2026-01-25 18:20:28 +00:00
final val = controller.d4rtValue;
if (val == null) {
_result = "";
return;
}
2026-01-25 18:20:28 +00:00
dynamic convertedValue;
if (val is NumberResult) {
if (input.unit != null) {
convertedValue = widget.corpus.convert(
val.value,
_selectedUnits[input.name]!,
input.unit as String,
);
2026-01-25 18:20:28 +00:00
} else {
convertedValue = val.value;
}
} else if (val is StringResult) {
convertedValue = val.value;
} else {
throw FormulaEvaluationException(
"Field ${input.name} has unsupported type ${val.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 is Number) {
2026-01-25 18:20:28 +00:00
final converted = widget.corpus.convert(result, unit, _selectedOutputUnit!);
if (converted is num) {
_result = converted.toStringAsFixed(2);
} else {
_result = converted.toString();
}
} else {
2026-01-25 18:20:28 +00:00
_result = result?.toString();
2025-11-09 19:29:58 +00:00
}
2025-10-14 17:21:35 +00:00
setState(() {});
} catch (e, stack) {
2026-02-09 15:57:53 +00:00
errorHandler.notify(e, stack);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
2026-02-09 15:57:53 +00:00
content: Text('Error: ${e.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
2026-02-25 08:08:06 +00:00
appBar: AppBar(
title: Text(widget.formula.name),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FormulaEditor(
formula: widget.formula,
corpus: widget.corpus,
2026-02-26 20:49:32 +00:00
onSave: (updatedFormula) {
// Refresh the screen after saving
setState(() {
// The corpus has been updated, refresh the displayed formula
widget.formula = updatedFormula;
2026-02-26 20:49:32 +00:00
});
},
2026-02-25 08:08:06 +00:00
),
),
);
},
tooltip: 'Edit Formula',
),
],
),
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();
}
2026-02-09 16:10:47 +00:00
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: ExpansionTile(
title: Text(
'Description',
2026-02-09 16:10:47 +00:00
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
2026-02-09 16:10:47 +00:00
),
initiallyExpanded: _isDescriptionExpanded,
onExpansionChanged: (bool expanded) {
setState(() {
_isDescriptionExpanded = expanded;
});
},
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
2026-02-16 13:19:10 +00:00
child: Markdown(
2026-02-09 16:10:47 +00:00
data: widget.formula.description!,
shrinkWrap: true,
builders: {
'latex': LatexElementBuilder(),
},
extensionSet: markdown.ExtensionSet(
[LatexBlockSyntax()],
[LatexInlineSyntax()],
),
),
2026-02-07 15:16:00 +00:00
),
),
2026-02-09 16:10:47 +00:00
],
),
);
}
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: [
// Fixed width for field name
SizedBox(
2025-10-05 14:53:46 +00:00
width: 150,
child: Text(
widget.formula.output.name,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8), // Add some spacing
// Flexible space for result field
Expanded(
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) {
_selectedOutputUnit = unit;
_evaluateFormula();
print( "En output unit changed to $unit: $_result");
setState(() {
});
},
),
],
),
],
);
}
Widget _buildVariableRow(VariableSpec variable) {
2026-01-25 18:20:28 +00:00
final isCategorical = variable.values != null && variable.values!.isNotEmpty;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
// Fixed width for field name
SizedBox(
width: 150,
child: Text(
variable.name,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8), // Add some spacing
// Flexible space for input field
Expanded(
child: isCategorical
? DropdownButtonFormField<String>(
value: _selectedValues[variable.name],
items: variable.values!
.map((v) => DropdownMenuItem<String>(value: v, child: Text(v)))
.toList(),
onChanged: (v) {
_selectedValues[variable.name] = v;
_evaluateFormula();
setState(() {
});
},
decoration: const InputDecoration(
border: UnderlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) return 'Required';
return null;
},
)
: TextFormField(
controller: _inputControllers[variable.name],
keyboardType: TextInputType.number,
inputFormatters: [
//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;
},
2026-01-25 18:20:28 +00:00
),
),
const SizedBox(width: 8),
if (variable.unit != null)
UnitDropdown(
corpus: widget.corpus,
variable: variable,
selectedUnit: _selectedUnits[variable.name],
onUnitChanged: (unit) {
setState(() {
_selectedUnits[variable.name] = unit;
});
_evaluateFormula();
},
2026-01-25 18:20:28 +00:00
),
],
),
);
}
}