First version of a formula widget
This commit is contained in:
parent
2472e0db7c
commit
785fe72449
5 changed files with 423 additions and 8 deletions
|
|
@ -33,7 +33,6 @@ The file is a dart array of formulas. Each formula is a dart set literal
|
||||||
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
|
||||||
366
lib/ai/FormulaWidget.dart
Normal file
366
lib/ai/FormulaWidget.dart
Normal file
|
|
@ -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<VariableSpec> 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),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,8 +48,6 @@ class FormulaEvaluator {
|
||||||
try {
|
try {
|
||||||
// Build the complete d4rt source code with variable declarations
|
// Build the complete d4rt source code with variable declarations
|
||||||
final completeSource = _buildCompleteSource(formula, inputValues);
|
final completeSource = _buildCompleteSource(formula, inputValues);
|
||||||
//print( "$formula");
|
|
||||||
print( "$completeSource" );
|
|
||||||
|
|
||||||
// Execute the code using d4rt (no args needed since variables are in source)
|
// Execute the code using d4rt (no args needed since variables are in source)
|
||||||
final result = _interpreter.execute(source: completeSource);
|
final result = _interpreter.execute(source: completeSource);
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,19 @@ class Formula {
|
||||||
return Formula.fromSet(setLiteral);
|
return Formula.fromSet(setLiteral);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<Formula> fromArrayStringLiteral( String arrayStringLiteral ){
|
||||||
|
var d4rt = D4rt();
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
buffer.write( "main(){ return $arrayStringLiteral; }");
|
||||||
|
final code = buffer.toString();
|
||||||
|
|
||||||
|
final List<Object?> list = d4rt.execute(source: code);
|
||||||
|
|
||||||
|
final formulas = list.map( (set) => Formula.fromSet(set as Map) );
|
||||||
|
|
||||||
|
return formulas.toList(growable: false) as List<Formula>;
|
||||||
|
}
|
||||||
|
|
||||||
factory Formula.fromSet(Map<Object?, Object?> theSet) {
|
factory Formula.fromSet(Map<Object?, Object?> theSet) {
|
||||||
|
|
||||||
Object safeGet(Map<Object?, Object?> map, String key){
|
Object safeGet(Map<Object?, Object?> map, String key){
|
||||||
|
|
@ -92,10 +105,6 @@ class Formula {
|
||||||
return safeGet(map,key) as List<Object?>;
|
return safeGet(map,key) as List<Object?>;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object?> mapValue(Map<Object?, Object?> map, String key){
|
|
||||||
return safeGet(map,key) as Map<String, Object?>;
|
|
||||||
}
|
|
||||||
|
|
||||||
VariableSpec parseVar(Map<Object?, Object?> varSpec) {
|
VariableSpec parseVar(Map<Object?, Object?> varSpec) {
|
||||||
String name = stringValue(varSpec, "name");
|
String name = stringValue(varSpec, "name");
|
||||||
String magnitude = stringValue(varSpec, "magnitude");
|
String magnitude = stringValue(varSpec, "magnitude");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue