Create a simple food recommender with JamAI and Flutter

Recommends food based on the user's profile. The app uses a combination of user input and predefined data to provide personalized food recommendations.

Key Features:

  • User Profile Management: Users can enter and update their personal information

  • Food type Selection: Users have their suggested meals generated for them.

Project setup

Create the UserInformation Action Tables

First we will create the UserInformation table. This table will store the user input.

Creating a table
Completed UserInformation Action Table

Create the menu Knowledge Table

We want to store our menu items inside a Knowledge Table to use our menu for RAG. The knowledge table will automatically chunk the data for us for RAG use. Easy.

Create Knowledge Table

Now we can use our menu in our Action Table.

Create the SuggestedMeal Action Table

Next, create the SuggestedMeal table. This table will store data and implement LLM's directly in the cell to update the cell values in the Output Columns. This table will use knowledge from the knowledge table we created earlier. It will perform RAG to retrieve the menu items from our given menu.

Modify the output columns to have a prompt that uses the input from the previous column(s).

the syntax for using the data from a column for your output column is this:

// Your example prompt
Hi I am ${name} please take a look at these: ${input_column}. tell me which is the best for me.

This is an example you can adapt to your column layouts.

Choose your model and choose the settings you like, Here we are leaving the settings on default. For the SugesstedMeal column, we enable RAG here to use our menu in this column from the Knowledge Table we created earlier.

The other columns don't need RAG so we'll use prompts.

Using an Action Table with RAG

Setup the remaining columns and test your prompts! by pressing Add row.

Try your prompts

Once you're happy with the results, use the JamAi API in your Flutter app.

Create your Flutter app

The app will have 3 widgets to collect user input and call the JamAi API to generate suggested meals for the user.

Create the ProfileCard Widget

  • This widget contains various TextFieldsfor the user to enter their name, age, weight, height, activity level, and fitness goal.

  • The user input is stored in the corresponding TextEditingController instances in the ProfileCardModel.

  • When the user clicks the "Save" button, the saveName method is called, which stores the user input in the respective variables in the ProfileCardModel.

```dart
class ProfileCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final screenHeight = MediaQuery.of(context).size.height;

    return Consumer<ProfileCardModel>(
      builder: (context, model, child) {
        return AnimatedContainer(
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          padding: EdgeInsets.all(16.0),
          height: model.isExpanded ? screenHeight * 0.4 : 100,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(16.0),
            color: Colors.white,
            border: Border.all(
              color: Colors.black,
              width: 2.0,
            ),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.5),
                spreadRadius: 2,
                blurRadius: 5,
                offset: Offset(0, 3),
              ),
            ],
          ),
          child: model.isExpanded
              ? ListView(
                  // crossAxisAlignment: CrossAxisAlignment.start,
                  // mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        CircleAvatar(
                          radius: 36,
                          backgroundImage: AssetImage('assets/images.jpeg'),
                        ),
                        Expanded(
                          child: Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: TextField(
                              controller: model.nameController,
                              focusNode: model.nameFocusNode,
                              decoration: InputDecoration(
                                hintText: 'Enter your name',
                                border: UnderlineInputBorder(),
                              ),
                              style: TextStyle(
                                fontSize: 22.0,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),

                    // rows for user information input
                    Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      // mainAxisAlignment: ,
                      children: [
                        Padding(
                          padding: const EdgeInsets.only(bottom: 20.0),
                          child: Icon(
                            Icons.cake,
                            size: 25.0,
                            // color: Colors.grey[600],
                          ),
                        ),
                        SizedBox(width: 4.0),
                        Padding(
                          padding: const EdgeInsets.only(bottom: 20),
                          child: Text(
                            "Age:",
                            style: TextStyle(
                              fontSize: 18.0,
                            ),
                          ),
                        ),
                        SizedBox(width: 3),
                        Expanded(
                          child: TextField(
                            controller: model.ageController,
                            focusNode: model.ageFocusNode,
                            decoration: InputDecoration(
                              hintText: 'Enter your age',
                              border: UnderlineInputBorder(),
                            ),
                            style: TextStyle(
                              fontSize: 18.0,
                              fontWeight: FontWeight.normal,
                            ),
                            keyboardType: TextInputType.number,
                            inputFormatters: [
                              FilteringTextInputFormatter.digitsOnly,
                            ],
                            maxLength: 3,
                          ),
                        ),
                      ],
                    ),
                    Row(
                      children: [
                        Icon(MdiIcons.genderMaleFemale),
                        SizedBox(
                          width: 4.0,
                        ),
                        Expanded(
                          // width: 330,
                          child: SegmentedButton<String>(
                            selectedIcon: Container(),
                            segments: const <ButtonSegment<String>>[
                              ButtonSegment<String>(
                                value: 'Male',
                                label: Text(
                                  "Male",
                                  style: TextStyle(
                                    fontSize: 16.0,
                                  ),
                                ),
                              ),
                              ButtonSegment<String>(
                                value: 'Female',
                                label: Text(
                                  "Female",
                                  style: TextStyle(fontSize: 16.0),
                                ),
                              ),
                              ButtonSegment(
                                  value: 'Others',
                                  label: Text(
                                    "Others",
                                    style: TextStyle(fontSize: 16.0),
                                  ))
                            ],
                            selected: {model.selectedGender},
                            onSelectionChanged: (Set<String> selected) {
                              model.setGender(selected.first);
                            },
                          ),
                        )
                      ],
                    ),

                    Row(
                      children: [
                        Padding(
                          padding: const EdgeInsets.only(bottom: 20.0),
                          child: Icon(
                            MdiIcons.weight,
                            size: 25.0,
                          ),
                        ),
                        SizedBox(
                          width: 4.0,
                        ),
                        Padding(
                          padding: const EdgeInsets.only(bottom: 20.0),
                          child: Text(
                            'Weight:',
                            style: TextStyle(
                              fontSize: 18.0,
                            ),
                          ),
                        ),
                        SizedBox(width: 4.0),
                        Expanded(
                          child: TextField(
                            controller: model.weightController,
                            focusNode: model.weightFocusNode,
                            decoration: InputDecoration(
                              hintText: "Enter your weight (kg)",
                              border: UnderlineInputBorder(),
                            ),
                            style: TextStyle(
                              fontSize: 18.0,
                              fontWeight: FontWeight.normal,
                            ),
                            keyboardType: TextInputType.number,
                            inputFormatters: [
                              FilteringTextInputFormatter.digitsOnly,
                            ],
                            maxLength: 3,
                          ),
                        )
                      ],
                    ),

                    Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Padding(
                          padding: const EdgeInsets.only(bottom: 20.0),
                          child: Icon(
                            Icons.height,
                            size: 25.0,
                          ),
                        ),
                        SizedBox(
                          width: 4.0,
                        ),
                        Padding(
                          padding: const EdgeInsets.only(bottom: 20.0),
                          child: Text(
                            'Height:',
                            style: TextStyle(
                              fontSize: 18.0,
                            ),
                          ),
                        ),
                        SizedBox(width: 4.0),
                        Expanded(
                          child: TextField(
                            controller: model.heightController,
                            focusNode: model.heightFocusNode,
                            decoration: InputDecoration(
                              hintText: "Enter your height (cm)",
                              border: UnderlineInputBorder(),
                            ),
                            style: TextStyle(
                              fontSize: 18.0,
                              fontWeight: FontWeight.normal,
                            ),
                            keyboardType: TextInputType.number,
                            inputFormatters: [
                              FilteringTextInputFormatter.digitsOnly,
                            ],
                            maxLength: 3,
                          ),
                        )
                      ],
                    ),

                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Center(
                          child: Text(
                        "Choose your Activity Level",
                        style: TextStyle(
                          fontSize: 20.0,
                          fontWeight: FontWeight.w400,
                        ),
                      )),
                    ),

                    AnimatedContainer(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(20),
                        color: model.activityContainerColor,
                      ),
                      duration: Duration(milliseconds: 300),
                      curve: Curves.easeInOut,
                      child: Column(
                        children: [
                          Text(model.currentActivityTitle,
                              style: TextStyle(
                                fontSize: 18.0,
                                fontWeight: FontWeight.bold,
                              )),
                          Row(
                            children: [
                              SizedBox(
                                width: 20,
                              ),
                              Container(
                                padding: EdgeInsets.symmetric(horizontal: 10),
                                width: 100,
                                height: 100,
                                child: Image.asset(
                                    'assets/${model.currentActivityIconPath}'),
                              ),
                              Flexible(
                                child: Text(
                                  model.currentActivityDes,
                                  style: TextStyle(
                                    fontWeight: FontWeight.bold,
                                    fontSize: 15,
                                  ),
                                  softWrap: true,
                                  overflow: TextOverflow.visible,
                                ),
                              ),
                              SizedBox(
                                width: 20,
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),

                    SizedBox(
                      height: 20,
                    ),

                    Slider(
                      min: 1,
                      max: 5,
                      divisions: 4,
                      value: model.activitySliderValue.toDouble(),
                      label: model.currentActivityTitle.toString(),
                      onChanged: (value) {
                        model.updateSliderValue(value.toInt());
                      },
                    ),

                    SizedBox(height: 16.0),

                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Center(
                          child: Text(
                        "Choose your fitness goal",
                        style: TextStyle(
                          fontSize: 20.0,
                          fontWeight: FontWeight.w400,
                        ),
                      )),
                    ),

                    AnimatedContainer(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(20),
                        color: model.fitnessCointanerColor,
                      ),
                      duration: Duration(milliseconds: 300),
                      curve: Curves.easeInOut,
                      child: Column(
                        children: [
                          Text(model.currentFitnessTitle,
                              style: TextStyle(
                                fontSize: 18.0,
                                fontWeight: FontWeight.bold,
                              )),
                          Row(
                            children: [
                              SizedBox(
                                width: 20,
                              ),
                              Container(
                                padding: EdgeInsets.symmetric(horizontal: 10),
                                width: 100,
                                height: 100,
                                child: Image.asset(
                                    'assets/${model.currentFitnessIconPath}'),
                              ),
                              Flexible(
                                child: Text(
                                  model.currentFitnessDes,
                                  style: TextStyle(
                                    fontWeight: FontWeight.bold,
                                    fontSize: 15,
                                  ),
                                  softWrap: true,
                                  overflow: TextOverflow.visible,
                                ),
                              ),
                              SizedBox(
                                width: 20,
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),

                    SizedBox(
                      height: 20,
                    ),

                    Slider(
                      min: 1,
                      max: 3,
                      divisions: 2,
                      value: model.fitnessSliderValue.toDouble(),
                      label: model.currentFitnessTitle.toString(),
                      onChanged: (value) {
                        model.updateFitnessSliderValue(value.toInt());
                      },
                    ),

                    SizedBox(height: 16.0),
                    ElevatedButton(
                      onPressed: () {
                        if (model.nameController.text.isEmpty) {
                          model.nameFocusNode.requestFocus();
                          model.showSnackBar(context, 'Please enter your name');
                        } else if (model.ageController.text.isEmpty) {
                          model.ageFocusNode.requestFocus();
                          model.showSnackBar(context, 'Please enter your age');
                        } else if (model.weightController.text.isEmpty) {
                          model.weightFocusNode.requestFocus();
                          model.showSnackBar(
                              context, 'Please enter your weight');
                        } else if (model.heightController.text.isEmpty) {
                          model.heightFocusNode.requestFocus();
                          model.showSnackBar(
                              context, 'Please enter your height');
                        } else {
                          model.saveName();
                          model.toggleExpansion();
                        }
                      },
                      child: Text('Save'),
                    ),
                  ],
                )
              : Row(
                  children: [
                    CircleAvatar(
                      radius: 24,
                      backgroundImage: AssetImage('assets/images.jpeg'),
                    ),
                    SizedBox(width: 16.0),
                    Expanded(
                      child: TextField(
                        controller: model.nameController,
                        onTap: () {
                          model.toggleExpansion();
                        },
                        decoration: InputDecoration(
                          hintText: 'Enter your name',
                          border: InputBorder.none,
                        ),
                        style: TextStyle(
                          fontSize: 18.0,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ],
                ),
        );
      },
    );
  }
}

```

The Profile Card widget is using a `ProfileCardModel to manage its state defined here:

```dart
class ProfileCardModel extends ChangeNotifier {
  final TextEditingController nameController = TextEditingController();
  final TextEditingController ageController = TextEditingController();
  String _selectedGender = "Male";
  final TextEditingController weightController = TextEditingController();
  final TextEditingController heightController = TextEditingController();
  int _activitySliderValue = 1;
  int _fitnessSliderValue = 1;

  final List<List<String>> _activityDes = [
    [
      "Sedentary",
      "resting.webp",
      "Mostly chilling out, sitting or lying down, not much moving around."
    ],
    [
      "Moderate",
      "moderate_activity.png",
      "Doing some light activities like walking or easy chores pretty regularly."
    ],
    [
      "Active",
      "active.png",
      "Keeping busy with daily exercise or sports, definitely on the move."
    ],
    [
      "Very Active",
      "very_active.png",
      "Often hitting it hard with activities like running or intense gym sessions."
    ],
    [
      "Athlete",
      "athlete.png",
      "Always going all out with heavy workouts or tough physical jobs, super energetic."
    ],
  ];

  String _currentActivtyDes =
      "Mostly chilling out, sitting or lying down, not much moving around.";
  String _currentActivtyTitle = 'Sedentary';
  String _currentActivtyIconPath = 'resting.webp';

  final List<List<String>> _fitnessDes = [
    ["Slim", "slim.png", "emphasizes a narrow, delicate build"],
    ["Lean", "lean.png", "showcases minimal body fat."],
    ["Fit", "fit.png", "highlights strength and health"],
  ];

  String _currentFitnessDes = "emphasizes a narrow, delicate build";
  String _currentFitnessTitle = "Slim";
  String _currentFitnessIconPath = 'slim.png';

  final nameFocusNode = FocusNode();
  final ageFocusNode = FocusNode();
  final weightFocusNode = FocusNode();
  final heightFocusNode = FocusNode();

  void toggleExpansion() {
    _isExpanded = !_isExpanded;
    notifyListeners();
  }

  void saveName() {
    _name = nameController.text;
    _age = ageController.text;
    _weight = weightController.text;
    _height = heightController.text;
    notifyListeners();
  }

  void setGender(selectedGender) {
    _selectedGender = selectedGender;
    notifyListeners();
  }

  void updateSliderValue(int value) {
    _activitySliderValue = value;
    updateContainerColor(value);
    updateActivityDes(value);
    updateActivityTitle(value);
    updateActivityIcon(value);
    notifyListeners();
  }

  // create function to change container color value
  void updateContainerColor(int value) {
    switch (value) {
      case 1:
        _activityContainerColor = Colors.blue[100];
        break;
      case 2:
        _activityContainerColor = Colors.yellow[200];
        break;
      case 3:
        _activityContainerColor = Colors.amber[200];
        break;
      case 4:
        _activityContainerColor = Colors.orangeAccent;
        break;
      case 5:
        _activityContainerColor = Colors.deepOrange;
        break;
      default:
        break;
    }
    notifyListeners();
  }

  void updateActivityDes(int value) {
    _currentActivtyDes = _activityDes[value - 1][2];
    notifyListeners();
  }

  void updateActivityTitle(int value) {
    _currentActivtyTitle = _activityDes[value - 1][0];
    notifyListeners();
  }

  void updateActivityIcon(int value) {
    _currentActivtyIconPath = _activityDes[value - 1][1];
    notifyListeners();
  }

  // functions to change fitness container color value
  void updateFitnessSliderValue(int value) {
    _fitnessSliderValue = value;
    updateFitnessContainerColor(value);
    updateFitnessDes(value);
    updateFitnessTitle(value);
    updateFitnessIcon(value);
    notifyListeners();
  }

  void updateFitnessContainerColor(int value) {
    switch (value) {
      case 1:
        _fitnessContainerColor = Colors.blue[200];
        break;
      case 2:
        _fitnessContainerColor = Colors.amber[200];
        break;
      case 3:
        _fitnessContainerColor = Colors.deepOrange;
      default:
        break;
    }
    notifyListeners();
  }

  void updateFitnessDes(int value) {
    _currentFitnessDes = _fitnessDes[value - 1][2];
    notifyListeners();
  }

  void updateFitnessIcon(int value) {
    _currentFitnessIconPath = _fitnessDes[value - 1][1];
    notifyListeners();
  }

  void updateFitnessTitle(int value) {
    _currentFitnessTitle = _fitnessDes[value - 1][0];
    notifyListeners();
  }

