Skip to content

Commit

Permalink
Basic calorie counting
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim Bisenius committed May 18, 2024
1 parent 1065fd7 commit 2602c87
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Calorie Counter/Components/Keys.swift
16 changes: 12 additions & 4 deletions Calorie Counter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
objects = {

/* Begin PBXBuildFile section */
3853E41F2BF80FEE00958AB4 /* Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E41E2BF80FEE00958AB4 /* Logic.swift */; };
3853E41F2BF80FEE00958AB4 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E41E2BF80FEE00958AB4 /* Utilities.swift */; };
3853E4212BF8129900958AB4 /* MealProcessingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E4202BF8129900958AB4 /* MealProcessingView.swift */; };
3853E4232BF8463B00958AB4 /* Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E4222BF8463B00958AB4 /* Weight.swift */; };
3853E4252BF84AD300958AB4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E4242BF84AD300958AB4 /* SettingsView.swift */; };
3853E4272BF85AC500958AB4 /* Miscellaneous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E4262BF85AC500958AB4 /* Miscellaneous.swift */; };
3853E4292BF85CC800958AB4 /* AddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E4282BF85CC800958AB4 /* AddView.swift */; };
3853E42B2BF88E4B00958AB4 /* AI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E42A2BF88E4B00958AB4 /* AI.swift */; };
3853E42D2BF8906B00958AB4 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3853E42C2BF8906B00958AB4 /* Keys.swift */; };
38C2ED562BF4680E00B2C7C8 /* Calorie_CounterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C2ED552BF4680E00B2C7C8 /* Calorie_CounterApp.swift */; };
38C2ED5C2BF4681000B2C7C8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38C2ED5B2BF4681000B2C7C8 /* Assets.xcassets */; };
38C2ED5F2BF4681000B2C7C8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38C2ED5E2BF4681000B2C7C8 /* Preview Assets.xcassets */; };
Expand All @@ -27,12 +29,14 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
3853E41E2BF80FEE00958AB4 /* Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logic.swift; sourceTree = "<group>"; };
3853E41E2BF80FEE00958AB4 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
3853E4202BF8129900958AB4 /* MealProcessingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealProcessingView.swift; sourceTree = "<group>"; };
3853E4222BF8463B00958AB4 /* Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weight.swift; sourceTree = "<group>"; };
3853E4242BF84AD300958AB4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
3853E4262BF85AC500958AB4 /* Miscellaneous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Miscellaneous.swift; sourceTree = "<group>"; };
3853E4282BF85CC800958AB4 /* AddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddView.swift; sourceTree = "<group>"; };
3853E42A2BF88E4B00958AB4 /* AI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AI.swift; sourceTree = "<group>"; };
3853E42C2BF8906B00958AB4 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
38C2ED522BF4680E00B2C7C8 /* Calorie Counter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Calorie Counter.app"; sourceTree = BUILT_PRODUCTS_DIR; };
38C2ED552BF4680E00B2C7C8 /* Calorie_CounterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calorie_CounterApp.swift; sourceTree = "<group>"; };
38C2ED5B2BF4681000B2C7C8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -113,9 +117,11 @@
38C2ED6D2BF47EDB00B2C7C8 /* CircularProgressView.swift */,
38C2ED712BF4815500B2C7C8 /* MacrosView.swift */,
38C2ED6B2BF4771600B2C7C8 /* LargeCardView.swift */,
3853E41E2BF80FEE00958AB4 /* Logic.swift */,
3853E41E2BF80FEE00958AB4 /* Utilities.swift */,
3853E4202BF8129900958AB4 /* MealProcessingView.swift */,
3853E4262BF85AC500958AB4 /* Miscellaneous.swift */,
3853E42A2BF88E4B00958AB4 /* AI.swift */,
3853E42C2BF8906B00958AB4 /* Keys.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -203,10 +209,11 @@
buildActionMask = 2147483647;
files = (
38C2ED722BF4815500B2C7C8 /* MacrosView.swift in Sources */,
3853E42D2BF8906B00958AB4 /* Keys.swift in Sources */,
38C2ED742BF494D900B2C7C8 /* MealCardView.swift in Sources */,
3853E4292BF85CC800958AB4 /* AddView.swift in Sources */,
38C2ED662BF4685200B2C7C8 /* Meal.swift in Sources */,
3853E41F2BF80FEE00958AB4 /* Logic.swift in Sources */,
3853E41F2BF80FEE00958AB4 /* Utilities.swift in Sources */,
38C2ED702BF4804800B2C7C8 /* MainTabView.swift in Sources */,
38C2ED762BF4972E00B2C7C8 /* WelcomeView.swift in Sources */,
3853E4212BF8129900958AB4 /* MealProcessingView.swift in Sources */,
Expand All @@ -215,6 +222,7 @@
38C2ED6C2BF4771600B2C7C8 /* LargeCardView.swift in Sources */,
38C2ED6E2BF47EDB00B2C7C8 /* CircularProgressView.swift in Sources */,
38C2ED562BF4680E00B2C7C8 /* Calorie_CounterApp.swift in Sources */,
3853E42B2BF88E4B00958AB4 /* AI.swift in Sources */,
3853E4232BF8463B00958AB4 /* Weight.swift in Sources */,
3853E4252BF84AD300958AB4 /* SettingsView.swift in Sources */,
);
Expand Down
116 changes: 116 additions & 0 deletions Calorie Counter/Components/AI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// AI.swift
// Calorie Counter
//
// Created by Jim Bisenius on 5/18/24.
//

import Foundation

struct OpenAIResponse: Codable {
struct Choice: Codable {
struct Message: Codable {
let role: String
let content: String
}
let message: Message
}
let choices: [Choice]
}

func callOpenAIAPIForMealDescription(mealDescription: String, completion: @escaping (Result<[String: Any], Error>) -> Void) {
let prompt = """
You are a nutritionist AI assistant. Based on the description of a meal provided, your task is to estimate its nutritional content. Provide a short label (16 characters or less) that best describes the meal, alongisde an emoji that describes the meal, and then estimate the calories, protein, carbohydrates, and fats in JSON format.
Description: \(mealDescription)
Response format:
{
"label": "Short label",
"emoji": "A single emoji that best represents this meal",
"calories": "Calorie estimate",
"protein": "Protein estimate in grams",
"carbohydrates": "Carbohydrates estimate in grams",
"fats": "Fats estimate in grams"
}
Example:
Description: Grilled chicken breast with quinoa and steamed broccoli
Response:
{
"label": "Chicken Meal",
"emoji": "🍗",
"calories": 450,
"protein": 35,
"carbohydrates": 40,
"fats": 15
}
Now, process the following meal description:
Description: \(mealDescription)
"""

guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
print("Error: Invalid URL")
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
return
}

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(Keys.sandboxKey)", forHTTPHeaderField: "Authorization") // Replace with your actual API key

let parameters: [String: Any] = [
"model": "gpt-4o", // Replace with your desired model
"messages": [
["role": "system", "content": "You are a helpful assistant."],
["role": "user", "content": prompt]
],
"max_tokens": 256,
"temperature": 1.0,
"top_p": 1.0,
"frequency_penalty": 0,
"presence_penalty": 0
]

do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
print("Error encoding JSON body: \(error.localizedDescription)")
completion(.failure(error))
return
}

let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Error: \(error.localizedDescription)")
completion(.failure(error))
return
}

guard let data = data else {
print("Error: No data received!")
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
return
}

do {
let response = try JSONDecoder().decode(OpenAIResponse.self, from: data)
if let firstChoice = response.choices.first {
let responseData = firstChoice.message.content.data(using: .utf8)!
let jsonResponse = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]
completion(.success(jsonResponse ?? [:]))
} else {
print("Error: No choices in response!")
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No choices in response"])))
}
} catch {
print("Error decoding JSON response: \(error.localizedDescription)")
completion(.failure(error))
}
}

task.resume()
}
6 changes: 4 additions & 2 deletions Calorie Counter/Components/Miscellaneous.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ func settingsSection<Content: View>(header: String, @ViewBuilder content: () ->
}

