From 785fe72449e50e05c13a404419df6984e3b0c326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Tue, 26 Aug 2025 17:17:42 +0200 Subject: [PATCH] First version of a formula widget --- README.md | 1 - lib/ai/FormulaWidget.dart | 366 ++++++++++++++++++++++++++++++++++ lib/formula_evaluator.dart | 4 +- lib/formula_models.dart | 17 +- test/formula_models_test.dart | 43 ++++ 5 files changed, 423 insertions(+), 8 deletions(-) create mode 100644 lib/ai/FormulaWidget.dart diff --git a/README.md b/README.md index 1ed8570..f3e695f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ The file is a dart array of formulas. Each formula is a dart set literal } ] - ``` ## Features diff --git a/lib/ai/FormulaWidget.dart b/lib/ai/FormulaWidget.dart new file mode 100644 index 0000000..193df1a --- /dev/null +++ b/lib/ai/FormulaWidget.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; + +class VariableSpec { + final String name; + final String magnitude; + static final MAGNITUDELESS = "magnitudeless"; + + VariableSpec({required this.name, required this.magnitude}); + + @override + String toString() => 'var($name: $magnitude)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VariableSpec && + runtimeType == other.runtimeType && + magnitude == other.magnitude && + name == other.name; + + @override + int get hashCode => Object.hash(magnitude, name); +} + +class Formula { + final String name; + final List input; + final VariableSpec output; + final String d4rtCode; + + Formula({ + required this.name, + required this.input, + required this.output, + required this.d4rtCode, + }); +} + +class FormulaWidget extends StatelessWidget { + final Formula formula; + final double fontSize; + final Color? textColor; + final Color? backgroundColor; + final EdgeInsets padding; + final bool showMagnitudes; + final bool showCode; + + const FormulaWidget({ + Key? key, + required this.formula, + this.fontSize = 16.0, + this.textColor, + this.backgroundColor, + this.padding = const EdgeInsets.all(16.0), + this.showMagnitudes = true, + this.showCode = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + color: backgroundColor, + child: Padding( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Formula name + Text( + formula.name, + style: TextStyle( + fontSize: fontSize * 1.2, + fontWeight: FontWeight.bold, + color: textColor ?? Theme.of(context).textTheme.headlineSmall?.color, + ), + ), + const SizedBox(height: 12), + + // Formula equation + _buildFormulaEquation(context), + + if (showMagnitudes) ...[ + const SizedBox(height: 16), + _buildMagnitudesSection(context), + ], + + if (showCode) ...[ + const SizedBox(height: 16), + _buildCodeSection(context), + ], + ], + ), + ), + ); + } + + Widget _buildFormulaEquation(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + child: Row( + children: [ + // Output variable + _buildVariableChip(formula.output, isOutput: true, context: context), + + const SizedBox(width: 12), + + // Equals sign + Text( + '=', + style: TextStyle( + fontSize: fontSize * 1.5, + fontWeight: FontWeight.bold, + color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + + const SizedBox(width: 12), + + // Function notation + Text( + '${formula.name}(', + style: TextStyle( + fontSize: fontSize, + fontStyle: FontStyle.italic, + color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + + // Input variables + Expanded( + child: Wrap( + spacing: 8, + runSpacing: 4, + children: [ + for (int i = 0; i < formula.input.length; i++) ...[ + _buildVariableChip(formula.input[i], context: context), + if (i < formula.input.length - 1) + Text( + ',', + style: TextStyle( + fontSize: fontSize, + color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + ], + ], + ), + ), + + Text( + ')', + style: TextStyle( + fontSize: fontSize, + fontStyle: FontStyle.italic, + color: textColor ?? Theme.of(context).textTheme.bodyLarge?.color, + ), + ), + ], + ), + ); + } + + Widget _buildVariableChip(VariableSpec variable, {bool isOutput = false, required BuildContext context}) { + final bool hasMagnitude = variable.magnitude != VariableSpec.MAGNITUDELESS; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isOutput + ? Theme.of(context).colorScheme.primary.withOpacity(0.1) + : Theme.of(context).colorScheme.secondary.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isOutput + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondary, + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + variable.name, + style: TextStyle( + fontSize: fontSize * 0.9, + fontWeight: FontWeight.w600, + color: isOutput + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondary, + ), + ), + if (hasMagnitude && showMagnitudes) ...[ + const SizedBox(width: 4), + Text( + '[${variable.magnitude}]', + style: TextStyle( + fontSize: fontSize * 0.8, + fontStyle: FontStyle.italic, + color: (isOutput + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondary).withOpacity(0.7), + ), + ), + ], + ], + ), + ); + } + + Widget _buildMagnitudesSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Variables:', + style: TextStyle( + fontSize: fontSize * 0.9, + fontWeight: FontWeight.w600, + color: textColor ?? Theme.of(context).textTheme.titleSmall?.color, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 12, + runSpacing: 6, + children: [ + ...formula.input.map((variable) => _buildVariableInfo(variable, context)), + _buildVariableInfo(formula.output, context, isOutput: true), + ], + ), + ], + ); + } + + Widget _buildVariableInfo(VariableSpec variable, BuildContext context, {bool isOutput = false}) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isOutput ? Icons.arrow_forward : Icons.input, + size: fontSize * 0.8, + color: isOutput + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: 4), + Text( + '${variable.name}: ', + style: TextStyle( + fontSize: fontSize * 0.85, + fontWeight: FontWeight.w500, + color: textColor ?? Theme.of(context).textTheme.bodyMedium?.color, + ), + ), + Text( + variable.magnitude == VariableSpec.MAGNITUDELESS ? 'dimensionless' : variable.magnitude, + style: TextStyle( + fontSize: fontSize * 0.85, + fontStyle: FontStyle.italic, + color: (textColor ?? Theme.of(context).textTheme.bodyMedium?.color)?.withOpacity(0.8), + ), + ), + ], + ); + } + + Widget _buildCodeSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Implementation:', + style: TextStyle( + fontSize: fontSize * 0.9, + fontWeight: FontWeight.w600, + color: textColor ?? Theme.of(context).textTheme.titleSmall?.color, + ), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + formula.d4rtCode, + style: TextStyle( + fontSize: fontSize * 0.8, + fontFamily: 'monospace', + color: textColor ?? Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ), + ], + ); + } +} + +// Example usage and demo +class FormulaDisplayDemo extends StatelessWidget { + @override + Widget build(BuildContext context) { + // Create sample formulas + final sampleFormulas = [ + Formula( + name: 'calculateArea', + input: [ + VariableSpec(name: 'length', magnitude: 'meters'), + VariableSpec(name: 'width', magnitude: 'meters'), + ], + output: VariableSpec(name: 'area', magnitude: 'square_meters'), + d4rtCode: 'return length * width;', + ), + Formula( + name: 'pythagoras', + input: [ + VariableSpec(name: 'a', magnitude: 'meters'), + VariableSpec(name: 'b', magnitude: 'meters'), + ], + output: VariableSpec(name: 'c', magnitude: 'meters'), + d4rtCode: 'return sqrt(a * a + b * b);', + ), + Formula( + name: 'normalize', + input: [ + VariableSpec(name: 'value', magnitude: VariableSpec.MAGNITUDELESS), + VariableSpec(name: 'max', magnitude: VariableSpec.MAGNITUDELESS), + ], + output: VariableSpec(name: 'normalized', magnitude: VariableSpec.MAGNITUDELESS), + d4rtCode: 'return value / max;', + ), + ]; + + return Scaffold( + appBar: AppBar( + title: const Text('Formula Display Demo'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + for (final formula in sampleFormulas) ...[ + FormulaWidget( + formula: formula, + showCode: true, + ), + const SizedBox(height: 16), + ], + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index 93c11e9..a0e04e4 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -48,9 +48,7 @@ class FormulaEvaluator { try { // Build the complete d4rt source code with variable declarations final completeSource = _buildCompleteSource(formula, inputValues); - //print( "$formula"); - print( "$completeSource" ); - + // Execute the code using d4rt (no args needed since variables are in source) final result = _interpreter.execute(source: completeSource); diff --git a/lib/formula_models.dart b/lib/formula_models.dart index 6506dba..1021af5 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -75,6 +75,19 @@ class Formula { return Formula.fromSet(setLiteral); } + static List fromArrayStringLiteral( String arrayStringLiteral ){ + var d4rt = D4rt(); + final buffer = StringBuffer(); + buffer.write( "main(){ return $arrayStringLiteral; }"); + final code = buffer.toString(); + + final List list = d4rt.execute(source: code); + + final formulas = list.map( (set) => Formula.fromSet(set as Map) ); + + return formulas.toList(growable: false) as List; + } + factory Formula.fromSet(Map theSet) { Object safeGet(Map map, String key){ @@ -92,10 +105,6 @@ class Formula { return safeGet(map,key) as List; } - Map mapValue(Map map, String key){ - return safeGet(map,key) as Map; - } - VariableSpec parseVar(Map varSpec) { String name = stringValue(varSpec, "name"); String magnitude = stringValue(varSpec, "magnitude"); diff --git a/test/formula_models_test.dart b/test/formula_models_test.dart index e7566bb..e572ba7 100644 --- a/test/formula_models_test.dart +++ b/test/formula_models_test.dart @@ -58,5 +58,48 @@ void main() { }); + + test( 'd4rt parses formula from list literal', (){ + final literal = """ + [ + { + "name": "Newton's second law", + "input": [ + { "name": 'm', "magnitude": 'mass'}, + { "name": 'a', "magnitude": 'acceleration'} + ], + "output": { "name": 'F', "magnitude": 'force'}, + "d4rtCode": ''' + F = a * m; + ''' + }, + { + "name": "Newton's second law, again", + "input": [ + { "name": 'mass', "magnitude": 'mass'}, + { "name": 'acc', "magnitude": 'acceleration'} + ], + "output": { "name": 'force', "magnitude": 'force'}, + "d4rtCode": ''' + force = mass * acc; + ''' + } + ] + """; + + final formulas = Formula.fromArrayStringLiteral(literal); + final evaluator = FormulaEvaluator(); + + final formula = formulas[0]; + + final result = evaluator.evaluate(formula, { + 'm': 10.0, // 10 kg + 'a': 9.8, // 9.8 m/s² + }); + + expect(result, 98.0); // F = m * a = 10 * 9.8 = 98 N + + }); + }