  void showSnackBar(BuildContext context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(message),
      duration: Duration(seconds: 2),
    ));
  }

  bool _isExpanded = false;

  bool get isExpanded => _isExpanded;

  String _name = 'John Doe';
  String _age = '';
  String _weight = '';
  String _height = '';
  var _activityContainerColor = Colors.blue[200];
  var _fitnessContainerColor = Colors.blue[200];

  String get name => _name;
  String get age => _age;
  String get selectedGender => _selectedGender;
  String get weight => _weight;
  String get height => _height;

  int get activitySliderValue => _activitySliderValue;
  get activityContainerColor => _activityContainerColor;
  String get currentActivityDes => _currentActivtyDes;
  String get currentActivityTitle => _currentActivtyTitle;
  String get currentActivityIconPath => _currentActivtyIconPath;

  int get fitnessSliderValue => _fitnessSliderValue;
  get fitnessCointanerColor => _fitnessContainerColor;
  String get currentFitnessDes => _currentFitnessDes;
  String get currentFitnessTitle => _currentFitnessTitle;
  String get currentFitnessIconPath => _currentFitnessIconPath;
}

```

The ProfileCardWidget will provide the user information used to update the UserInformation table.

Create the FoodTypeSelector Widget

Now we need to get one last input from the user. We let the user choose the type of food to use as input for the SuggestedMeal table.

```dart
class FoodTypeSelector extends StatelessWidget {
  const FoodTypeSelector({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<foodTypeSelectorModel>(builder: (context, model, child) {
      return AnimatedContainer(
        duration: Duration(milliseconds: 300),
        clipBehavior: Clip.none,
        curve: Curves.easeInOut,
        height: model.isExpanded ? 480 : 100,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(16.0),
          color: Colors.white,
          border: Border.all(
            color: Colors.black,
            width: 2.0,
          ),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.5),
              spreadRadius: 2,
              blurRadius: 5,
              offset: Offset(0, 3),
            ),
          ],
        ),
        child: model.isExpanded
            ? Padding(
                padding: const EdgeInsets.all(8.0),
                child: SingleChildScrollView(
                  child: Column(
                    children: model.foodTypes,
                  ),
                ),
              )
            : Padding(
                padding: const EdgeInsets.all(8.0),
                child: GestureDetector(
                  onTap: () => model.toggleExpansion(),
                  child: model.selectedFoodType,
                ),
              ),
      );
    });
  }
}

class FoodTypes extends StatelessWidget {
  final String title;
  final String imagePath;
  final int widgetIndex;

  const FoodTypes({
    Key? key,
    required this.title,
    required this.imagePath,
    required this.widgetIndex,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 5.0),
      child: Consumer<foodTypeSelectorModel>(
        builder: (context, model, child) {
          return GestureDetector(
            onTap: () {
              model.selectedFood(title, widgetIndex);
              model.toggleExpansion();
            },
            child: Container(
                width: 400,
                height: 80,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(20),
                  color: Colors.grey[200],
                ),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8),
                      child: CircleAvatar(
                        radius: 32.0,
                        backgroundImage: AssetImage(imagePath),
                      ),
                    ),
                    Text(
                      title,
                      style: const TextStyle(
                        fontSize: 20.0,
                        fontWeight: FontWeight.w400,
                      ),
                    ),
                  ],
                )),
          );
        },
      ),
    );
  }
}
```

Create the FoodTypeSelectorModel to manage the state of the FoodTypeSelector Widget.

```dart
class foodTypeSelectorModel extends ChangeNotifier {
  bool _isExpanded = false;
  final List<FoodTypes> _foodTypes = [
    FoodTypes(
        widgetIndex: 0, title: "Japanese", imagePath: "assets/japanese.jpeg"),
    FoodTypes(
        widgetIndex: 1, title: "Western", imagePath: "assets/western.jpg"),
    FoodTypes(
        widgetIndex: 2, title: "Malaysian", imagePath: "assets/malaysian.jpg"),
    FoodTypes(
        widgetIndex: 3, title: "Chinese", imagePath: "assets/chinese.jpg"),
    FoodTypes(widgetIndex: 4, title: "Korean", imagePath: "assets/korean.jpeg"),
  ];

  FoodTypes _selectedFoodType = FoodTypes(
      title: "Choose your food type",
      imagePath: "assets/japanese.jpeg",
      widgetIndex: 0);
  String _foodSelected = "Choose your food type";

  void toggleExpansion() {
    _isExpanded = !_isExpanded;
    notifyListeners();
  }

  void selectedFood(String food, int index) {
    _foodSelected = food;
    _selectedFoodType = _foodTypes[index];

    print("food index: ${index}");
    notifyListeners();
  }

  bool get isExpanded => _isExpanded;
  List<FoodTypes> get foodTypes => _foodTypes;
  String get foodSelected => _foodSelected;
  FoodTypes get selectedFoodType => _selectedFoodType;
}

```

Now we have the right inputs for our SuggestedMeal Table. We can generate the Suggested meal for the user!

Create the MealGenerator Widget.

This widget will show the generated meals coming from the Output columns we created in the Action Table.

```dart
class MealGenrator extends StatelessWidget {
  const MealGenrator({super.key});

  Future<void> _genereateMeal(
      ProfileCardModel profileCardModel,
      foodTypeSelectorModel foodTypeSelectorModel,
      MealGenratorModel mealGenratorModel,
      jamaiService) async {
    if (profileCardModel.nameController.text.isEmpty ||
        profileCardModel.ageController.text.isEmpty ||
        profileCardModel.weightController.text.isEmpty ||
        profileCardModel.heightController.text.isEmpty) {
      mealGenratorModel
          .updateCanGenerateStatusText("Please update profile first");
    } else if (foodTypeSelectorModel.foodSelected == "Choose your food type") {
      mealGenratorModel
          .updateCanGenerateStatusText("Please choose your food type");
    } else {
      mealGenratorModel.toggleIsLoading();
      mealGenratorModel.updateCanGenerateStatusText("Generate Meal Now");
      // mealGenratorModel.toggleExpansion();

      // Create message to send to API
      // update user information table
      String currentAddRowId = await jamaiService.addRow(
        profileCardModel.name,
        profileCardModel.age,
        profileCardModel.selectedGender,
        int.parse(profileCardModel.weight),
        int.parse(profileCardModel.height),
        profileCardModel.currentActivityTitle,
        profileCardModel.currentFitnessTitle,
      );
      mealGenratorModel.updateCurrentGeneratedRowId(currentAddRowId);

      // create inputs to add row to suggested meal
      // get data user information from UserInformation table
      ParseUserInformationGetRow userInformation =
          await jamaiService.getUserInformation(currentAddRowId);
      String inputUserProfile = '''
      Gender: ${userInformation.gender},
      Age: ${userInformation.age},
      Weight: ${userInformation.currentWeight},
      Height: ${userInformation.height},
      fitness goal: ${userInformation.fitnessGoals},
      activity level: ${userInformation.activityLevel}
      ''';

      print(inputUserProfile);

      // add row to SuggestedMealTable
      mealGenratorModel.updateSuggestedMealResponse(
          await jamaiService.addRowSuggestedMeal(
              inputUserProfile, foodTypeSelectorModel.foodSelected));
      // add row to SuggestedMeal table
      // var suggestedMealResponse = await jamaiService.addRowSuggestedMeal(inputUserProfile, foodTypeSelectorModel.foodSelected);
      mealGenratorModel.toggleIsLoading();

      mealGenratorModel.toggleExpansion();
    }
  }

  @override
  Widget build(BuildContext context) {
    final JamaiService jamaiService = JamaiService();

    return Consumer3<ProfileCardModel, foodTypeSelectorModel,
            MealGenratorModel>(
        builder: (context, profileCardModel, foodTypeSelectorModel,
            mealGenratorModel, child) {
      return AnimatedContainer(
          duration: Duration(milliseconds: 300),
          clipBehavior: Clip.none,
          curve: Curves.easeInOut,
          height: mealGenratorModel.isExpanded ? 1280 : 100,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(16.0),
            color: Colors.white,
            border: Border.all(
              color: Colors.black,
              width: 2.0,
            ),
            boxShadow: [
              BoxShadow(
                color: Colors.grey.withOpacity(0.5),
                spreadRadius: 2,
                blurRadius: 5,
                offset: Offset(0, 3),
              ),
            ],
          ),
          child: mealGenratorModel.isExpanded
              ? GestureDetector(
                  onTap: () {
                    mealGenratorModel.toggleExpansion();
                  },
                  child: SingleChildScrollView(
                    child: Column(
                      children: [
                        Center(
                            child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Text(
                            "Suggested Meals",
                            style: TextStyle(
                                fontSize: 26.0, fontWeight: FontWeight.bold),
                          ),
                        )),
                        MealItems(
                          imagePath:
                              "${mealGenratorModel.meal1MessageContent.foodName}.jpeg",
                          foodName:
                              mealGenratorModel.meal1MessageContent.foodName,
                          servingSize:
                              mealGenratorModel.meal1MessageContent.servingSize,
                          calories:
                              mealGenratorModel.meal1MessageContent.calories,
                          carbohydratesValue: mealGenratorModel
                              .meal1MessageContent.carbohydrates.value
                              .toDouble(),
                          proteinValue: mealGenratorModel
                              .meal1MessageContent.protein.value
                              .toDouble(),
                          totalFatValue: mealGenratorModel
                              .meal1MessageContent.totalFat.value
                              .toDouble(),
                          fiberValue: mealGenratorModel
                              .meal1MessageContent.fiber.value
                              .toDouble(),
                        ),
                        MealItems(
                          imagePath:
                              "${mealGenratorModel.meal2MessageContent.foodName}.jpeg",
                          foodName:
                              mealGenratorModel.meal2MessageContent.foodName,
                          servingSize:
                              mealGenratorModel.meal2MessageContent.servingSize,
                          calories:
                              mealGenratorModel.meal2MessageContent.calories,
                          carbohydratesValue: mealGenratorModel
                              .meal2MessageContent.carbohydrates.value
                              .toDouble(),
                          proteinValue: mealGenratorModel
                              .meal2MessageContent.protein.value
                              .toDouble(),
                          totalFatValue: mealGenratorModel
                              .meal2MessageContent.totalFat.value
                              .toDouble(),
                          fiberValue: mealGenratorModel
                              .meal2MessageContent.fiber.value
                              .toDouble(),
                        ),
                        MealItems(
                          imagePath:
                              "${mealGenratorModel.meal3MessageContent.foodName}.jpeg",
                          foodName:
                              mealGenratorModel.meal3MessageContent.foodName,
                          servingSize:
                              mealGenratorModel.meal3MessageContent.servingSize,
                          calories:
                              mealGenratorModel.meal3MessageContent.calories,
                          carbohydratesValue: mealGenratorModel
                              .meal3MessageContent.carbohydrates.value
                              .toDouble(),
                          proteinValue: mealGenratorModel
                              .meal3MessageContent.protein.value
                              .toDouble(),
                          totalFatValue: mealGenratorModel
                              .meal3MessageContent.totalFat.value
                              .toDouble(),
                          fiberValue: mealGenratorModel
                              .meal3MessageContent.fiber.value
                              .toDouble(),
                        ),
                      ],
                    ),
                  ),
                )
              : Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(mealGenratorModel.canGenerateStatusText,
                          style: TextStyle(
                              fontSize: 18.0, fontWeight: FontWeight.w500)),
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Container(
                          width: 200,
                          height: 50,
                          child: ElevatedButton.icon(
                              onPressed: mealGenratorModel.isLoading
                                  ? null
                                  : () async {
                                      await _genereateMeal(
                                          profileCardModel,
                                          foodTypeSelectorModel,
                                          mealGenratorModel,
                                          jamaiService);
                                      // expand container after we get results
                                    },
                              icon: Icon(MdiIcons.chefHat),
                              label: Text(
                                "Generate Meal",
                                style: TextStyle(fontSize: 18.0),
                              )),
                        ),
                        if (mealGenratorModel.isLoading)
                          Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: CircularProgressIndicator(),
                          ),
                      ],
                    )
                  ],
                ));
    });
  }
}

