Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
source "https://rubygems.org"

gem "file_to_data_uri"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.0.2"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
Expand Down
16 changes: 12 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ai-chat (0.2.1)
ai-chat (0.2.3)
base64 (> 0.1.1)
json (~> 2.0)
marcel (~> 1.0)
Expand All @@ -96,7 +96,7 @@ GEM
tabulo
ast (2.4.2)
awesome_print (1.9.2)
base64 (0.2.0)
base64 (0.3.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin)
Expand Down Expand Up @@ -139,7 +139,7 @@ GEM
ostruct
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
connection_pool (2.5.3)
crack (1.0.0)
bigdecimal
rexml
Expand Down Expand Up @@ -190,6 +190,9 @@ GEM
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
file_to_data_uri (0.0.2)
base64 (~> 0.1, >= 0.1.0)
mime-types (~> 3.0)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
Expand Down Expand Up @@ -233,7 +236,7 @@ GEM
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.10.2)
json (2.13.2)
kamal (2.5.3)
activesupport (>= 7.0)
base64 (~> 0.2)
Expand Down Expand Up @@ -274,6 +277,10 @@ GEM
marcel (1.0.4)
matrix (0.4.2)
method_source (1.1.0)
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0812)
mini_magick (5.2.0)
benchmark
logger
Expand Down Expand Up @@ -601,6 +608,7 @@ DEPENDENCIES
dotenv
draft_generators!
faker
file_to_data_uri
grade_runner (~> 0.0.13)
haikunator
htmlbeautifier
Expand Down
123 changes: 123 additions & 0 deletions app/controllers/solutions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
class SolutionsController < ApplicationController

Check failure on line 2 in app/controllers/solutions_controller.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body beginning.

Check failure on line 2 in app/controllers/solutions_controller.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body beginning.
def display_form
render({ :template => "solution_templates/new_form" })
end

def process_inputs
@the_image = params.fetch("image_param", "")
@the_description = params.fetch("description_param", "")

chat = AI::Chat.new
chat.model = "o3"
chat.reasoning_effort = :high
chat.web_search = true
chat.system("You are an expert nutritionist. The user will provide either a photo, a description, or both of one or more food items. Your job is to identify the food items and estimate how many grams of carbohydrates, grams of protein, grams of fat, and total calories are in a meal. You should also add a breakdown of how you arrived at these figures, and any other notes you have. You can search the web.")
chat.schema = '{
"name": "meal_nutrition_estimate",
"schema": {
"type": "object",
"properties": {
"rationale": {
"type": "string",
"description": "Explanation of how you came to the estimates for the nutritional values of the items and totals."
},
"items": {
"type": "array",
"description": "List of food items with their corresponding macronutrient and calorie values.",
"items": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "Description or name of the food item."
},
"grams_of_carbs": {
"type": "integer",
"description": "Amount of carbohydrates in grams."
},
"grams_of_fat": {
"type": "integer",
"description": "Amount of fat in grams."
},
"grams_of_protein": {
"type": "integer",
"description": "Amount of protein in grams."
},
"calories": {
"type": "integer",
"description": "Total calories contained in the item."
}
},
"required": [
"description",
"grams_of_carbs",
"grams_of_fat",
"grams_of_protein",
"calories"
],
"additionalProperties": false
}
},
"total_grams_of_carbs": {
"type": "integer",
"description": "Total grams of carbohydrates for all items."
},
"total_grams_of_fat": {
"type": "integer",
"description": "Total grams of fat for all items."
},
"total_grams_of_protein": {
"type": "integer",
"description": "Total grams of protein for all items."
},
"total_calories": {
"type": "integer",
"description": "Total calories for all items."
}
},
"required": [
"rationale",
"items",
"total_grams_of_carbs",
"total_grams_of_fat",
"total_grams_of_protein",
"total_calories"
],
"additionalProperties": false
},
"strict": true
}'