@ViewBuilder
func settingsRow(title: String, value: String? = nil, imageName: String? = nil, lastRow: Bool?, gray: Bool?, danger: Bool?) -> some View {
Button(action: { handleTap(option: title) }) {
func settingsRow(title: String, value: String? = nil, imageName: String? = nil, lastRow: Bool?, gray: Bool?, danger: Bool?, onTap: ((String) -> Void)?) -> some View {
Button(action: { if onTap != nil {
onTap?(title)
}}) {
HStack {
Text(title)
.fontWeight(.medium)
Expand Down
File renamed without changes.
63 changes: 57 additions & 6 deletions Calorie Counter/Screens/AddView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,79 @@ import Foundation
import SwiftUI

struct AddView: View {
@Environment(\.modelContext) private var modelContext
@State private var caloriesGoal: String = "2,000 calories / day"
@State private var proteinGoal: String = "23g / day"
@State private var weightGoal: String = "192 lbs"
@State private var askForDescription: Bool = true
@State private var mealsToShow: String = "3"
@State private var mealReminders: Bool = false
@State private var sendAnonymousData: Bool = true
@State private var showingDescribeMealAlert: Bool = false
@State private var mealDescription: String = ""
@Binding var selection: String

var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
settingsSection(header: "Weight") {
settingsRow(title: "Enter your weight manually", imageName: "Received", lastRow: nil, gray: nil, danger: nil)
settingsRow(title: "Pair smart-scale", imageName: "Scale", lastRow: true, gray: nil, danger: nil)
settingsRow(title: "Enter your weight manually", imageName: "Received", lastRow: nil, gray: nil, danger: nil, onTap: nil)
settingsRow(title: "Pair smart-scale", imageName: "Scale", lastRow: true, gray: nil, danger: nil, onTap: nil)
}

settingsSection(header: "Meal") {
settingsRow(title: "Take a photo of your meal", imageName: "Camera", lastRow: nil, gray: nil, danger: nil)
settingsRow(title: "Select meal photo from camera roll", imageName: "Photo", lastRow: nil, gray: nil, danger: nil)
settingsRow(title: "Describe your meal", imageName: "Chat", lastRow: nil, gray: nil, danger: nil)
settingsRow(title: "Scan barcode", imageName: "Barcode", lastRow: true, gray: nil, danger: nil)
settingsRow(title: "Take a photo of your meal", imageName: "Camera", lastRow: nil, gray: nil, danger: nil, onTap: nil)
settingsRow(title: "Select meal photo from camera roll", imageName: "Photo", lastRow: nil, gray: nil, danger: nil, onTap: nil)
settingsRow(title: "Describe your meal", imageName: "Chat", lastRow: nil, gray: nil, danger: nil, onTap: {_ in
showingDescribeMealAlert.toggle()
}
)
.alert("Describe your meal", isPresented: $showingDescribeMealAlert) {
TextField("ex: A 12ct chicken nugget meal from Chickfila", text: $mealDescription)
Button("Save meal", action: {
let desiredMealDescription = $mealDescription.wrappedValue
if desiredMealDescription != "" {
// TODO: Move to Processing page?
print("Prompt sent to OpenAI")
callOpenAIAPIForMealDescription(mealDescription: desiredMealDescription) { result in
switch result {
case .success(let response):
print("OpenAI Success")
print(response)
let meal = Meal(
emoji: response["emoji"] as? String,
createdAt: Date(),
label: response["label"] as? String,
details: desiredMealDescription,
reviewedAt: Date(),
calories: response["calories"] as? Int,
protein: response["protein"] as? Int,
carbohydrates: response["carbohydrates"] as? Int,
fats: response["fats"] as? Int
)
modelContext.insert(meal)
mealDescription = ""
do {
try modelContext.save()
// TODO: Redirect to Meal detail page rather than Summary
selection = "Summary"
} catch {
print(error)
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
}
})
Button("Cancel", action: {
showingDescribeMealAlert.toggle()
})
} message: {
Text("Enter as much information as you can. ex: What and how much you ate, from where, modifications, etc")
}
settingsRow(title: "Scan barcode", imageName: "Barcode", lastRow: true, gray: nil, danger: nil, onTap: nil)
}
}
.padding()
Expand Down
2 changes: 1 addition & 1 deletion Calorie Counter/Screens/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct MainTabView: View {
Text("Summary")
}.tag("Summary")

AddView()
AddView(selection: $selection)
.tabItem {
Image(systemName: "plus.circle.fill")
Text("Add new")
Expand Down
Loading

0 comments on commit 2602c87

Please sign in to comment.