diff --git a/TODO.md b/TODO.md index ca55009..8b6f6af 100644 --- a/TODO.md +++ b/TODO.md @@ -32,8 +32,18 @@ - [X] Add a Share button to the formula list. It will export the array string literal of the formula with the units from Corpus.withDependencies(). - [X] Replace flutter-markdown with flutter-markdown-plus - [X] Heron's formula: investigate why a=3, b=40, c=5 yields NaN. Root cause: input values don't form a valid triangle (violate triangle inequality: 3+5=8 is not > 40). Added documentation note to the formula description. -- [R] Refactor ./assets/formulas d4rt files: - - [R] Pretty print files as dart literals (like JSON, but allow raw strings r"""like this""") - - [R] Ensure there is no formula duplicates. If necesary, move or delete the formula in file formulas.d4rt - - [R] defaultCorpus must load all formula files -- [ ] Investigate starup time when there is no previous database and corpus is loaded from assets. +- [X] Refactor ./assets/formulas d4rt files: + - [X] Pretty print files as dart literals (like JSON, but allow raw strings r"""like this""") + - [X] Ensure there is no formula duplicates. If necesary, move or delete the formula in file formulas.d4rt + - [X] defaultCorpus must load all formula files +- [X] Create a formula in ./assets/formulas/networking.d4art: input is a string with ip address and mask, output is ip subnet of this address and broadcast address. +- [R] Develop a new screen that edits a formula in ./lib/ai directory: + - [R] FormulaEditor initializes with a Formula + - [R] A textfield allows editing the "name" of the formula + - [R] A text area allows editing the "description". A button pops up a preview of the markdown. + - [R] There is one row per input variable. The "name" is a textfield. A first drop down allows to select the base unit, and a second dropdown is populated with all derived units of the selected base unit, and the base unit. The unit of the input variable is the derived unit. + - [R] Each input variable can be deleted with a button + - [R] A button after the inputs variables allows to insert a new input variable + - [R] There is one row for the ouput variable, similar to the row for the input variable + - [R] d4rtCode is a text area with dart syntax highligthing + - [R] At the botton, a button allows to test the edited Formula, launching a FormulaScreen diff --git a/assets/formula-element-format.md b/assets/formula-element-format.md new file mode 100644 index 0000000..3eb8710 --- /dev/null +++ b/assets/formula-element-format.md @@ -0,0 +1,507 @@ +# Formula and Unit File Format Guide + +This document describes the format for contributing formulas and units to the d4rt_formulas project. It is intended for formula contributors and developers. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Formula Files](#formula-files) +3. [Unit Files](#unit-files) +4. [Writing Descriptions](#writing-descriptions) +5. [Best Practices](#best-practices) +6. [Examples](#examples) + +--- + +## Overview + +The project uses two types of asset files: + +| File Type | Location | Extension | Format | +|--------------|--------------------|---------------|---------------------------------| +| **Formulas** | `assets/formulas/` | `.d4rt` | Dart array literals (JSON-like) | +| **Units** | `assets/units/` | `.d4rt.units` | Dart array literals (JSON-like) | + +Both formats use Dart set/array literals with map entries. Files are parsed at runtime to populate the formula calculator. + +--- + +## Formula Files + +### File Structure + +Formula files are organized by topic (e.g., `geometry.d4rt`, `electromagnetism.d4rt`). Each file contains a Dart array literal with formula objects: + +```dart +[ + { + "name": "Formula Name", + "description": r"""Markdown description with LaTeX""", + "input": [ + {"name": "variable1", "unit": "unit_name"}, + {"name": "variable2", "unit": "unit_name"} + ], + "output": {"name": "result", "unit": "unit_name"}, + "d4rtCode": "result = expression;", + "tags": ["tag1", "tag2"] + }, + // More formulas... +] +``` + +### Formula Object Fields + +| Field | Type | Required | Description | +|---------------|--------|----------|------------------------------------------------------------------------------------------| +| `name` | String | Yes | Human-readable formula name | +| `description` | String | Yes | Markdown description with LaTeX math (see [Writing Descriptions](#writing-descriptions)) | +| `input` | Array | Yes | List of input variables with their units | +| `output` | Object | Yes | Output variable name and unit | +| `d4rtCode` | String | Yes | Dart code that computes the result | +| `tags` | Array | Yes | Categorization tags for search/filter | + +### Input/Output Format + +**Input variables:** +```dart +"input": [ + {"name": "m", "unit": "kilogram"}, + {"name": "a", "unit": "meters per square second"} +] +``` + +**Output variable:** +```dart +"output": {"name": "F", "unit": "newton"} +``` + +### Unit Names + +Unit names must match entries in the `assets/units/` directory. Use the full unit name (lowercase), not the symbol: + +| Correct | Incorrect | +|-----------------------|-----------| +| `"meter"` | `"m"` | +| `"kilogram"` | `"kg"` | +| `"meters per second"` | `"m/s"` | +| `"square meter"` | `"m²"` | + +### Dart Code (`d4rtCode`) + +The `d4rtCode` field contains valid Dart code that: +- Uses input variable names directly +- Assigns the result to the output variable name +- Can use Dart's `math` library functions (`sin`, `cos`, `sqrt`, `pow`, `pi`, etc.) + +**Simple formula:** +```dart +"d4rtCode": "F = m * a;" +``` + +**Multi-line formula:** +```dart +"d4rtCode": """ + var radians = angle * (pi / 180); + result = sin(radians); +""" +``` + +**With validation:** +```dart +"d4rtCode": """ + if (a + b < c) { + signal("Invalid triangle: sides do not satisfy triangle inequality"); + } + var s = (a + b + c) / 2; + A = sqrt(s * (s - a) * (s - b) * (s - c)); +""" +``` + +--- + +## Unit Files + +### File Structure + +Unit files define units of measurement organized by category (e.g., `distance.d4rt.units`, `force.d4rt.units`). Each file contains a Dart array literal with unit objects: + +```dart +[ + {"name": "meter", "symbol": "m", "isBase": true}, + {"name": "kilometer", "symbol": "km", "baseUnit": "meter", "factor": 1000}, + // More units... +] +``` + +### Unit Object Fields + +| Field | Type | Required | Description | +|------------|---------|-------------|---------------------------------------------------------------------| +| `name` | String | Yes | Full unit name (lowercase) | +| `symbol` | String | Yes | Unit symbol for display | +| `isBase` | Boolean | Conditional | `true` if this is a base unit (no conversion needed) | +| `baseUnit` | String | Conditional | Name of the base unit for conversion | +| `factor` | Number | Conditional | Multiplication factor to convert to base unit | +| `toBase` | String | Conditional | Expression/code to convert to base unit (for complex conversions) | +| `fromBase` | String | Conditional | Expression/code to convert from base unit (for complex conversions) | + +### Base Units vs Derived Units + +**Base units** define the reference for a category: +```dart +{"name": "meter", "symbol": "m", "isBase": true} +{"name": "newton", "symbol": "N", "isBase": true} +{"name": "joule", "symbol": "J", "isBase": true} +{"name": "Kelvin", "symbol": "K", "isBase": true} +``` + +**Derived units** specify conversion to their base unit. There are two types: + +#### Simple Linear Conversions (using `factor`) + +For units where conversion is a simple multiplication: + +```dart +{"name": "kilometer", "symbol": "km", "baseUnit": "meter", "factor": 1000} +{"name": "inch", "symbol": "in", "baseUnit": "meter", "factor": 0.0254} +{"name": "pound-force", "baseUnit": "newton", "factor": 4.44822} +``` + +The `factor` converts **from** the defined unit **to** the base unit: + +```dart +// 1 kilometer = 1000 meters +{"name": "kilometer", "baseUnit": "meter", "factor": 1000} + +// 1 inch = 0.0254 meters +{"name": "inch", "baseUnit": "meter", "factor": 0.0254} +``` + +#### Complex Conversions (using `toBase` and `fromBase`) + +For units requiring non-linear conversions (e.g., temperature scales), use `toBase` and `fromBase` expressions. The variable `x` represents the value to convert. + +**Example: Celsius to Kelvin** +```dart +{ + "name": "Celsius", + "symbol": "°C", + "baseUnit": "Kelvin", + "toBase": "x + 273.15", // °C → K + "fromBase": "x - 273.15", // K → °C +} +``` + +**Example: Fahrenheit to Kelvin** +```dart +{ + "name": "Fahrenheit", + "symbol": "°F", + "baseUnit": "Kelvin", + "toBase": "(x - 32) * 5/9 + 273.15", // °F → K + "fromBase": "(x - 273.15) * 9/5 + 32", // K → °F +} +``` + +**Example: Multi-line conversion (Gas Mark to Kelvin)** +```dart +{ + "name": "Gas Mark", + "symbol": "GM", + "baseUnit": "Kelvin", + "toBase": r""" + if (x < 1) { + double celsius = (243 - 25 * (log(1 / x) / log(2))) / 1.8; + return celsius + 273.15; + } else { + double celsius = x * 14 + 121; + return celsius + 273.15; + } + """, + "fromBase": """ + double celsius = x - 273.15; + if (celsius < 135) { + return pow(2, (1.8 * celsius - 243) / 25); + } else { + return (celsius - 121) / 14; + } + """ +} +``` + +### Common Temperature Conversions + +| Unit | toBase (→ K) | fromBase (← K) | +|------------|------------------------------|------------------------------| +| Celsius | `x + 273.15` | `x - 273.15` | +| Fahrenheit | `(x - 32) * 5/9 + 273.15` | `(x - 273.15) * 9/5 + 32` | +| Rankine | `x * 5/9` | `x * 9/5` | +| Réaumur | `x * 5/4 + 273.15` | `(x - 273.15) * 4/5` | +| Delisle | `373.15 - x * 2/3` | `(373.15 - x) * 3/2` | +| Rømer | `(x - 7.5) * 40/21 + 273.15` | `(x - 273.15) * 21/40 + 7.5` | + +--- + +## Writing Descriptions + +The `description` field uses **raw Dart string literals** (`r"""..."""`) with **Markdown** and **LaTeX** math. + +### Format + +```dart +"description": r""" +Short description of the formula. + +$$F = m \cdot a$$ + +Where: +- $F$: Force (Newtons) +- $m$: Mass (kilograms) +- $a$: Acceleration (m/s²) + +Additional context or notes.""", +``` + +### LaTeX Math + +Use **MathJax/KaTeX** syntax for mathematical expressions: + +| Type | Syntax | Example | +|---------------------|-----------------------------|--------------------------| +| Inline math | `$...$` | `$F = ma$` | +| Display math | `$$...$$` | `$$E = mc^2$$` | +| Fractions | `\frac{a}{b}` | `$$\frac{1}{2}mv^2$$` | +| Subscripts | `x_i` | `$v_0$` | +| Superscripts | `x^2` | `$a^2 + b^2$` | +| Greek letters | `\alpha`, `\beta`, `\theta` | `$$\sin(\theta)$$` | +| Special symbols | `\cdot`, `\times`, `\pm` | `$m \cdot a$` | +| Units in math | `\mathrm{m/s^2}` | `$9.81\ \mathrm{m/s^2}$` | +| Scientific notation | `\times 10^{-11}` | `$6.674\times 10^{-11}$` | + +### Including Images + +Add Wikipedia or other educational images using Markdown: + +```markdown +![Description](https://upload.wikimedia.org/wikipedia/commons/...) +``` + +**Example:** +```dart +"description": r""" +Newton's law of universal gravitation. + +$$F = G\frac{m_1m_2}{r^2}$$ + +![Gravitation Diagram](https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/NewtonsLawOfUniversalGravitation.svg/1200px-NewtonsLawOfUniversalGravitation.svg.png)""", +``` + +### Description Structure + +A well-structured description includes: + +1. **Opening sentence** - Brief statement of what the formula calculates +2. **LaTeX formula** - The mathematical expression in display mode +3. **Variable definitions** - List of all variables with units +4. **Additional context** - Notes, assumptions, or applications +5. **Image** (optional) - Diagram or illustration + +--- + +## Best Practices + +### For Formulas + +1. **Use clear variable names** - Single letters for physics conventions (`F`, `m`, `a`), descriptive names when clarity matters +2. **Match units precisely** - Ensure input/output units match what the formula expects +3. **Add validation** - Use `signal()` for invalid inputs (e.g., triangle inequality) +4. **Include tags** - Add relevant tags for discoverability +5. **Use LaTeX for all math** - Even simple formulas should have LaTeX representation +6. **Add images** - Include diagrams from Wikipedia when helpful +7. **Comment your code** - Use `//` comments before each formula object + +### For Units + +1. **Use lowercase names** - `"meter"` not `"Meter"` +2. **Include common conversions** - Add both metric and imperial units when relevant +3. **Use standard symbols** - Follow SI conventions where applicable +4. **Document the factor** - Ensure conversion factors are accurate + +### For Descriptions + +1. **Be concise but complete** - Explain what the formula does and what each variable means +2. **Use consistent formatting** - Follow the established pattern in existing files +3. **Include units in variable definitions** - Always specify units for each variable +4. **Add context** - Explain when/why the formula is used +5. **Note assumptions** - Mention any constraints or special conditions + +--- + +## Examples + +### Complete Formula Example + +```dart +// Newton's Second Law +{ + "name": "Newton's Second Law", + "description": r""" +Force equals mass times acceleration. + +$$F = m \cdot a$$ + +Where: +- $m$: Mass of object ($\mathrm{kg}$) +- $a$: Acceleration ($\mathrm{m/s^2}$) + +![Newton's Second Law](https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Newtonslawsofmotion.jpg/800px-Newtonslawsofmotion.jpg)""", + "input": [ + {"name": "m", "unit": "kilogram"}, + {"name": "a", "unit": "meters per square second"} + ], + "output": {"name": "F", "unit": "newton"}, + "d4rtCode": "F = m * a;", + "tags": ["physics", "mechanics", "newton"] +} +``` + +### Complete Unit Example + +```dart +[ + { + "name": "newton", + "symbol": "N", + "isBase": true + }, + { + "name": "kilonewton", + "symbol": "kN", + "baseUnit": "newton", + "factor": 1000 + }, + { + "name": "pound-force", + "symbol": "lbf", + "baseUnit": "newton", + "factor": 4.44822 + } +] +``` + +### Multi-line Dart Code Example + +```dart +// Cosine Rule +{ + "name": "Cosine Rule", + "description": r""" +Generalization of the Pythagorean theorem for any triangle. + +$$c^2 = a^2 + b^2 - 2ab\cos(C)$$ + +Where: +- $a$, $b$, $c$: Sides of the triangle +- $C$: Angle opposite to side $c$""", + "input": [ + {"name": "a", "unit": "meter"}, + {"name": "b", "unit": "meter"}, + {"name": "C", "unit": "degree"} + ], + "output": {"name": "c", "unit": "meter"}, + "d4rtCode": """ + var angleCRad = C * (pi / 180); + c = sqrt(pow(a, 2) + pow(b, 2) - 2*a*b*cos(angleCRad)); + """, + "tags": ["trigonometry", "triangle", "cosine"] +} +``` + +--- + +## File Organization + +### Formula Categories + +| File | Topic | +|----------------------------------|--------------------------------| +| `formulas.d4rt` | General physics formulas | +| `geometry.d4rt` | Geometric calculations | +| `electromagnetism.d4rt` | Electric and magnetic formulas | +| `energy_and_power.d4rt` | Energy, work, and power | +| `thermodynamics.d4rt` | Heat and thermodynamics | +| `fluids_and_pressure.d4rt` | Fluid mechanics | +| `optics.d4rt` | Light and optics | +| `trigonometry.d4rt` | Trigonometric relations | +| `materials_elasticity.d4rt` | Material properties | +| `medical_and_bio.d4rt` | Medical/biological formulas | +| `networking.d4rt` | Network calculations | +| `conversions_and_constants.d4rt` | Physical constants | +| `misc_math.d4rt` | Miscellaneous mathematics | + +### Unit Categories + +| File | Unit Type | +|--------------------------|------------------------| +| `distance.d4rt.units` | Length/distance | +| `mass.d4rt.units` | Mass | +| `time.d4rt.units` | Time | +| `force.d4rt.units` | Force | +| `energy.d4rt.units` | Energy | +| `power.d4rt.units` | Power | +| `pressure.d4rt.units` | Pressure | +| `velocity.d4rt.units` | Speed/velocity | +| `area.d4rt.units` | Area | +| `volume.d4rt.units` | Volume | +| `temperature.d4rt.units` | Temperature | +| `angle.d4rt.units` | Angles | +| `frequency.d4rt.units` | Frequency | +| `electricity.d4rt.units` | Electrical units | +| `derived.d4rt.units` | Derived/compound units | + +--- + +## Quick Reference + +### Common LaTeX Symbols + +| Symbol | LaTeX | Symbol | LaTeX | +|--------|-----------|--------|----------| +| × | `\times` | · | `\cdot` | +| ± | `\pm` | ÷ | `\div` | +| ≤ | `\leq` | ≥ | `\geq` | +| √ | `\sqrt{}` | ∞ | `\infty` | +| π | `\pi` | θ | `\theta` | +| α | `\alpha` | β | `\beta` | +| Δ | `\Delta` | δ | `\delta` | +| Σ | `\Sigma` | σ | `\sigma` | +| Ω | `\Omega` | ω | `\omega` | + +### Common Dart Math Functions + +| Function | Description | +|---------------------------------|-----------------------------------| +| `sin(x)`, `cos(x)`, `tan(x)` | Trigonometric functions (radians) | +| `asin(x)`, `acos(x)`, `atan(x)` | Inverse trig functions | +| `sqrt(x)` | Square root | +| `pow(x, y)` | x raised to power y | +| `log(x)` | Natural logarithm | +| `log10(x)` | Base-10 logarithm | +| `abs(x)` | Absolute value | +| `exp(x)` | e raised to power x | +| `pi` | π constant | + +--- + +## Contributing + +1. **Choose the right file** - Add formulas to the appropriate category file +2. **Follow the format** - Match the structure of existing entries +3. **Test your code** - Ensure `d4rtCode` is valid Dart syntax +4. **Add description** - Include complete LaTeX documentation +5. **Tag appropriately** - Add relevant tags for searchability +6. **Review** - Check existing formulas for consistency + +For questions or clarifications, refer to existing formulas in the `assets/formulas/` directory as examples. diff --git a/assets/formulas/networking.d4rt b/assets/formulas/networking.d4rt new file mode 100644 index 0000000..ceda27e --- /dev/null +++ b/assets/formulas/networking.d4rt @@ -0,0 +1,83 @@ +[ + // IP Subnet and Broadcast Calculator + { + "name": "IP Subnet and Broadcast", + "description": r""" +Calculates the network (subnet) address and broadcast address for an IPv4 address with CIDR notation. + +**Input format:** `ip_address/prefix` where: +- `ip_address`: IPv4 address in dotted decimal notation (e.g., `192.168.1.100`) +- `prefix`: CIDR prefix length (1-30) or subnet mask in dotted notation (e.g., `24` or `255.255.255.0`) + +**Output:** +- `subnet`: Network address in dotted decimal notation +- `broadcast`: Broadcast address in dotted decimal notation + +**Examples:** +- Input: `192.168.1.100/24` → Subnet: `192.168.1.0`, Broadcast: `192.168.1.255` +- Input: `10.0.0.50/8` → Subnet: `10.0.0.0`, Broadcast: `10.255.255.255` +- Input: `172.16.5.100/16` → Subnet: `172.16.0.0`, Broadcast: `172.16.255.255`""", + "input": [ + {"name": "ipWithMask", "unit": "scalar"} + ], + "output": {"name": "subnet", "unit": "scalar"}, + "d4rtCode": """ + var input = ipWithMask.toString(); + var slashIndex = input.indexOf('/'); + if (slashIndex == -1) { + subnet = 'error: no / found'; + broadcast = ''; + } else { + var ipPart = input.substring(0, slashIndex).trim(); + var maskPart = input.substring(slashIndex + 1).trim(); + + // Parse IP address + var ipParts = ipPart.split('.'); + if (ipParts.length != 4) { + subnet = 'error: invalid IP'; + broadcast = ''; + } else { + var octet1 = int.parse(ipParts[0]); + var octet2 = int.parse(ipParts[1]); + var octet3 = int.parse(ipParts[2]); + var octet4 = int.parse(ipParts[3]); + + // Convert IP to 32-bit integer + var ipInt = (octet1 << 24) | (octet2 << 16) | (octet3 << 8) | octet4; + + // Parse mask (CIDR prefix or dotted notation) + int maskInt; + if (maskPart.contains('.')) { + var maskParts = maskPart.split('.'); + var m1 = int.parse(maskParts[0]); + var m2 = int.parse(maskParts[1]); + var m3 = int.parse(maskParts[2]); + var m4 = int.parse(maskParts[3]); + maskInt = (m1 << 24) | (m2 << 16) | (m3 << 8) | m4; + } else { + var prefix = int.parse(maskPart); + maskInt = prefix == 0 ? 0 : (-1 << (32 - prefix)); + } + + // Calculate subnet and broadcast + var subnetInt = ipInt & maskInt; + var broadcastInt = subnetInt | (~maskInt & 0xFFFFFFFF); + + // Convert back to dotted notation + var s1 = (subnetInt >> 24) & 0xFF; + var s2 = (subnetInt >> 16) & 0xFF; + var s3 = (subnetInt >> 8) & 0xFF; + var s4 = subnetInt & 0xFF; + subnet = '\$s1.\$s2.\$s3.\$s4'; + + var b1 = (broadcastInt >> 24) & 0xFF; + var b2 = (broadcastInt >> 16) & 0xFF; + var b3 = (broadcastInt >> 8) & 0xFF; + var b4 = broadcastInt & 0xFF; + broadcast = '\$b1.\$b2.\$b3.\$b4'; + } + } + """, + "tags": ["networking", "ip", "subnet", "broadcast", "cidr"] + } +] diff --git a/lib/ai/formula_editor.dart b/lib/ai/formula_editor.dart new file mode 100644 index 0000000..ff7805e --- /dev/null +++ b/lib/ai/formula_editor.dart @@ -0,0 +1,705 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown_plus_latex/flutter_markdown_plus_latex.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:markdown/markdown.dart' as markdown; +import '../formula_models.dart'; +import '../corpus.dart'; +import 'formula_screen.dart'; +import 'unit_dropdown.dart'; + +/// A screen for editing a Formula's properties including name, description, +/// input/output variables, and d4rt code. +class FormulaEditor extends StatefulWidget { + final Formula formula; + final Corpus corpus; + + const FormulaEditor({ + super.key, + required this.formula, + required this.corpus, + }); + + @override + State createState() => _FormulaEditorState(); +} + +class _FormulaEditorState extends State { + final _formKey = GlobalKey(); + late TextEditingController _nameController; + late TextEditingController _descriptionController; + late TextEditingController _d4rtCodeController; + + // Track input variables + final List<_InputVariableRowData> _inputVariables = []; + + // Output variable + late _OutputVariableRowData _outputVariable; + + bool _isPreviewVisible = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.formula.name); + _descriptionController = TextEditingController(text: widget.formula.description ?? ''); + _d4rtCodeController = TextEditingController(text: widget.formula.d4rtCode); + + // Initialize input variables + for (final input in widget.formula.input) { + _inputVariables.add(_InputVariableRowData( + nameController: TextEditingController(text: input.name), + unit: input.unit, + values: input.values != null ? List.from(input.values!) : null, + )); + } + + // Initialize output variable + _outputVariable = _OutputVariableRowData( + nameController: TextEditingController(text: widget.formula.output.name), + unit: widget.formula.output.unit, + ); + } + + @override + void dispose() { + _nameController.dispose(); + _descriptionController.dispose(); + _d4rtCodeController.dispose(); + for (final variable in _inputVariables) { + variable.nameController.dispose(); + } + _outputVariable.nameController.dispose(); + super.dispose(); + } + + void _addInputVariable() { + setState(() { + _inputVariables.add(_InputVariableRowData( + nameController: TextEditingController(text: 'var${_inputVariables.length + 1}'), + unit: null, + values: null, + )); + }); + } + + void _removeInputVariable(int index) { + setState(() { + _inputVariables.removeAt(index); + }); + } + + void _showPreview() { + setState(() { + _isPreviewVisible = true; + }); + } + + void _hidePreview() { + setState(() { + _isPreviewVisible = false; + }); + } + + void _testFormula() { + // Validate the formula before testing + if (!_validateFormula()) { + return; + } + + final formula = _buildFormula(); + if (formula == null) return; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FormulaScreen( + formula: formula, + corpus: widget.corpus, + ), + ), + ); + } + + bool _validateFormula() { + // Validate name + if (_nameController.text.trim().isEmpty) { + _showErrorDialog('Formula name cannot be empty'); + return false; + } + + // Validate output name + if (_outputVariable.nameController.text.trim().isEmpty) { + _showErrorDialog('Output variable name cannot be empty'); + return false; + } + + // Validate input variable names + for (final variable in _inputVariables) { + if (variable.nameController.text.trim().isEmpty) { + _showErrorDialog('Input variable names cannot be empty'); + return false; + } + } + + // Validate d4rt code + if (_d4rtCodeController.text.trim().isEmpty) { + _showErrorDialog('D4RT code cannot be empty'); + return false; + } + + return true; + } + + Formula? _buildFormula() { + try { + final input = []; + for (final variable in _inputVariables) { + input.add(VariableSpec( + name: variable.nameController.text.trim(), + unit: variable.unit, + values: variable.values, + )); + } + + final output = VariableSpec( + name: _outputVariable.nameController.text.trim(), + unit: _outputVariable.unit, + ); + + return Formula( + name: _nameController.text.trim(), + description: _descriptionController.text.isEmpty ? null : _descriptionController.text, + input: input, + output: output, + d4rtCode: _d4rtCodeController.text, + tags: widget.formula.tags, // Preserve existing tags + ); + } catch (e) { + _showErrorDialog('Error building formula: $e'); + return null; + } + } + + void _saveFormula() { + if (!_validateFormula()) { + return; + } + + final formula = _buildFormula(); + if (formula == null) return; + + // For now, just show a success message + // In a real implementation, this would save to database + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Formula "${formula.name}" saved successfully!'), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + ); + } + + void _showErrorDialog(String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Error'), + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Edit Formula'), + actions: [ + IconButton( + icon: const Icon(Icons.play_arrow), + onPressed: _testFormula, + tooltip: 'Test Formula', + ), + IconButton( + icon: const Icon(Icons.save), + onPressed: _saveFormula, + tooltip: 'Save', + ), + ], + ), + body: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + _buildNameSection(), + const SizedBox(height: 16), + _buildDescriptionSection(), + const SizedBox(height: 16), + _buildInputVariablesSection(), + const SizedBox(height: 16), + _buildOutputVariableSection(), + const SizedBox(height: 16), + _buildD4rtCodeSection(), + const SizedBox(height: 32), + ], + ), + ), + ), + ); + } + + Widget _buildNameSection() { + return TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Formula Name', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.title), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required'; + } + return null; + }, + ); + } + + Widget _buildDescriptionSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Description (Markdown)', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isPreviewVisible) + TextButton.icon( + icon: const Icon(Icons.visibility_off), + label: const Text('Hide Preview'), + onPressed: _hidePreview, + ) + else + TextButton.icon( + icon: const Icon(Icons.visibility), + label: const Text('Preview'), + onPressed: _showPreview, + ), + ], + ), + ], + ), + const SizedBox(height: 8), + if (_isPreviewVisible) ...[ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(8), + ), + child: Markdown( + data: _descriptionController.text, + shrinkWrap: true, + builders: { + 'latex': LatexElementBuilder(), + }, + extensionSet: markdown.ExtensionSet( + [LatexBlockSyntax()], + [LatexInlineSyntax()], + ), + ), + ), + const SizedBox(height: 16), + ], + TextFormField( + controller: _descriptionController, + decoration: const InputDecoration( + hintText: 'Enter formula description (supports Markdown and LaTeX)', + border: OutlineInputBorder(), + ), + maxLines: 5, + ), + ], + ), + ), + ); + } + + Widget _buildInputVariablesSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Input Variables', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + ..._inputVariables.asMap().entries.map((entry) { + final index = entry.key; + final variable = entry.value; + return _buildInputVariableRow(index, variable); + }).toList(), + const SizedBox(height: 8), + ElevatedButton.icon( + icon: const Icon(Icons.add), + label: const Text('Add Input Variable'), + onPressed: _addInputVariable, + ), + ], + ), + ), + ); + } + + Widget _buildInputVariableRow(int index, _InputVariableRowData variable) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: TextFormField( + controller: variable.nameController, + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Base Unit', style: TextStyle(fontSize: 12)), + DropdownButtonFormField( + value: _getBaseUnit(variable.unit), + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('None', style: TextStyle(fontSize: 14)), + ), + ..._getAllBaseUnits().map((baseUnit) { + return DropdownMenuItem( + value: baseUnit, + child: Text(baseUnit, style: const TextStyle(fontSize: 14)), + ); + }).toList(), + ], + onChanged: (baseUnit) { + setState(() { + variable.unit = baseUnit; + }); + }, + ), + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Derived Unit', style: TextStyle(fontSize: 12)), + DropdownButtonFormField( + value: variable.unit, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('None', style: TextStyle(fontSize: 14)), + ), + ..._getDerivedUnits(variable.unit).map((unit) { + final unitSpec = widget.corpus.getUnit(unit); + return DropdownMenuItem( + value: unit, + child: Text('${unitSpec.symbol} - ${unit}', + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + ], + onChanged: (unit) { + setState(() { + variable.unit = unit; + }); + }, + ), + ], + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _removeInputVariable(index), + tooltip: 'Delete variable', + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildOutputVariableSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Output Variable', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + flex: 2, + child: TextFormField( + controller: _outputVariable.nameController, + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Base Unit', style: TextStyle(fontSize: 12)), + DropdownButtonFormField( + value: _getBaseUnit(_outputVariable.unit), + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('None', style: TextStyle(fontSize: 14)), + ), + ..._getAllBaseUnits().map((baseUnit) { + return DropdownMenuItem( + value: baseUnit, + child: Text(baseUnit, style: const TextStyle(fontSize: 14)), + ); + }).toList(), + ], + onChanged: (baseUnit) { + setState(() { + _outputVariable.unit = baseUnit; + }); + }, + ), + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Derived Unit', style: TextStyle(fontSize: 12)), + DropdownButtonFormField( + value: _outputVariable.unit, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('None', style: TextStyle(fontSize: 14)), + ), + ..._getDerivedUnits(_outputVariable.unit).map((unit) { + final unitSpec = widget.corpus.getUnit(unit); + return DropdownMenuItem( + value: unit, + child: Text('${unitSpec.symbol} - ${unit}', + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + ], + onChanged: (unit) { + setState(() { + _outputVariable.unit = unit; + }); + }, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildD4rtCodeSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'D4RT Code', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(4), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + ), + child: Row( + children: [ + Icon(Icons.code, size: 16, color: Theme.of(context).colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Dart Syntax', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + TextFormField( + controller: _d4rtCodeController, + decoration: const InputDecoration( + hintText: 'Enter D4RT/Dart code here', + border: InputBorder.none, + contentPadding: EdgeInsets.all(12), + ), + maxLines: 10, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // Helper methods for unit management + String? _getBaseUnit(String? unit) { + if (unit == null) return null; + try { + return widget.corpus.getUnit(unit).baseUnit; + } catch (e) { + return null; + } + } + + List _getAllBaseUnits() { + final baseUnits = {}; + for (final unit in widget.corpus.allUnits()) { + baseUnits.add(unit.baseUnit); + } + return baseUnits.toList()..sort(); + } + + List _getDerivedUnits(String? baseUnit) { + if (baseUnit == null) return []; + return widget.corpus.unitsOfSameMagnitude(baseUnit)..sort(); + } +} + +// Data classes to track variable state +class _InputVariableRowData { + final TextEditingController nameController; + String? unit; + List? values; + + _InputVariableRowData({ + required this.nameController, + this.unit, + this.values, + }); +} + +class _OutputVariableRowData { + final TextEditingController nameController; + String? unit; + + _OutputVariableRowData({ + required this.nameController, + this.unit, + }); +} diff --git a/lib/ai/formula_list.dart b/lib/ai/formula_list.dart index 5348710..a6b2129 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; // For Clipboard import 'package:d4rt_formulas/formula_models.dart'; import '../corpus.dart'; import 'formula_screen.dart'; +import 'formula_editor.dart'; import 'package:share_plus/share_plus.dart'; class FormulaList extends StatefulWidget { @@ -55,13 +56,13 @@ class _FormulaListState extends State { try { // Get the formula and its dependencies final dependencies = widget.corpus.withDependencies(formula); - + // Convert each dependency to its string literal representation final literals = dependencies.map((element) => element.toStringLiteral()).toList(); - + // Create an array string literal containing all the elements final exportString = '[${literals.join(', ')}]'; - + // Share the string await Share.share( exportString, @@ -72,6 +73,18 @@ class _FormulaListState extends State { } } + void _editFormula(Formula formula) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FormulaEditor( + formula: formula, + corpus: widget.corpus, + ), + ), + ); + } + void _copyFormula(Formula formula) async { try { // Get the formula and its dependencies @@ -144,35 +157,45 @@ class _FormulaListState extends State { subtitle: formula.tags.isNotEmpty ? Text('Tags: ${formula.tags.join(', ')}') : null, - trailing: PopupMenuButton( - icon: Icon(Icons.share), - onSelected: (value) { - if (value == 'share') { - _shareFormula(formula); - } else if (value == 'copy') { - _copyFormula(formula); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'share', - child: Row( - children: [ - Icon(Icons.share), - SizedBox(width: 8), - Text('Share'), - ], - ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _editFormula(formula), + tooltip: 'Edit Formula', ), - PopupMenuItem( - value: 'copy', - child: Row( - children: [ - Icon(Icons.copy), - SizedBox(width: 8), - Text('Copy to clipboard'), - ], - ), + PopupMenuButton( + icon: const Icon(Icons.share), + onSelected: (value) { + if (value == 'share') { + _shareFormula(formula); + } else if (value == 'copy') { + _copyFormula(formula); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'share', + child: Row( + children: [ + Icon(Icons.share), + SizedBox(width: 8), + Text('Share'), + ], + ), + ), + PopupMenuItem( + value: 'copy', + child: Row( + children: [ + Icon(Icons.copy), + SizedBox(width: 8), + Text('Copy to clipboard'), + ], + ), + ), + ], ), ], ), diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 7bf6dc5..5395598 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -8,6 +8,7 @@ import '../formula_evaluator.dart'; import '../corpus.dart'; import '../error_handler.dart'; import 'unit_dropdown.dart'; +import 'formula_editor.dart'; class FormulaScreen extends StatefulWidget { final Formula formula; @@ -171,7 +172,26 @@ class _FormulaScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(widget.formula.name)), + 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, + ), + ), + ); + }, + tooltip: 'Edit Formula', + ), + ], + ), body: Form( key: _formKey, child: Padding( diff --git a/lib/defaults/default_corpus.dart b/lib/defaults/default_corpus.dart index 1719f04..f712f97 100644 --- a/lib/defaults/default_corpus.dart +++ b/lib/defaults/default_corpus.dart @@ -62,6 +62,7 @@ Future createDefaultCorpus() async{ "assets/formulas/medical_and_bio.d4rt", "assets/formulas/conversions_and_constants.d4rt", "assets/formulas/misc_math.d4rt", + "assets/formulas/networking.d4rt", ]; for (final formRes in formulaResources) {