class MealItems extends StatelessWidget {
  final String imagePath;
  final String foodName;
  final String servingSize;
  final int calories;
  final double carbohydratesValue;
  final double proteinValue;
  final double totalFatValue;
  final double fiberValue;

  const MealItems(
      {required this.imagePath,
      required this.foodName,
      required this.servingSize,
      required this.calories,
      required this.carbohydratesValue,
      required this.proteinValue,
      required this.totalFatValue,
      required this.fiberValue,
      super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        decoration: BoxDecoration(
          color: Colors.grey[100],
          borderRadius: BorderRadius.circular(16.0),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.6),
              blurRadius: 6.0,
              offset: Offset(0, 2),
            ),
          ],
        ),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(12.0),
                child: Image.asset(
                  "assets/meals/$imagePath",
                  height: 100,
                  width: 100,
                  fit: BoxFit.cover,
                ),
              ),
              SizedBox(width: 24.0),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      foodName,
                      style: TextStyle(
                        fontSize: 24.0,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 8.0),
                    Text(
                      'Serving Size: $servingSize',
                      style: TextStyle(
                        fontSize: 16.0,
                        color: Colors.grey[600],
                      ),
                    ),
                    SizedBox(height: 16.0),
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        NutritionInfoChip(
                          label: 'Calories',
                          value: calories.toString(),
                        ),
                        SizedBox(height: 8.0),
                        NutritionInfoChip(
                          label: 'Carbs',
                          value: carbohydratesValue.toString(),
                        ),
                        SizedBox(height: 8.0),
                        NutritionInfoChip(
                          label: 'Protein',
                          value: proteinValue.toString(),
                        ),
                        SizedBox(height: 8.0),
                        NutritionInfoChip(
                          label: 'Fat',
                          value: totalFatValue.toString(),
                        ),
                        SizedBox(height: 8.0),
                        NutritionInfoChip(
                          label: 'Fiber',
                          value: fiberValue.toString(),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

```

Create the MealGenaratorModel to handle the state of the MealGenerator widget.

class MealGenratorModel extends ChangeNotifier {
  bool _isExpanded = false;
  String _canGenerateStatusText = "Please fill in all fields";
  bool _canPressButton = false;
  String _currentGeneratedRowId = "";
  bool _isLoading = false;

  late ParseAddRowSuggestedMeal _suggestedMealResponse;
  late ParseUserInformationGetRow _userInformationGetRow;
  late ParseMessageContent _meal1MessageContent;
  late ParseMessageContent _meal2MessageContent;
  late ParseMessageContent _meal3MessageContent;

  void toggleExpansion() {
    _isExpanded = !_isExpanded;
    notifyListeners();
  }

  void updateCanGenerateStatusText(String status) {
    _canGenerateStatusText = status;
    notifyListeners();
  }

  void toggleButtonState() {
    _canPressButton = !_canPressButton;
    notifyListeners();
  }

  void updateCurrentGeneratedRowId(String value) {
    _currentGeneratedRowId = value;
    notifyListeners();
  }

  void toggleIsLoading() {
    _isLoading = !_isLoading;
    print("toggle loading");
    notifyListeners();
  }

  void updateUserInformationGetRow(ParseUserInformationGetRow response) {
    _userInformationGetRow = response;
    notifyListeners();
  }

  void updateSuggestedMealResponse(ParseAddRowSuggestedMeal response) {
    _suggestedMealResponse = response;
    updateMealMessageContent(response);
    notifyListeners();
  }

  void updateMealMessageContent(ParseAddRowSuggestedMeal response) {
    _meal1MessageContent = ParseMessageContent.fromJson(
        jsonDecode(response.columns.meal1.choices[0].message.content));
    print(_meal1MessageContent.foodName);
    _meal2MessageContent = ParseMessageContent.fromJson(
        jsonDecode(response.columns.meal2.choices[0].message.content));
    print(_meal2MessageContent.foodName);
    _meal3MessageContent = ParseMessageContent.fromJson(
        jsonDecode(response.columns.meal3.choices[0].message.content));
    print(_meal3MessageContent.foodName);
    notifyListeners();
  }

  bool get isExpanded => _isExpanded;
  String get canGenerateStatusText => _canGenerateStatusText;
  String get currentGeneratedRowid => _currentGeneratedRowId;
  bool get isLoading => _isLoading;
  ParseAddRowSuggestedMeal get suggestedMealResponse => _suggestedMealResponse;
  ParseUserInformationGetRow get userInformationGetRow =>
      _userInformationGetRow;

  ParseMessageContent get meal1MessageContent => _meal1MessageContent;
  ParseMessageContent get meal2MessageContent => _meal2MessageContent;
  ParseMessageContent get meal3MessageContent => _meal3MessageContent;
}

Here's a summary of what will happen when you press the Generate Meal button.

  1. We add a row to the UserInformation table from the user input

```dart
String currentAddRowId jamaiService.addRow(
        profileCardModel.name,
        profileCardModel.age,
        profileCardModel.selectedGender,
        int.parse(profileCardModel.weight),
        int.parse(profileCardModel.height),
        profileCardModel.currentActivityTitle,
        profileCardModel.currentFitnessTitle,
      );
```
  1. We get the user information from the row we just added from UserInformation

```dart
ParseUserInformationGetRow userInformation =
          await jamaiService.getUserInformation(currentAddRowId);
```
  1. We add a row to the SuggestedMeal table to generate our suggested meal for the user

```dart
mealGenratorModel.updateSuggestedMealResponse(
          await jamaiService.addRowSuggestedMeal(
              inputUserProfile, foodTypeSelectorModel.foodSelected));
```

Create the JamaiService class

Now we have all the widgets to build our app, we'll create the JamaiServiceclass in a separate to import into our main app that will handle the API calls for us.

```dart
import 'dart:convert';
import 'package:http/http.dart' as http;


class ParseUserInformationAddRow {
  // String columns;
  String rowId;

  ParseUserInformationAddRow({
    // required this.columns,
    required this.rowId,
  });

  factory ParseUserInformationAddRow.fromJson(Map<String, dynamic> json) {
    return ParseUserInformationAddRow(
      // columns: json['columns'],
      rowId: json['row_id'],
    );
  }
}

class ParseUserInformationGetRow {
  String rowId;
  String updatedTime;
  String name;
  int age;
  String gender;
  int height;
  String fitnessGoals;
  String activityLevel;
  int currentWeight;

  ParseUserInformationGetRow({
    required this.rowId,
    required this.updatedTime,
    required this.name,
    required this.age,
    required this.gender,
    required this.height,
    required this.fitnessGoals,
    required this.activityLevel,
    required this.currentWeight,
  });

  factory ParseUserInformationGetRow.fromJson(Map<String, dynamic> json) {
    return ParseUserInformationGetRow(
      rowId: json['ID'],
      updatedTime: json['Updated at'],
      name: json['Name'],
      age: json['Age'],
      gender: json['Gender'],
      height: json['Height'],
      fitnessGoals: json['FitnessGoals'],
      activityLevel: json['ActivityLevel'],
      currentWeight: json['CurrentWeight'],
    );
  }
}

// classes to parse suggested meal output
class ParseAddRowSuggestedMeal {
  ParseColumns columns;
  String rowId;

  ParseAddRowSuggestedMeal({
    required this.columns,
    required this.rowId,
  });

  factory ParseAddRowSuggestedMeal.fromJson(Map<String, dynamic> json) {
    return ParseAddRowSuggestedMeal(
      columns: ParseColumns.fromJson(json['columns']),
      rowId: json['row_id'],
    );
  }
}

class ParseColumns {
  ParseColumn suggestedMeal;
  ParseColumn meal1;
  ParseColumn meal2;
  ParseColumn meal3;

  ParseColumns({
    required this.suggestedMeal,
    required this.meal1,
    required this.meal2,
    required this.meal3,
  });

  factory ParseColumns.fromJson(Map<String, dynamic> json) {
    return ParseColumns(
      suggestedMeal: ParseColumn.fromJson(json['SuggestedMeal']),
      meal2: ParseColumn.fromJson(json['Meal2']),
      meal1: ParseColumn.fromJson(json['Meal1']),
      meal3: ParseColumn.fromJson(json['Meal3']),
    );
  }
}

class ParseColumn {
  String id;
  String object;
  int created;
  String model;
  ParseUsage usage;
  List<ParseChoices> choices;

  ParseColumn({
    required this.id,
    required this.object,
    required this.created,
    required this.model,
    required this.usage,
    required this.choices,
  });

  factory ParseColumn.fromJson(Map<String, dynamic> json) {
    return ParseColumn(
      id: json['id'],
      object: json['object'],
      created: json['created'],
      model: json['model'],
      usage: ParseUsage.fromJson(json['usage']),
      choices: (json['choices'] as List)
          .map((x) => ParseChoices.fromJson(x))
          .toList(),
    );
  }
}

class ParseUsage {
  int promptTokens;
  int completionTokens;
  int totalTokens;

  ParseUsage({
    required this.promptTokens,
    required this.completionTokens,
    required this.totalTokens,
  });

  factory ParseUsage.fromJson(Map<String, dynamic> json) {
    return ParseUsage(
      promptTokens: json['prompt_tokens'],
      completionTokens: json['completion_tokens'],
      totalTokens: json['total_tokens'],
    );
  }
}

class ParseChoices {
  ParseMessage message;
  int index;
  String finishReason;

  ParseChoices({
    required this.message,
    required this.index,
    required this.finishReason,
  });

  factory ParseChoices.fromJson(Map<String, dynamic> json) {
    return ParseChoices(
      message: ParseMessage.fromJson(json['message']),
      index: json['index'],
      finishReason: json['finish_reason'],
    );
  }
}

class ParseMessage {
  String role;
  String content;
  String? name;

  ParseMessage({
    required this.role,
    required this.content,
    required this.name,
  });

  factory ParseMessage.fromJson(Map<String, dynamic> json) {
    return ParseMessage(
      role: json['role'],
      content: json['content'],
      name: json['name'],
    );
  }
}

// parse json coming from message content
class ParseMessageContent {
  String foodName;
  String servingSize;
  int calories;
  ItemDetails carbohydrates;
  ItemDetails protein;
  ItemDetails totalFat;
  ItemDetails fiber;

  ParseMessageContent({
    required this.foodName,
    required this.servingSize,
    required this.calories,
    required this.carbohydrates,
    required this.protein,
    required this.totalFat,
    required this.fiber,
  });

  factory ParseMessageContent.fromJson(Map<String, dynamic> json) {
    return ParseMessageContent(
      foodName: json['food_name'],
      servingSize: json['serving_size'],
      calories: json['calories'],
      carbohydrates: ItemDetails.fromJson(json['carbohydrates']),
      protein: ItemDetails.fromJson(json['protein']),
      totalFat: ItemDetails.fromJson(json['total_fat']),
      fiber: ItemDetails.fromJson(json['fiber']),
    );
  }
}

class ItemDetails {
  double value;
  String unit;

  ItemDetails({
    required this.value,
    required this.unit,
  });

  factory ItemDetails.fromJson(Map<String, dynamic> json) {
    return ItemDetails(
      value: json['value'].toDouble(),
      unit: json['unit'],
    );
  }
}

class JamaiService {
 
  final user = 'YourUsername';
  final password = 'YourPassword';

  late var bytes;
  late var authHeader;

  // function to add row to Userinformation table
  Future<String> addRow(
      String name,
      String age,
      String gender,
      int currentWeight,
      int height,
      String fitnessGoals,
      String activityLevel) async {
    const String tableId = "UserInformation";
    bytes = utf8.encode('$user:$password');
    authHeader = 'Basic ${base64.encode(bytes)}';
    const addRowApiUrl =
        "https://app.jamaibase.com/api/v1/gen_tables/action/rows/add";
    final response = await http.post(Uri.parse(addRowApiUrl),
        headers: {
          'Authorization': authHeader,
          "accept": 'application/json',
          'content-type': 'application/json'
        },
        body: jsonEncode({
          'table_id': tableId,
          'data': {
            'Name': name,
            'Age': age,
            'Gender': gender,
            'CurrentWeight': currentWeight,
            'Height': height,
            'FitnessGoals': fitnessGoals,
            'ActivityLevel': activityLevel,
          },
          'stream': false
        }));
    if (response.statusCode == 200) {
      print("response after add row: ${response.body}");
      final responseParsed =
          ParseUserInformationAddRow.fromJson(jsonDecode(response.body));
      return responseParsed.rowId;
    } else {
      print("error from add row: ${response.body}");

      throw Exception("error from add row: Status Code: ${response.body}");
    }
  }

  Future<ParseUserInformationGetRow> getUserInformation(
      String userInformationRowId) async {
    const String tableType = "action";
    const String tableId = "UserInformation";

    bytes = utf8.encode('$user:$password');
    authHeader = 'Basic ${base64.encode(bytes)}';

    final String getRowApiUrl =
        "https://app.jamaibase.com/api/v1/gen_tables/${tableType}/${tableId}/rows/${userInformationRowId}";

    final response = await http.get(Uri.parse(getRowApiUrl), headers: {
      'Authorization': authHeader,
    });

    if (response.statusCode == 200) {
      print("response from getUserInformation: ${response.body}");
      final responseParsed =
          ParseUserInformationGetRow.fromJson(jsonDecode(response.body));
      return responseParsed;
    } else {
      print("error from getUserInformation: ${response.body}");
      throw Exception(
          "failed to load user information. Status Code: ${response.statusCode}");
    }
  }

  Future<ParseAddRowSuggestedMeal> addRowSuggestedMeal(
    String inputUserProfile,
    String typeOfMeal,
  ) async {
    const String tableId = "SuggestedMeal";
    bytes = utf8.encode('$user:$password');
    authHeader = 'Basic ${base64.encode(bytes)}';

    const addRowApiUrl =
        "https://app.jamaibase.com/api/v1/gen_tables/action/rows/add";
    final response = await http.post(Uri.parse(addRowApiUrl),
        headers: {
          'Authorization': authHeader,
          "accept": 'application/json',
          'content-type': 'application/json'
        },
        body: jsonEncode({
          'table_id': tableId,
          'data': {
            'InputUserProfile': inputUserProfile,
            'TypeOfMeal': typeOfMeal,
          },
          'stream': false
        }));

    if (response.statusCode == 200) {
      print("response from addRowSuggestedMeal: ${response.body}");
      final responseParsed =
          ParseAddRowSuggestedMeal.fromJson(jsonDecode(response.body));

      return responseParsed;
    } else {
      print("error from addRowSuggestedMeal: {}");
      throw Exception(
          "failed to add row to SuggestedMeal table. Status code:${response.statusCode}");
    }
  }

  Future<String> getRow(
    String tableType,
    String tableId,
    String rowId,
  ) async {
    final _tableType = tableType;
    final _tableId = tableId;
    final _rowId = rowId;

    bytes = utf8.encode('$user:$password');
    authHeader = 'Basic ${base64.encode(bytes)}';
    final _getRowApiUrl =
        "https://app.jamaibase.com/api/v1/gen_tables/${_tableType}/${_tableId}/rows/${_rowId}";

    final response = await http.get(Uri.parse(_getRowApiUrl), headers: {
      'Authorization': authHeader,
    });
    if (response.statusCode == 200) {
      print("response body: ${response.body}");
      return 'true';
    } else {
      throw Exception("get row error: Status Code: ${response.body}");
    }
  }

  Future<String> listRows(String tableType, String tableId) async {
    final _tableType = tableType;
    final _tableId = tableId;

    bytes = utf8.encode('$user:$password');
    authHeader = 'Basic ${base64.encode(bytes)}';
    final _listRowApiUrl =
        "https://app.jamaibase.com/api/v1/gen_tables/${_tableType}/${_tableId}/rows";

    final response = await http.get(Uri.parse(_listRowApiUrl), headers: {
      'Authorization': authHeader,
    });

    if (response.statusCode == 200) {
      print("list row response: ${response.body}");
      return 'true';
    } else {
      throw Exception("list row response: Status Code:${response.body}");
    }
  }
}

```

For now, we are using the Flutter http library to call the API. we need to handle the Json responses to out the cell values in our simple app.

To see how to handle the responses, refer to the documentation below:

Conclusion

Once you get your app running, you can look at your Jamai Action table rows to review the data that you can access with your app.

The suggested meal for the user is in the SuggestedMeal

You can view and use the data here in your table by referencing the the rowId in your table.

Next Steps

We have demonstrated how easy it is to store data and use LLM's to update your cell values with prompts directly. This simple example demonstrates the simple use of an Action Table here in your app. If you have more data you want to leverage and make your app smarter, you can implement the Knowledge Table to make your Action Tables smarter with Retrieval Augmented Generation (RAG).

For example, you can add your menu catalog and let meal columns generate meals based on your menu.

Last updated