diff --git a/Stanford360.xcodeproj/project.pbxproj b/Stanford360.xcodeproj/project.pbxproj index 1e2a9d0..efb06ec 100644 --- a/Stanford360.xcodeproj/project.pbxproj +++ b/Stanford360.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 45E2F0FC2D82132B0097C339 /* HydrationTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = HydrationTests; sourceTree = ""; }; 45E2F12D2D8291E30097C339 /* HydrationUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = HydrationUITests; sourceTree = ""; }; + 4D490EDC2D86A78200C7E06F /* ProteinUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ProteinUITests; sourceTree = ""; }; 4DB472D62D80459F005E895E /* ProteinTest */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ProteinTest; sourceTree = ""; }; 4F8EA0B92D680A4400A94137 /* Activity */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Activity; sourceTree = ""; }; 4FF18DDD2D5FAB5E00E13832 /* ActivityTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ActivityTests; sourceTree = ""; }; @@ -368,6 +369,7 @@ 653A256A28338800005D4D48 /* Stanford360UITests */ = { isa = PBXGroup; children = ( + 4D490EDC2D86A78200C7E06F /* ProteinUITests */, 45E2F12D2D8291E30097C339 /* HydrationUITests */, 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */, 653A256B28338800005D4D48 /* SchedulerTests.swift */, @@ -519,6 +521,7 @@ ); fileSystemSynchronizedGroups = ( 45E2F12D2D8291E30097C339 /* HydrationUITests */, + 4D490EDC2D86A78200C7E06F /* ProteinUITests */, ); name = Stanford360UITests; packageProductDependencies = ( diff --git a/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 75927ff..7dc47d0 100644 --- a/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Stanford360.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a0e5b682a3ad5cf3f0fd5341934e0ec50d4fc6c8448b5abf3b87454d9e7996c3", + "originHash" : "ef7379b8da4e7ffd5608f83fec5fa0a8ecd3d5eda1037c2ddc2332af246890f6", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -487,15 +487,6 @@ "version" : "7.0.2" } }, - { - "identity" : "tppdf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/techprimate/TPPDF", - "state" : { - "revision" : "975049c0e1022922580b7927e030901604024925", - "version" : "2.6.1" - } - }, { "identity" : "xctestextensions", "kind" : "remoteSourceControl", diff --git a/Stanford360/Protein/Model/PromptTemplate.swift b/Stanford360/Protein/Model/PromptTemplate.swift index cddc062..070bfbb 100644 --- a/Stanford360/Protein/Model/PromptTemplate.swift +++ b/Stanford360/Protein/Model/PromptTemplate.swift @@ -9,25 +9,26 @@ // SPDX-License-Identifier: MIT // -// import Foundation -// -// class ProteinPromptConstructor: ObservableObject { -// func constructPrompt(mealName: String) -> String { -// let prompt = """ -// You are an expert in nutritional science with a focus on dietary needs for children aged 10-15. -// Here are some important information that you can refer: -// 1. Those belong to Meats, Poultry, and Fish usually around 6-10 grams. -// 2. Those belong to Soy and Vegetable Protein usually around 3-13 grams. -// 3. Those belong to Legumes and Nuts usually around 5-10 grams. -// 4. Those belong to Milk and Dairy usually around 10 grams. -// 5. Those belong to Vegetables usually around 2 grams. -// 6. Those belong to Grains usually around 5-10 grams. -// Task: -// 1. Analyze the meal name: "\(mealName)" -// 2. Determine the appropriate protein content (in grams) based on nutritional standards for this age group. -// 3. Respond with a single numeric value representing the protein content in grams. -// 4. Do not include any additional text in your response. -// """ -// return prompt -// } -// } + import Foundation + + class ProteinPromptConstructor: ObservableObject { + func constructPrompt(mealName: String) -> String { + let prompt = """ + You are an expert in nutritional science with a focus on dietary needs for children aged 10-15. + Here are some important information that you can refer: + 1. Those belong to Meats, Poultry, and Fish usually around 6-10 grams. + 2. Those belong to Soy and Vegetable Protein usually around 3-13 grams. + 3. Those belong to Legumes and Nuts usually around 5-10 grams. + 4. Those belong to Milk and Dairy usually around 10 grams. + 5. Those belong to Vegetables usually around 5 grams. + 6. Those belong to Grains usually around 10 grams. + 7. Other food contains different components usually has more protein, such as hamburger. + Task: + 1. Analyze the meal name: "\(mealName)" + 2. Determine the appropriate protein content (in grams) based on nutritional standards for this age group. + 3. Respond with a single numeric value representing the protein content in grams. + 4. Do not include any additional text in your response. + """ + return prompt + } + } diff --git a/Stanford360/Protein/Model/ProteinManager.swift b/Stanford360/Protein/Model/ProteinManager.swift index 056f75c..8801135 100644 --- a/Stanford360/Protein/Model/ProteinManager.swift +++ b/Stanford360/Protein/Model/ProteinManager.swift @@ -73,17 +73,6 @@ class ProteinManager: Module, EnvironmentAccessible { let totalIntake = getTodayTotalGrams() return milestoneManager.getLatestMilestone(total: totalIntake) } - - // Add a new meal to the list -// func addMeal(name: String, proteinGrams: Double, imageURL: String? = nil, timestamp: Date = Date()) { -// let newMeal = Meal(name: name, proteinGrams: proteinGrams, imageURL: imageURL, timestamp: timestamp) -// meals.append(newMeal) -// } - - // Delete a meal from the list by its name - // func deleteMeal(byName name: String) { - // meals.removeAll { $0.name == name } - // } // Update an existing meal's details // func updateMeal( diff --git a/Stanford360/Protein/Service/FirebaseManager.swift b/Stanford360/Protein/Service/FirebaseManager.swift index 7d18c7d..7b3628c 100644 --- a/Stanford360/Protein/Service/FirebaseManager.swift +++ b/Stanford360/Protein/Service/FirebaseManager.swift @@ -17,18 +17,6 @@ extension Stanford360Standard { return } -// var updatedMeal = meal -// -// // If an image is selected, upload the image and get its download URL -// if let image = selectedImage { -// if let imageURL = await uploadImageToFirebase(image, imageName: meal.name) { -// updatedMeal.imageURL = imageURL -// print("✅ Image URL uploaded: \(imageURL)") -// } else { -// print("❌ Failed to upload image.") -// } -// } - // store the Meal to Firestore do { let docRef = try await configuration.userDocumentReference diff --git a/Stanford360/Protein/View/AddMealView.swift b/Stanford360/Protein/View/AddMealView.swift index fbd3dd8..2a09168 100644 --- a/Stanford360/Protein/View/AddMealView.swift +++ b/Stanford360/Protein/View/AddMealView.swift @@ -22,7 +22,7 @@ struct AddMealView: View { @Environment(\.dismiss) private var dismiss @Environment(Stanford360Standard.self) private var standard @Environment(ProteinManager.self) private var proteinManager - // @Environment(LLMRunner.self) var runner + @Environment(LLMRunner.self) var runner // LLM runner state for protein // Original state variables @@ -42,7 +42,7 @@ struct AddMealView: View { @State private var classificationOptions: [String] = [] @State private var isProcessing: Bool = false @State private var errorMessage: String? - // @StateObject private var promptTemplate = ProteinPromptConstructor() + @StateObject private var promptTemplate = ProteinPromptConstructor() @StateObject private var classifier = ImageClassifier() var body: some View { @@ -60,9 +60,9 @@ struct AddMealView: View { .onChange(of: selectedImage) { _, newImage in classifier.image = newImage } -// .onChange(of: mealName) { newMealName in -// handleMealNameChange(newMealName) -// } + .onChange(of: mealName) { newMealName in + handleMealNameChange(newMealName) + } .onReceive(Publishers.keyboardHeight) { height in withAnimation(.easeOut(duration: 0.25)) { keyboardOffset = height > 0 ? height - 30 : 0 // Slight adjustment for a more natural feel @@ -98,15 +98,15 @@ struct AddMealView: View { } } -// func handleMealNameChange(_ newMealName: String) { -// if !newMealName.isEmpty { -// Task { -// await getMealProtein(meal: newMealName) -// } -// } else { -// proteinAmount = "" -// } -// } + func handleMealNameChange(_ newMealName: String) { + if !newMealName.isEmpty { + Task { + await getMealProtein(meal: newMealName) + } + } else { + proteinAmount = "" + } + } } // MARK: - Main Content @@ -344,36 +344,36 @@ extension AddMealView { } } -// extension AddMealView { -// func getMealProtein(meal: String) async { -// await MainActor.run { -// self.proteinAmount = "" -// } -// let prompt = promptTemplate.constructPrompt(mealName: meal) -// -// let llmSchema = LLMLocalSchema( -// // model: .custom(id: <#T##String#>), -// model: .llama3_2_1B_4bit, -// parameters: .init( -// systemPrompt: prompt -// ) -// ) -// let llmSession = runner(with: llmSchema) -// var output = "" -// -// do { -// for try await token in try await llmSession.generate() { -// output.append(token) -// } -// await MainActor.run { -// self.proteinAmount = output -// } -// print("Protein extracted is ", proteinAmount) -// } catch { -// print("Error generating protein: \(error)") -// } -// } -// } + extension AddMealView { + func getMealProtein(meal: String) async { + await MainActor.run { + self.proteinAmount = "" + } + let prompt = promptTemplate.constructPrompt(mealName: meal) + + let llmSchema = LLMLocalSchema( + // model: .custom(id: <#T##String#>), + model: .llama3_2_1B_4bit, + parameters: .init( + systemPrompt: prompt + ) + ) + let llmSession = runner(with: llmSchema) + var output = "" + + do { + for try await token in try await llmSession.generate() { + output.append(token) + } + await MainActor.run { + self.proteinAmount = output + } + print("Protein extracted is ", proteinAmount) + } catch { + print("Error generating protein: \(error)") + } + } + } extension AddMealView { diff --git a/Stanford360/Protein/View/LLMLocalOnboardingDownloadView.swift b/Stanford360/Protein/View/LLMLocalOnboardingDownloadView.swift index db41b1e..cb22808 100644 --- a/Stanford360/Protein/View/LLMLocalOnboardingDownloadView.swift +++ b/Stanford360/Protein/View/LLMLocalOnboardingDownloadView.swift @@ -11,22 +11,22 @@ // SPDX-License-Identifier: MIT // -// import SpeziLLM -// import SpeziLLMLocal -// import SpeziLLMLocalDownload -// import SpeziOnboarding -// import SwiftUI -// -// struct LLMLocalOnboardingDownloadView: View { -// @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath -// -// -// var body: some View { -// LLMLocalDownloadView( -// model: .llama3_2_1B_4bit, -// downloadDescription: "The Llama3 1B model will be downloaded" -// ) { -// onboardingNavigationPath.nextStep() -// } -// } -// } + import SpeziLLM + import SpeziLLMLocal + import SpeziLLMLocalDownload + import SpeziOnboarding + import SwiftUI + + struct LLMLocalOnboardingDownloadView: View { + @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath + + + var body: some View { + LLMLocalDownloadView( + model: .llama3_2_1B_4bit, + downloadDescription: "The Llama3 1B model will be downloaded" + ) { + onboardingNavigationPath.nextStep() + } + } + } diff --git a/Stanford360/Stanford360Delegate.swift b/Stanford360/Stanford360Delegate.swift index 60af89b..8f64e06 100644 --- a/Stanford360/Stanford360Delegate.swift +++ b/Stanford360/Stanford360Delegate.swift @@ -65,9 +65,10 @@ class Stanford360Delegate: SpeziAppDelegate { ProteinManager() HealthKitManager() PatientManager() - LLMRunner { - LLMLocalPlatform() - } + + LLMRunner { + LLMLocalPlatform() + } } } diff --git a/Stanford360UITests/ProteinUITests/MealDetailViewTests.swift b/Stanford360UITests/ProteinUITests/MealDetailViewTests.swift new file mode 100644 index 0000000..64522cd --- /dev/null +++ b/Stanford360UITests/ProteinUITests/MealDetailViewTests.swift @@ -0,0 +1,51 @@ +// This source file is part of the Stanford 360 based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import XCTest + +final class MealDetailViewTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding"] + app.launch() + + let dontAllowIdentifier = app.buttons["UIA.Health.AuthSheet.CancelButton"] + if dontAllowIdentifier.waitForExistence(timeout: 5) { + dontAllowIdentifier.tap() + } + } + + // Test Meal Detail View Displays Information Correctly + @MainActor + func testMealDetailViewDisplaysInfo() throws { + let app = XCUIApplication() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5)) + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Protein"].exists) + app.tabBars["Tab Bar"].buttons["Protein"].tap() + + let historyButton = app.segmentedControls.buttons["History"] + XCTAssertTrue(historyButton.waitForExistence(timeout: 2), "History tab should exist") + historyButton.tap() + + let mealEntry = app.staticTexts["mealLogEntry"] + XCTAssertTrue(mealEntry.waitForExistence(timeout: 2), "Meal log entry should exist") + mealEntry.tap() + + let mealName = app.staticTexts["mealName"] + XCTAssertTrue(mealName.waitForExistence(timeout: 2), "Meal name should be displayed") + + let proteinContent = app.staticTexts["Protein Content"] + XCTAssertTrue(proteinContent.waitForExistence(timeout: 2), "Protein content card should be visible") + + let intakeTime = app.staticTexts["Intake time"] + XCTAssertTrue(intakeTime.waitForExistence(timeout: 2), "Intake time card should be visible") + } +} diff --git a/Stanford360UITests/ProteinUITests/MealHistoryTests.swift b/Stanford360UITests/ProteinUITests/MealHistoryTests.swift new file mode 100644 index 0000000..8dc4f43 --- /dev/null +++ b/Stanford360UITests/ProteinUITests/MealHistoryTests.swift @@ -0,0 +1,51 @@ +// This source file is part of the Stanford 360 based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import XCTest + +final class MealHistoryTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding"] + app.launch() + + let dontAllowIdentifier = app.buttons["UIA.Health.AuthSheet.CancelButton"] + if dontAllowIdentifier.waitForExistence(timeout: 5) { + dontAllowIdentifier.tap() + } + } + + // Test Meal History Displays Entries + @MainActor + func testMealHistoryDisplaysLogs() throws { + let app = XCUIApplication() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5)) + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Protein"].exists) + app.tabBars["Tab Bar"].buttons["Protein"].tap() + + let historyButton = app.segmentedControls.buttons["History"] + XCTAssertTrue(historyButton.waitForExistence(timeout: 2), "History tab should exist") + historyButton.tap() + + let addButton = app.buttons["Add"] + addButton.tap() + + let logButton = app.buttons["logMealButton"] + XCTAssertTrue(app.staticTexts["Chicken Breast"].waitForExistence(timeout: 2), "Preset button for Chicken Breast should exist") + app.staticTexts["Chicken Breast"].tap() + logButton.tap() + + historyButton.tap() + + let mealLogEntry = app.staticTexts["mealLogEntry"] + XCTAssertTrue(mealLogEntry.waitForExistence(timeout: 2), "Meal log entry should be displayed if logs exist.") + } +} diff --git a/Stanford360UITests/ProteinUITests/ProteinTabViewTests.swift b/Stanford360UITests/ProteinUITests/ProteinTabViewTests.swift new file mode 100644 index 0000000..de13b42 --- /dev/null +++ b/Stanford360UITests/ProteinUITests/ProteinTabViewTests.swift @@ -0,0 +1,46 @@ +// This source file is part of the Stanford 360 based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import XCTest + +final class ProteinTabViewTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding"] + app.launch() + + let dontAllowIdentifier = app.buttons["UIA.Health.AuthSheet.CancelButton"] + if dontAllowIdentifier.waitForExistence(timeout: 5) { + dontAllowIdentifier.tap() + } + } + + // Test Protein Tab View Navigation + @MainActor + func testProteinTabViewNavigation() throws { + let app = XCUIApplication() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5)) + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Protein"].exists) + app.tabBars["Tab Bar"].buttons["Protein"].tap() + + let addTab = app.segmentedControls.buttons["Add"] + XCTAssertTrue(addTab.waitForExistence(timeout: 2), "Add tab should exist") + addTab.tap() + + let historyTab = app.segmentedControls.buttons["History"] + XCTAssertTrue(historyTab.waitForExistence(timeout: 2), "History tab should exist") + historyTab.tap() + + let discoverTab = app.segmentedControls.buttons["Discover"] + XCTAssertTrue(discoverTab.waitForExistence(timeout: 2), "Discover tab should exist") + discoverTab.tap() + } +} diff --git a/Stanford360UITests/ProteinUITests/ProteinViewTests.swift b/Stanford360UITests/ProteinUITests/ProteinViewTests.swift new file mode 100644 index 0000000..d22b109 --- /dev/null +++ b/Stanford360UITests/ProteinUITests/ProteinViewTests.swift @@ -0,0 +1,41 @@ +// This source file is part of the Stanford 360 based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2025 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import XCTest + +final class ProteinViewTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding"] + app.launch() + + let dontAllowIdentifier = app.buttons["UIA.Health.AuthSheet.CancelButton"] + if dontAllowIdentifier.waitForExistence(timeout: 5) { + dontAllowIdentifier.tap() + } + } + + // Test Add Protein Button Functionality + @MainActor + func testAddProteinButton() throws { + let app = XCUIApplication() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5)) + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Protein"].exists) + app.tabBars["Tab Bar"].buttons["Protein"].tap() + + let addProteinButton = app.buttons["Add Protein Button"] + XCTAssertTrue(addProteinButton.waitForExistence(timeout: 2), "Add Protein button should exist") + addProteinButton.tap() + + let addMealView = app.otherElements["AddMealView"] + XCTAssertTrue(addMealView.waitForExistence(timeout: 2), "Add Meal View should appear when Add Protein button is tapped") + } +}