if @the_image.blank? && @the_description.blank?
@rationale = "You must provide at least one of image or description."
else
if @the_image.present?
chat.user("Here's an image of the meal:", image: @the_image)
end

if @the_description.present?
chat.user(@the_description)
end

result = chat.generate!

ap result

@list_of_items = result.fetch(:items)
@g_carbs = result.fetch(:total_grams_of_carbs)
@g_protein = result.fetch(:total_grams_of_protein)
@g_fat = result.fetch(:total_grams_of_fat)
@kcal = result.fetch(:total_calories)
@rationale = result.fetch(:rationale)
end

if @the_image.present?
@the_image_data_uri = DataURI.convert(@the_image)
end

render({ :template => "solution_templates/results" })
end

Check failure on line 122 in app/controllers/solutions_controller.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body end.

Check failure on line 122 in app/controllers/solutions_controller.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/EmptyLinesAroundClassBody: Extra empty line detected at class body end.
end
23 changes: 23 additions & 0 deletions app/views/solution_templates/new_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<h1>Estimate macros</h1>

<p>Send us an image and/or a description of a meal and we'll ask AI to estimate how many calories and macronutrients it contains:</p>

<div>
<form action="/solutions/process_form" method="post" enctype="multipart/form-data">
<div>
<label for="image_field">Image</label>
<input type="file" id="image_field" name="image_param">
</div>

<div>
<label for="description_field">Description</label>
<textarea id="description_field" name="description_param"></textarea>
</div>

<div>
<button>
Estimate!
</button>
</div>
</form>
</div>
68 changes: 68 additions & 0 deletions app/views/solution_templates/results.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<h1>Results</h1>

<% if @the_image.present? %>
<div>
<img src="<%= @the_image_data_uri %>" width="480">
</div>
<% end %>

<% if @the_description.present? %>
<div>
<%= simple_format(@the_description) %>
</div>
<% end %>

<dl>
<dt>Total carbs (g)</dt>
<dd><%= @g_carbs %></dd>

<dt>Total protein (g)</dt>
<dd><%= @g_protein %></dd>

<dt>Total fat (g)</dt>
<dd><%= @g_fat %></dd>

<dt>Total calories (kcal) </dt>
<dd><%= @kcal %></dd>

<dt>Rationale</dt>
<dd><%= simple_format(@rationale) %></dd>
</dl>

<h2>Items</h2>

<ol>
<% @list_of_items.each do |an_item| %>
<li>
<dl>
<dt>Description</dt>
<dd>
<%= an_item.fetch(:description) %>
</dd>

<dt>Carbs (g)</dt>
<dd>
<%= an_item.fetch(:grams_of_carbs) %>
</dd>

<dt>Protein (g)</dt>
<dd>
<%= an_item.fetch(:grams_of_protein) %>
</dd>

<dt>Fat (g)</dt>
<dd>
<%= an_item.fetch(:grams_of_fat) %>
</dd>

<dt>Calories (kcal)</dt>
<dd>
<%= an_item.fetch(:calories) %>
</dd>

</dl>
</li>
<% end %>
</ul>

<p><a href="/solutions/blank_form">Estimate another</a></p>
14 changes: 12 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
Rails.application.routes.draw do
# This is a blank app! Pick your first screen, build out the RCAV, and go from there. E.g.:
# get("/your_first_screen", { :controller => "pages", :action => "first" })
# Write your routes here.






Check failure on line 8 in config/routes.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.

Check failure on line 8 in config/routes.rb

View workflow job for this annotation

GitHub Actions / lint

Layout/TrailingWhitespace: Trailing whitespace detected.
# Solutions below. Don't peek until you try it yourself and get stuck!

get("/solutions/blank_form", { :controller => "solutions", :action => "display_form" })

post("/solutions/process_form", { :controller => "solutions", :action => "process_inputs" })
end
Binary file added public/monteverde-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/monteverde-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading